From f5a02c2d291373c05bdcf614f2589ea60ba43f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 6 Aug 2026 09:35:43 +0200 Subject: [PATCH] test(02-11): cover guardian egress races - Exercise revocation and expiry against blocked channel egress\n- Require a direct retention index for notification events --- .../guardian-notification-fanout.test.ts | 120 +++++++++++++++++- .../notification-attribution.test.ts | 15 +++ 2 files changed, 132 insertions(+), 3 deletions(-) diff --git a/tests/integration/guardian-notification-fanout.test.ts b/tests/integration/guardian-notification-fanout.test.ts index 9dff5de63..22eb3f266 100644 --- a/tests/integration/guardian-notification-fanout.test.ts +++ b/tests/integration/guardian-notification-fanout.test.ts @@ -14,6 +14,11 @@ const sent = vi.hoisted(() => ({ apns: [] as Array<{ recipientUserId: string; recordUserId?: string }>, })); +const ntfyEgress = vi.hoisted(() => ({ + block: null as Promise | null, + onStart: null as (() => void) | null, +})); + vi.mock("@/lib/notifications/senders/ntfy", async () => { const { recordPushAttemptForPayload } = await vi.importActual< typeof import("@/lib/notifications/senders/push-attempt-record") @@ -28,9 +33,11 @@ vi.mock("@/lib/notifications/senders/ntfy", async () => { eventType: string; title: string; message: string; - }, - ) => { - const recipientUserId = payload.recipientUserId ?? payload.userId; + }, + ) => { + ntfyEgress.onStart?.(); + if (ntfyEgress.block) await ntfyEgress.block; + const recipientUserId = payload.recipientUserId ?? payload.userId; sent.ntfy.push({ recipientUserId, title: payload.title, @@ -136,7 +143,9 @@ vi.mock("@/lib/notifications/senders/telegram", async () => { import { encrypt } from "@/lib/crypto"; import type { Locale } from "@/lib/i18n/config"; +import { withManagedProfileLock } from "@/lib/managed-profiles/lifecycle"; import { dispatchNotification } from "@/lib/notifications/dispatcher"; +import { revokeGrant } from "@/lib/sharing/grants"; import { getPrismaClient, truncateAllTables } from "./setup"; let sequence = 0; @@ -188,6 +197,8 @@ async function addChannel( } beforeEach(async () => { + ntfyEgress.block = null; + ntfyEgress.onStart = null; sent.deliveries.length = 0; sent.ntfy.length = 0; sent.apns.length = 0; @@ -379,4 +390,107 @@ describe("Guardian fan-out (real Postgres)", () => { }), ).resolves.toBe(expected.length); }); + + it("keeps a Guardian revocation from committing while its channel egress is in flight", async () => { + const [record, recipient, reserveGuardian] = await Promise.all([ + createUser({ label: "record", locale: "en", managed: true }), + createUser({ label: "recipient", locale: "en" }), + createUser({ label: "reserve-guardian", locale: "de" }), + ]); + await Promise.all([ + addGuardian(record.id, recipient.id), + addGuardian(record.id, reserveGuardian.id), + addChannel(recipient.id, "NTFY"), + ]); + const recipientGrant = await getPrismaClient().accountGrant.findFirstOrThrow({ + where: { grantorId: record.id, granteeId: recipient.id }, + select: { id: true }, + }); + let releaseEgress!: () => void; + let markEgressStarted!: () => void; + ntfyEgress.block = new Promise((resolve) => { + releaseEgress = resolve; + }); + const egressStarted = new Promise((resolve) => { + markEgressStarted = resolve; + }); + ntfyEgress.onStart = markEgressStarted; + + const delivery = dispatchNotification({ + eventType: "MEDICATION_REMINDER", + userId: record.id, + title: "record reminder", + message: "record message", + }); + await egressStarted; + + const revocation = revokeGrant({ + grantId: recipientGrant.id, + grantorId: record.id, + }); + await expectPromiseToRemainPending(revocation); + + releaseEgress(); + await delivery; + await revocation; + }); + + it("keeps a Guardian expiry transition from committing while its channel egress is in flight", async () => { + const [record, recipient, reserveGuardian] = await Promise.all([ + createUser({ label: "record", locale: "en", managed: true }), + createUser({ label: "recipient", locale: "en" }), + createUser({ label: "reserve-guardian", locale: "de" }), + ]); + await Promise.all([ + addGuardian(record.id, recipient.id), + addGuardian(record.id, reserveGuardian.id), + addChannel(recipient.id, "NTFY"), + ]); + const recipientGrant = await getPrismaClient().accountGrant.findFirstOrThrow({ + where: { grantorId: record.id, granteeId: recipient.id }, + select: { id: true }, + }); + let releaseEgress!: () => void; + let markEgressStarted!: () => void; + ntfyEgress.block = new Promise((resolve) => { + releaseEgress = resolve; + }); + const egressStarted = new Promise((resolve) => { + markEgressStarted = resolve; + }); + ntfyEgress.onStart = markEgressStarted; + + const delivery = dispatchNotification({ + eventType: "MEDICATION_REMINDER", + userId: record.id, + title: "record reminder", + message: "record message", + }); + await egressStarted; + + const expiry = getPrismaClient().$transaction((tx) => + withManagedProfileLock(tx, record.id, () => + tx.accountGrant.update({ + where: { id: recipientGrant.id }, + data: { expiresAt: new Date() }, + }), + ), + ); + await expectPromiseToRemainPending(expiry); + + releaseEgress(); + await delivery; + await expiry; + }); }); + +async function expectPromiseToRemainPending(promise: Promise) { + const settled = await Promise.race([ + promise.then( + () => true, + () => true, + ), + new Promise((resolve) => setTimeout(() => resolve(false), 25)), + ]); + expect(settled).toBe(false); +} diff --git a/tests/integration/notification-attribution.test.ts b/tests/integration/notification-attribution.test.ts index 7f9e411b5..408ba65b0 100644 --- a/tests/integration/notification-attribution.test.ts +++ b/tests/integration/notification-attribution.test.ts @@ -141,6 +141,21 @@ describe("notification attribution (real Postgres)", () => { ).toBeNull(); }); + it("provides the created-at index used by global notification event retention", async () => { + const indexes = await getPrismaClient().$queryRaw< + Array<{ indexname: string }> + >` + SELECT indexname + FROM pg_indexes + WHERE schemaname = current_schema() + AND tablename = 'notification_events' + `; + + expect(indexes.map(({ indexname }) => indexname)).toContain( + "notification_events_created_at_idx", + ); + }); + it("allows exactly one concurrent claim for a notification event", async () => { const recordUser = await createUser("claim-record", true); const client = getPrismaClient();