Closer/functions/dist/backup/cleanupRestoreRequests.test.js

129 lines
8.1 KiB
JavaScript
Raw Normal View History

"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