Files
HealthLog/tests/integration/notification-attribution.test.ts
T
Marc-André Bombeck 9380ea0f18 feat: separate notification event anchors
- Store record-scoped scheduler events outside delivery attempts\n- Keep delivery diagnostics free of bookkeeping rows
2026-08-06 04:32:52 +02:00

367 lines
11 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
import { resolveNotificationDeliveryIdentity } from "@/lib/notifications/delivery-identity";
import {
hasReminderDedupAnchor,
writeReminderDedupAnchor,
} from "@/lib/notifications/reminder-dedup";
import { recordPushAttempt } from "@/lib/notifications/senders/push-attempt-record";
let sequence = 0;
async function createUser(label: string, managed = false) {
const suffix = sequence++;
return getPrismaClient().user.create({
data: {
username: `${label}-${suffix}`,
email: `${label}-${suffix}@example.test`,
managedProfileAt: managed ? new Date() : null,
},
});
}
async function createGuardianGrant(input: {
recordUserId: string;
recipientUserId: string;
access?: "READ" | "WRITE" | "MANAGE";
acceptedAt?: Date | null;
revokedAt?: Date | null;
expiresAt?: Date | null;
}) {
return getPrismaClient().accountGrant.create({
data: {
grantorId: input.recordUserId,
granteeId: input.recipientUserId,
access: input.access ?? "MANAGE",
acceptedAt: input.acceptedAt === undefined ? new Date() : input.acceptedAt,
revokedAt: input.revokedAt ?? null,
revokedBy: input.revokedAt ? "GRANTOR" : null,
expiresAt: input.expiresAt ?? null,
},
});
}
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
});
describe("notification attribution (real Postgres)", () => {
it("persistence retains distinct record and recipient principals", async () => {
const recordUser = await createUser("record", true);
const recipientUser = await createUser("recipient");
recordPushAttempt({
recordUserId: recordUser.id,
recipientUserId: recipientUser.id,
channel: "NTFY",
eventType: "MEDICATION_REMINDER",
result: "ok",
});
await vi.waitFor(async () => {
const attempt = await getPrismaClient().pushAttempt.findFirstOrThrow({
where: {
recordUserId: recordUser.id,
recipientUserId: recipientUser.id,
},
select: {
recordUserId: true,
recipientUserId: true,
channel: true,
eventType: true,
},
});
expect(attempt).toEqual({
recordUserId: recordUser.id,
recipientUserId: recipientUser.id,
channel: "NTFY",
eventType: "MEDICATION_REMINDER",
});
});
});
it("persistence leaves explicit self attribution unchanged", async () => {
const self = await createUser("self");
const attempt = await getPrismaClient().pushAttempt.create({
data: {
userId: self.id,
recordUserId: self.id,
recipientUserId: self.id,
channel: "NTFY",
eventType: "MEDICATION_REMINDER",
result: "ok",
},
select: {
userId: true,
recordUserId: true,
recipientUserId: true,
},
});
expect(attempt).toEqual({
userId: self.id,
recordUserId: self.id,
recipientUserId: self.id,
});
});
it("persists a managed reminder anchor as a record event without a delivery attempt", async () => {
const recordUser = await createUser("managed-record", true);
const client = getPrismaClient();
const now = new Date();
await writeReminderDedupAnchor(client, {
userId: recordUser.id,
eventType: "MEDICATION_REMINDER",
reason: "med:slot",
});
await expect(
hasReminderDedupAnchor(client, {
userId: recordUser.id,
eventType: "MEDICATION_REMINDER",
reason: "med:slot",
now,
}),
).resolves.toBe(true);
expect(await client.pushAttempt.count()).toBe(0);
const event = await client.notificationEvent.findFirstOrThrow({
where: { recordUserId: recordUser.id, dedupKey: "med:slot" },
select: { id: true },
});
await client.user.delete({ where: { id: recordUser.id } });
expect(
await client.notificationEvent.findUnique({ where: { id: event.id } }),
).toBeNull();
});
it("persistence maps an omitted pair from the legacy recipient", async () => {
const self = await createUser("legacy");
const attempt = await getPrismaClient().pushAttempt.create({
data: {
userId: self.id,
channel: "NTFY",
eventType: "MEDICATION_REMINDER",
result: "ok",
},
select: {
recordUserId: true,
recipientUserId: true,
},
});
expect(attempt).toEqual({
recordUserId: self.id,
recipientUserId: self.id,
});
});
it("persistence rejects a partial or contradictory principal pair atomically", async () => {
const recordUser = await createUser("record", true);
const recipientUser = await createUser("recipient");
const otherUser = await createUser("other");
await expect(
getPrismaClient().pushAttempt.create({
data: {
userId: recipientUser.id,
recordUserId: recordUser.id,
channel: "NTFY",
eventType: "MEDICATION_REMINDER",
result: "ok",
},
}),
).rejects.toThrow("push attempt attribution requires both principals");
await expect(
getPrismaClient().pushAttempt.create({
data: {
userId: otherUser.id,
recordUserId: recordUser.id,
recipientUserId: recipientUser.id,
channel: "NTFY",
eventType: "MEDICATION_REMINDER",
result: "ok",
},
}),
).rejects.toThrow("push attempt recipient must match user_id");
expect(await getPrismaClient().pushAttempt.count()).toBe(0);
});
it("persistence refuses a managed recipient and cascades an attributed recipient deletion", async () => {
const recordUser = await createUser("record", true);
const recipientUser = await createUser("recipient");
const managedRecipient = await createUser("managed-recipient", true);
await expect(
getPrismaClient().pushAttempt.create({
data: {
userId: managedRecipient.id,
recordUserId: recordUser.id,
recipientUserId: managedRecipient.id,
channel: "NTFY",
eventType: "MEDICATION_REMINDER",
result: "ok",
},
}),
).rejects.toThrow("managed profile cannot receive notification delivery");
const attempt = await getPrismaClient().pushAttempt.create({
data: {
userId: recipientUser.id,
recordUserId: recordUser.id,
recipientUserId: recipientUser.id,
channel: "NTFY",
eventType: "MEDICATION_REMINDER",
result: "ok",
},
});
await getPrismaClient().user.delete({ where: { id: recipientUser.id } });
expect(
await getPrismaClient().pushAttempt.findUnique({
where: { id: attempt.id },
}),
).toBeNull();
});
it("principal resolution requires an active MANAGE grant for managed delivery", async () => {
const recordUser = await createUser("record", true);
const recipientUser = await createUser("recipient");
const ordinaryRecord = await createUser("ordinary");
await expect(
resolveNotificationDeliveryIdentity({
eventType: "MEDICATION_REMINDER",
userId: recordUser.id,
recordUserId: recordUser.id,
recipientUserId: recipientUser.id,
title: "Record content",
message: "Record schedule",
}),
).resolves.toBeNull();
await createGuardianGrant({
recordUserId: recordUser.id,
recipientUserId: recipientUser.id,
});
await expect(
resolveNotificationDeliveryIdentity({
eventType: "MEDICATION_REMINDER",
userId: recordUser.id,
recordUserId: recordUser.id,
recipientUserId: recipientUser.id,
title: "Record content",
message: "Record schedule",
}),
).resolves.toEqual({
recordUserId: recordUser.id,
recipientUserId: recipientUser.id,
managed: true,
});
await expect(
resolveNotificationDeliveryIdentity({
eventType: "MEDICATION_REMINDER",
userId: recordUser.id,
title: "Record content",
message: "Record schedule",
}),
).resolves.toBeNull();
await expect(
resolveNotificationDeliveryIdentity({
eventType: "MEDICATION_REMINDER",
userId: ordinaryRecord.id,
recordUserId: ordinaryRecord.id,
recipientUserId: recipientUser.id,
title: "Record content",
message: "Record schedule",
}),
).resolves.toBeNull();
});
it("principal resolution rejects inactive, insufficient, wrong-record, and managed-recipient grants", async () => {
const recordUser = await createUser("record", true);
const otherRecord = await createUser("other-record", true);
const pendingRecipient = await createUser("pending-recipient");
const revokedRecipient = await createUser("revoked-recipient");
const expiredRecipient = await createUser("expired-recipient");
const writeRecipient = await createUser("write-recipient");
const wrongRecordRecipient = await createUser("wrong-record-recipient");
const managedRecipient = await createUser("managed-recipient", true);
await Promise.all([
createGuardianGrant({
recordUserId: recordUser.id,
recipientUserId: pendingRecipient.id,
acceptedAt: null,
}),
createGuardianGrant({
recordUserId: recordUser.id,
recipientUserId: revokedRecipient.id,
revokedAt: new Date(),
}),
createGuardianGrant({
recordUserId: recordUser.id,
recipientUserId: expiredRecipient.id,
expiresAt: new Date(Date.now() - 1_000),
}),
createGuardianGrant({
recordUserId: recordUser.id,
recipientUserId: writeRecipient.id,
access: "WRITE",
}),
createGuardianGrant({
recordUserId: otherRecord.id,
recipientUserId: wrongRecordRecipient.id,
}),
createGuardianGrant({
recordUserId: recordUser.id,
recipientUserId: managedRecipient.id,
}),
]);
for (const recipientUserId of [
pendingRecipient.id,
revokedRecipient.id,
expiredRecipient.id,
writeRecipient.id,
wrongRecordRecipient.id,
managedRecipient.id,
]) {
await expect(
resolveNotificationDeliveryIdentity({
eventType: "MEDICATION_REMINDER",
userId: recordUser.id,
recordUserId: recordUser.id,
recipientUserId,
title: "Record content",
message: "Record schedule",
}),
).resolves.toBeNull();
}
const ordinaryUser = await createUser("ordinary-self");
await expect(
resolveNotificationDeliveryIdentity({
eventType: "MEDICATION_REMINDER",
userId: ordinaryUser.id,
title: "Personal content",
message: "Personal schedule",
}),
).resolves.toEqual({
recordUserId: ordinaryUser.id,
recipientUserId: ordinaryUser.id,
managed: false,
});
});
});