129 lines
8.1 KiB
JavaScript
129 lines
8.1 KiB
JavaScript
"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
|