From bd6220bbba0d245ed3c20a715da213aacf1fce58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 5 Aug 2026 23:26:42 +0200 Subject: [PATCH] test: add sharing security principals --- src/__tests__/v137-wave-zero-fixtures.test.ts | 56 +++++++++++++ .../fixtures/v137/legacy-account-payloads.ts | 49 ++++++++++++ tests/fixtures/v137/notification-matrix.ts | 39 ++++++++++ tests/fixtures/v137/security-principals.ts | 78 +++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 tests/fixtures/v137/legacy-account-payloads.ts create mode 100644 tests/fixtures/v137/notification-matrix.ts create mode 100644 tests/fixtures/v137/security-principals.ts diff --git a/src/__tests__/v137-wave-zero-fixtures.test.ts b/src/__tests__/v137-wave-zero-fixtures.test.ts index 8549c4073..b199186e9 100644 --- a/src/__tests__/v137-wave-zero-fixtures.test.ts +++ b/src/__tests__/v137-wave-zero-fixtures.test.ts @@ -4,6 +4,12 @@ import { ADMITTED_MUTATING_HANDLERS, SHARING_DOMAINS, } from "../../tests/fixtures/v137/sharing-matrix"; +import { SECURITY_PRINCIPALS } from "../../tests/fixtures/v137/security-principals"; +import { LEGACY_ACCOUNT_PAYLOADS } from "../../tests/fixtures/v137/legacy-account-payloads"; +import { + NOTIFICATION_CHANNELS, + NOTIFICATION_DELIVERY_MATRIX, +} from "../../tests/fixtures/v137/notification-matrix"; describe("sharing and handler inventories", () => { it("freezes the exact sharing-domain vocabulary", () => { @@ -41,3 +47,53 @@ describe("sharing and handler inventories", () => { } }); }); + +describe("security principals and compatibility", () => { + it("keeps actor, record, recipient, origin, grant, and job identities distinct", () => { + const { activeDelegation, revokedDelegation } = SECURITY_PRINCIPALS; + + expect(new Set(Object.values(activeDelegation.identities)).size).toBe(3); + expect(activeDelegation.origin).toBe("delegated-request"); + expect(activeDelegation.grant.state).toBe("active"); + expect(revokedDelegation.grant.state).toBe("revoked"); + expect(activeDelegation.job.id).not.toBe(revokedDelegation.job.id); + }); + + it("covers every guardian, profile, and delivery channel exactly once", () => { + expect(NOTIFICATION_CHANNELS).toEqual([ + "apns", + "web-push", + "telegram", + "ntfy", + ]); + expect(NOTIFICATION_DELIVERY_MATRIX).toHaveLength(16); + expect( + new Set( + NOTIFICATION_DELIVERY_MATRIX.map( + ({ channel, recipientUserId, recordUserId }) => + `${recordUserId}:${recipientUserId}:${channel}`, + ), + ).size, + ).toBe(NOTIFICATION_DELIVERY_MATRIX.length); + expect( + NOTIFICATION_DELIVERY_MATRIX.every( + ({ interactive, recipientUserId, recordUserId }) => + !interactive && recipientUserId !== recordUserId, + ), + ).toBe(true); + }); + + it("retains legacy access compatibility and fails closed for scoped payloads", () => { + expect(LEGACY_ACCOUNT_PAYLOADS.map(({ name }) => name)).toEqual([ + "whole-read", + "whole-write", + "manage-as-write", + "scoped-fail-closed", + ]); + expect(LEGACY_ACCOUNT_PAYLOADS[2]).toMatchObject({ + access: "write", + level: "manage", + }); + expect(LEGACY_ACCOUNT_PAYLOADS[3]?.legacyDecoder).toBe("deny"); + }); +}); diff --git a/tests/fixtures/v137/legacy-account-payloads.ts b/tests/fixtures/v137/legacy-account-payloads.ts new file mode 100644 index 000000000..7375b3426 --- /dev/null +++ b/tests/fixtures/v137/legacy-account-payloads.ts @@ -0,0 +1,49 @@ +export interface LegacyAccountPayloadFixture { + readonly name: + "whole-read" | "whole-write" | "manage-as-write" | "scoped-fail-closed"; + readonly access: "read" | "write"; + readonly level: "read" | "write" | "manage"; + readonly sections: readonly string[] | null; + readonly canWrite: boolean; + readonly recordKind: "shared" | "managed"; + readonly legacyDecoder: "allow" | "deny"; +} + +export const LEGACY_ACCOUNT_PAYLOADS = [ + { + name: "whole-read", + access: "read", + level: "read", + sections: null, + canWrite: false, + recordKind: "shared", + legacyDecoder: "allow", + }, + { + name: "whole-write", + access: "write", + level: "write", + sections: null, + canWrite: true, + recordKind: "shared", + legacyDecoder: "allow", + }, + { + name: "manage-as-write", + access: "write", + level: "manage", + sections: null, + canWrite: true, + recordKind: "managed", + legacyDecoder: "allow", + }, + { + name: "scoped-fail-closed", + access: "read", + level: "read", + sections: ["measurements"], + canWrite: false, + recordKind: "shared", + legacyDecoder: "deny", + }, +] as const satisfies readonly LegacyAccountPayloadFixture[]; diff --git a/tests/fixtures/v137/notification-matrix.ts b/tests/fixtures/v137/notification-matrix.ts new file mode 100644 index 000000000..6bdf12c35 --- /dev/null +++ b/tests/fixtures/v137/notification-matrix.ts @@ -0,0 +1,39 @@ +export const NOTIFICATION_CHANNELS = [ + "apns", + "web-push", + "telegram", + "ntfy", +] as const; + +export type NotificationChannel = (typeof NOTIFICATION_CHANNELS)[number]; + +const GUARDIAN_RECIPIENTS = [ + { id: "guardian-alpha", locale: "en" }, + { id: "guardian-beta", locale: "de" }, +] as const; + +const MANAGED_RECORDS = [ + "managed-profile-alpha", + "managed-profile-beta", +] as const; + +export interface NotificationDeliveryFixture { + readonly recordUserId: (typeof MANAGED_RECORDS)[number]; + readonly recipientUserId: (typeof GUARDIAN_RECIPIENTS)[number]["id"]; + readonly recipientLocale: (typeof GUARDIAN_RECIPIENTS)[number]["locale"]; + readonly channel: NotificationChannel; + readonly interactive: false; +} + +export const NOTIFICATION_DELIVERY_MATRIX = MANAGED_RECORDS.flatMap( + (recordUserId) => + GUARDIAN_RECIPIENTS.flatMap(({ id: recipientUserId, locale }) => + NOTIFICATION_CHANNELS.map((channel) => ({ + recordUserId, + recipientUserId, + recipientLocale: locale, + channel, + interactive: false, + })), + ), +) as readonly NotificationDeliveryFixture[]; diff --git a/tests/fixtures/v137/security-principals.ts b/tests/fixtures/v137/security-principals.ts new file mode 100644 index 000000000..bcf9ffafd --- /dev/null +++ b/tests/fixtures/v137/security-principals.ts @@ -0,0 +1,78 @@ +export type ExternalOrigin = "owner-request" | "delegated-request"; +export type GrantState = "active" | "revoked" | "not-required"; + +export interface SecurityPrincipalFixture { + readonly identities: { + readonly actorUserId: string; + readonly recordUserId: string; + readonly recipientUserId: string; + }; + readonly origin: ExternalOrigin; + readonly grant: { + readonly id: string | null; + readonly state: GrantState; + }; + readonly job: { + readonly id: string; + readonly origin: ExternalOrigin; + }; +} + +export const SECURITY_PRINCIPALS = { + activeDelegation: { + identities: { + actorUserId: "guardian-alpha", + recordUserId: "managed-profile-alpha", + recipientUserId: "guardian-beta", + }, + origin: "delegated-request", + grant: { id: "grant-active-alpha", state: "active" }, + job: { id: "job-active-alpha", origin: "delegated-request" }, + }, + revokedDelegation: { + identities: { + actorUserId: "guardian-alpha", + recordUserId: "managed-profile-alpha", + recipientUserId: "guardian-beta", + }, + origin: "delegated-request", + grant: { id: "grant-revoked-alpha", state: "revoked" }, + job: { id: "job-revoked-alpha", origin: "delegated-request" }, + }, + ownerProviderPositiveControl: { + identities: { + actorUserId: "owner-alpha", + recordUserId: "owner-alpha", + recipientUserId: "owner-alpha", + }, + origin: "owner-request", + grant: { id: null, state: "not-required" }, + job: { id: "job-owner-provider", origin: "owner-request" }, + }, + ownerIdempotencyPositiveControl: { + identities: { + actorUserId: "owner-beta", + recordUserId: "owner-beta", + recipientUserId: "owner-beta", + }, + origin: "owner-request", + grant: { id: null, state: "not-required" }, + job: { id: "job-owner-idempotency", origin: "owner-request" }, + }, +} as const satisfies Record; + +export async function mutateThenRestore( + mutate: () => Promise, + restore: (result: T) => Promise, +): Promise { + let result: T; + let completed = false; + + try { + result = await mutate(); + completed = true; + return result; + } finally { + if (completed) await restore(result as T); + } +}