teqo-backup/tests/int/testDatabaseLease.int.spec.ts

392 lines
12 KiB
TypeScript

// @vitest-environment node
import { getPayload, type Payload } from 'payload'
import { beforeAll, describe, expect, it } from 'vitest'
import config from '@/payload.config'
import {
acquireSharedTestDatabaseLease,
acquireTestDatabaseLease,
CAMPAIGN_INVITE_CONSENT_LEASE_KEY,
ensureInviteConsent,
startTestDatabaseLeaseAcquisition,
waitForAdvisoryLockWaiter,
withInviteConsent,
withMissingInviteConsentFixture,
withMutableConsentFixture,
withSharedTestDatabaseLease,
} from '../helpers/testDatabaseLease'
let payload: Payload
const expectExactWaiter = (
waiting: Awaited<ReturnType<typeof waitForAdvisoryLockWaiter>>,
pid: number,
mode: 'ExclusiveLock' | 'ShareLock',
) => {
expect(waiting).toEqual({
activityDatabaseOID: waiting.databaseOID,
classID: waiting.expectedClassID,
databaseOID: expect.any(Number),
expectedClassID: expect.stringMatching(/^\d+$/),
expectedObjectID: expect.stringMatching(/^\d+$/),
granted: false,
mode,
objectID: waiting.expectedObjectID,
objectSubID: 1,
pid,
virtualTransaction: expect.stringMatching(/^\d+\/\d+$/),
})
expect(waiting.databaseOID).toBeGreaterThan(0)
}
describe('test database lease', () => {
beforeAll(async () => {
payload = await getPayload({ config: await config })
})
it('blocks an independent transaction session until the first lease releases', async () => {
const first = await acquireTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY)
let secondEntered = false
const pendingSecond = startTestDatabaseLeaseAcquisition(
payload,
CAMPAIGN_INVITE_CONSENT_LEASE_KEY,
)
const secondPID = await pendingSecond.backendPID
const secondAcquisition = pendingSecond.acquisition.then((lease) => {
secondEntered = true
return lease
})
const waiting = await waitForAdvisoryLockWaiter(payload, {
key: `test:${CAMPAIGN_INVITE_CONSENT_LEASE_KEY}`,
mode: 'ExclusiveLock',
waiterPID: secondPID,
})
expectExactWaiter(waiting, secondPID, 'ExclusiveLock')
expect(secondEntered).toBe(false)
await first.release()
const second = await secondAcquisition
expect(secondEntered).toBe(true)
await second.release()
})
it('allows shared readers to overlap and blocks an exclusive writer until both release', async () => {
const firstReader = await acquireSharedTestDatabaseLease(
payload,
CAMPAIGN_INVITE_CONSENT_LEASE_KEY,
)
const secondReader = await acquireSharedTestDatabaseLease(
payload,
CAMPAIGN_INVITE_CONSENT_LEASE_KEY,
)
let writerEntered = false
const pendingWriter = startTestDatabaseLeaseAcquisition(
payload,
CAMPAIGN_INVITE_CONSENT_LEASE_KEY,
)
const writerPID = await pendingWriter.backendPID
const writerAcquisition = pendingWriter.acquisition.then((lease) => {
writerEntered = true
return lease
})
const waiting = await waitForAdvisoryLockWaiter(payload, {
key: `test:${CAMPAIGN_INVITE_CONSENT_LEASE_KEY}`,
mode: 'ExclusiveLock',
waiterPID: writerPID,
})
expectExactWaiter(waiting, writerPID, 'ExclusiveLock')
expect(writerEntered).toBe(false)
await firstReader.release()
const stillWaiting = await waitForAdvisoryLockWaiter(payload, {
key: `test:${CAMPAIGN_INVITE_CONSENT_LEASE_KEY}`,
mode: 'ExclusiveLock',
waiterPID: writerPID,
})
expectExactWaiter(stillWaiting, writerPID, 'ExclusiveLock')
expect(stillWaiting.virtualTransaction).toBe(waiting.virtualTransaction)
expect(writerEntered).toBe(false)
await secondReader.release()
const writer = await writerAcquisition
expect(writerEntered).toBe(true)
await writer.release()
})
it('blocks shared readers while an exclusive writer is active', async () => {
const writer = await acquireTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY)
let readerEntered = false
const pendingReader = startTestDatabaseLeaseAcquisition(
payload,
CAMPAIGN_INVITE_CONSENT_LEASE_KEY,
'shared',
)
const readerPID = await pendingReader.backendPID
const readerAcquisition = pendingReader.acquisition.then((lease) => {
readerEntered = true
return lease
})
const waiting = await waitForAdvisoryLockWaiter(payload, {
key: `test:${CAMPAIGN_INVITE_CONSENT_LEASE_KEY}`,
mode: 'ShareLock',
waiterPID: readerPID,
})
expectExactWaiter(waiting, readerPID, 'ShareLock')
expect(readerEntered).toBe(false)
await writer.release()
const reader = await readerAcquisition
expect(readerEntered).toBe(true)
await reader.release()
})
it('creates a missing configured consent only once under concurrent ensures', async () => {
await withMissingInviteConsentFixture(payload, async () => {
const ensured = await Promise.all([
ensureInviteConsent(payload),
ensureInviteConsent(payload),
ensureInviteConsent(payload),
])
const configured = await withSharedTestDatabaseLease(
payload,
CAMPAIGN_INVITE_CONSENT_LEASE_KEY,
() =>
payload.find({
collection: 'consent',
where: { key: { equals: 'lideranca-autopreenchimento' } },
depth: 0,
limit: 10,
}),
)
expect(new Set(ensured.map((document) => document.id))).toHaveLength(1)
expect(configured.docs).toHaveLength(1)
})
})
it('releases a shared consent scope after its callback fails', async () => {
const callbackFailure = new Error('intentional shared callback failure')
await expect(
withInviteConsent(payload, async () => {
throw callbackFailure
}),
).rejects.toBe(callbackFailure)
const writer = await acquireTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY)
await writer.release()
})
it('restores the exact configured consent after deletion and recreation', async () => {
const before = await withInviteConsent(payload, async (consent) => consent)
await withMutableConsentFixture(payload, async (consent) => {
await payload.delete({ collection: 'consent', id: consent.id })
await payload.create({
collection: 'consent',
data: {
key: 'lideranca-autopreenchimento',
text: {
root: {
type: 'root',
children: [
{
type: 'paragraph',
children: [{ type: 'text', text: 'Temporary recreation', version: 1 }],
direction: null,
format: '',
indent: 0,
version: 1,
},
],
direction: null,
format: '',
indent: 0,
version: 1,
},
},
},
depth: 0,
})
})
const after = await withInviteConsent(payload, async (consent) => consent)
expect(after).toEqual(before)
})
it('restores the configured consent and releases after a callback assertion failure', async () => {
const before = await withSharedTestDatabaseLease(
payload,
CAMPAIGN_INVITE_CONSENT_LEASE_KEY,
() =>
payload.find({
collection: 'consent',
where: { key: { equals: 'lideranca-autopreenchimento' } },
depth: 0,
limit: 1,
}),
)
const callbackFailure = new Error('intentional callback assertion failure')
await expect(
withMutableConsentFixture(payload, async (consent) => {
await payload.update({
collection: 'consent',
id: consent.id,
data: {
key: 'temporarily-renamed-consent',
text: {
root: {
type: 'root',
children: [
{
type: 'paragraph',
children: [{ type: 'text', text: 'Temporary text', version: 1 }],
direction: null,
format: '',
indent: 0,
version: 1,
},
],
direction: null,
format: '',
indent: 0,
version: 1,
},
},
},
})
throw callbackFailure
}),
).rejects.toBe(callbackFailure)
const after = await withSharedTestDatabaseLease(
payload,
CAMPAIGN_INVITE_CONSENT_LEASE_KEY,
() =>
payload.find({
collection: 'consent',
where: { key: { equals: 'lideranca-autopreenchimento' } },
depth: 0,
limit: 1,
}),
)
if (before.docs[0]) {
expect(after.docs[0]).toMatchObject({
id: before.docs[0].id,
key: before.docs[0].key,
text: before.docs[0].text,
})
} else {
expect(after.docs).toHaveLength(0)
}
const subsequent = await acquireTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY)
await subsequent.release()
})
it('aggregates callback and restore lease acquisition failures in original order', async () => {
const callbackFailure = new Error('intentional callback failure')
const restoreAcquisitionFailure = new Error('intentional restore acquisition failure')
const failure = await withMissingInviteConsentFixture(
payload,
async () => {
throw callbackFailure
},
{
beforeRestoreLeaseAcquire: async () => {
throw restoreAcquisitionFailure
},
},
).catch((error: unknown) => error)
expect(failure).toBeInstanceOf(AggregateError)
expect((failure as AggregateError).errors).toEqual([callbackFailure, restoreAcquisitionFailure])
})
it('aggregates snapshot setup and setup lease release failures in original order', async () => {
const snapshotFailure = new Error('intentional snapshot setup failure')
const releaseFailure = new Error('intentional setup release failure')
const failure = await withMissingInviteConsentFixture(payload, async () => undefined, {
beforeSnapshotSetup: async () => {
throw snapshotFailure
},
beforeSetupLeaseRelease: async () => {
throw releaseFailure
},
}).catch((error: unknown) => error)
expect(failure).toBeInstanceOf(AggregateError)
expect((failure as AggregateError).errors).toEqual([snapshotFailure, releaseFailure])
const subsequent = await acquireTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY)
await subsequent.release()
})
it('rolls back a failed missing-consent setup and preserves rollback errors', async () => {
const before = await withInviteConsent(payload, async (consent) => consent)
const releaseFailure = new Error('intentional setup release failure after deletion')
const rollbackFailure = new Error('intentional setup rollback cleanup failure')
let callbackCalled = false
const failure = await withMissingInviteConsentFixture(
payload,
async () => {
callbackCalled = true
},
{
beforeSetupLeaseRelease: async () => {
throw releaseFailure
},
beforeSetupLeaseRollback: async () => {
throw rollbackFailure
},
},
).catch((error: unknown) => error)
expect(callbackCalled).toBe(false)
expect(failure).toBeInstanceOf(AggregateError)
expect((failure as AggregateError).errors).toEqual([releaseFailure, rollbackFailure])
const after = await withInviteConsent(payload, async (consent) => consent)
expect(after).toEqual(before)
const subsequent = await acquireTestDatabaseLease(payload, CAMPAIGN_INVITE_CONSENT_LEASE_KEY)
await subsequent.release()
})
it('keeps an absent consent absent when missing-consent setup release fails', async () => {
const consentKey = `absent-consent-${Date.now()}-${Math.random()}`
const leaseKey = `absent-consent-lease-${Date.now()}-${Math.random()}`
const releaseFailure = new Error('intentional absent setup release failure')
const failure = await withMissingInviteConsentFixture(
payload,
async () => {
throw new Error('callback must not run')
},
{
consentKey,
leaseKey,
beforeSetupLeaseRelease: async () => {
throw releaseFailure
},
},
).catch((error: unknown) => error)
expect(failure).toBe(releaseFailure)
const configured = await payload.find({
collection: 'consent',
where: { key: { equals: consentKey } },
depth: 0,
limit: 1,
})
expect(configured.docs).toHaveLength(0)
const subsequent = await acquireTestDatabaseLease(payload, leaseKey)
await subsequent.release()
})
})