"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const cleanupRestoreRequests_1 = require("./cleanupRestoreRequests"); const queueAndPush_1 = require("../notifications/queueAndPush"); jest.mock('../notifications/queueAndPush', () => ({ queueAndPush: jest.fn().mockResolvedValue(undefined) })); const NOW = 1800000000000; describe('shouldReapRestoreRequest', () => { it('keeps a fresh request', () => { expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ status: 'REQUESTED', expiresAt: NOW + 60000 }, NOW)).toBe(false); }); it('keeps an expired request still inside the grace window', () => { // The client already treats it as expired; we wait out the grace so we can never race a // mid-completion restore. expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ status: 'READY', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS + 1000 }, NOW)).toBe(false); }); it('reaps expired requests regardless of status — DECLINED-after-READY still carries the keybox', () => { const expiresAt = NOW - cleanupRestoreRequests_1.GRACE_MS - 1000; for (const status of ['REQUESTED', 'READY', 'DECLINED', 'RESTORED', 'EXPIRED']) { expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ status, expiresAt }, NOW)).toBe(true); } }); it('falls back to createdAt when expiresAt is unusable', () => { expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ createdAt: NOW - cleanupRestoreRequests_1.FALLBACK_TTL_MS - 1 }, NOW)).toBe(true); expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ expiresAt: 0, createdAt: NOW - cleanupRestoreRequests_1.FALLBACK_TTL_MS - 1 }, NOW)).toBe(true); expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ createdAt: NOW - 60000 }, NOW)).toBe(false); }); it('leaves a doc with neither field — delete only on positive evidence', () => { expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({}, NOW)).toBe(false); expect((0, cleanupRestoreRequests_1.shouldReapRestoreRequest)({ expiresAt: 'soon', createdAt: null }, NOW)).toBe(false); }); }); describe('shouldNotifyExpiry', () => { const justExpired = NOW - cleanupRestoreRequests_1.GRACE_MS - 60000; it('notifies for a recently expired request still waiting on someone', () => { expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', expiresAt: justExpired }, NOW)).toBe(true); expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'READY', expiresAt: justExpired }, NOW)).toBe(true); }); it('stays silent for terminal states — RESTORED succeeded, DECLINED was answered', () => { expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'RESTORED', expiresAt: justExpired }, NOW)).toBe(false); expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'DECLINED', expiresAt: justExpired }, NOW)).toBe(false); }); it('stays silent for backlog debris — first deploy must not blast ancient expiries', () => { expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', expiresAt: NOW - cleanupRestoreRequests_1.NOTIFY_WINDOW_MS - cleanupRestoreRequests_1.GRACE_MS - 1000 }, NOW)).toBe(false); }); it('stays silent for the createdAt-fallback branch (no real expiry to report)', () => { expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', createdAt: NOW - cleanupRestoreRequests_1.FALLBACK_TTL_MS - 1 }, NOW)).toBe(false); }); it('never notifies for something it would not reap', () => { expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', expiresAt: NOW + 60000 }, NOW)).toBe(false); expect((0, cleanupRestoreRequests_1.shouldNotifyExpiry)({ status: 'REQUESTED', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS + 1000 }, NOW)).toBe(false); }); }); describe('sweepExpiredRestoreRequests', () => { const mockQueueAndPush = queueAndPush_1.queueAndPush; function fakeDoc(opts) { var _a; const coupleRef = { id: `couple_${opts.id}`, get: jest.fn().mockResolvedValue({ exists: (_a = opts.coupleExists) !== null && _a !== void 0 ? _a : true }), }; return { id: opts.id, data: () => opts.data, ref: { path: `couples/couple_${opts.id}/restore_requests/${opts.id}`, parent: { parent: coupleRef }, delete: opts.deleteError ? jest.fn().mockRejectedValue(opts.deleteError) : jest.fn().mockResolvedValue(undefined), }, }; } function fakeDb(docs) { return { collectionGroup: jest.fn().mockReturnValue({ where: jest.fn().mockReturnValue({ limit: jest.fn().mockReturnValue({ get: jest.fn().mockResolvedValue({ size: docs.length, docs }), }), }), }), }; } beforeEach(() => mockQueueAndPush.mockClear()); it('deletes the expired doc and nudges the requester', async () => { const doc = fakeDoc({ id: 'uidA', data: { status: 'READY', recipientUid: 'uidA', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 } }); const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([doc]), NOW); expect(doc.ref.delete).toHaveBeenCalledTimes(1); expect(mockQueueAndPush).toHaveBeenCalledWith(expect.anything(), 'uidA', expect.objectContaining({ type: 'restore_request_expired' })); expect(counts).toMatchObject({ scanned: 1, reaped: 1, notified: 1, failed: 0 }); }); it('leaves a query hit that fails re-verify', async () => { // The index says expired; the doc says otherwise (clock skew, weird data). The predicate wins. const doc = fakeDoc({ id: 'uidB', data: { status: 'REQUESTED', expiresAt: NOW + 60000 } }); const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([doc]), NOW); expect(doc.ref.delete).not.toHaveBeenCalled(); expect(counts).toMatchObject({ reaped: 0, skipped: 1 }); }); it('skips the nudge for an orphan reap (couple gone) but still deletes', async () => { const doc = fakeDoc({ id: 'uidC', data: { status: 'READY', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 }, coupleExists: false, }); const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([doc]), NOW); expect(doc.ref.delete).toHaveBeenCalledTimes(1); expect(mockQueueAndPush).not.toHaveBeenCalled(); expect(counts).toMatchObject({ reaped: 1, notified: 0 }); }); it('one failing doc never stops the sweep', async () => { const bad = fakeDoc({ id: 'uidD', data: { status: 'READY', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 }, deleteError: new Error('firestore unavailable'), }); const good = fakeDoc({ id: 'uidE', data: { status: 'REQUESTED', recipientUid: 'uidE', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 } }); const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([bad, good]), NOW); expect(good.ref.delete).toHaveBeenCalledTimes(1); expect(counts).toMatchObject({ scanned: 2, reaped: 1, failed: 1 }); }); it('a notify failure costs the nudge, never the reap or the pass', async () => { mockQueueAndPush.mockRejectedValueOnce(new Error('fcm down')); const doc = fakeDoc({ id: 'uidF', data: { status: 'READY', recipientUid: 'uidF', expiresAt: NOW - cleanupRestoreRequests_1.GRACE_MS - 60000 } }); const counts = await (0, cleanupRestoreRequests_1.sweepExpiredRestoreRequests)(fakeDb([doc]), NOW); expect(doc.ref.delete).toHaveBeenCalledTimes(1); expect(counts).toMatchObject({ reaped: 1, notified: 0, failed: 0 }); }); }); //# sourceMappingURL=cleanupRestoreRequests.test.js.map