// ---------------------------------------------------------------------------// The membership mirror's RECONCILER (`workos-events-sync.ts`) and the// webhook that pokes it (`workos-webhook.ts`), against the real PGlite// Postgres every cloud unit test runs on (scripts/test-globalsetup.ts).// WorkOS is a fake `WorkOSClient` for the Events API (the emulator has no// events route); the signature check runs the REAL client's verifier over a// locally computed HMAC, because that check is the webhook's only// authentication.//// What this pins:// - every followed event type lands in the mirror: user created/updated/// deleted, membership created/updated/deleted, organization renamed// - an older event never regresses a newer row (`stale`) — an older// organization rename included — a replayed delete is `absent`, and// `organization.deleted` MARKS the org deleted (refusing every membership// authorization) without purging anything;// replayed, or after cloud's own flow marked it first, it is `absent`// - `organization.deleted` for an org the mirror has never seen MINTS a// tombstone row, so a login that fetched a membership of it before the// deletion cannot mint the org or the membership afterwards// - a delete TOMBSTONES its row as of the event's `createdAt`, so an older// payload replayed after it is `stale` and the row stays inactive — even// when the delete arrives before the mirror has ever seen the membership// (the reconciler ahead of the backfill): the tombstone is minted, with// its organization, so the backfill cannot insert the row live// - a membership created or updated for a member the mirror holds no// profile for reads the profile from WorkOS (one `getUser`, only then),// so a pre-boundary user who joins a scanned org is searchable by name// and email; a member WorkOS no longer has is mirrored bare, and a// transient failure fails the run// - a membership for an organization the mirror has never seen mirrors// the org first (one WorkOS read), so the foreign key holds; one whose// org WorkOS no longer has marks the org deleted instead (minting the// tombstone) and the cursor still advances, while a transient WorkOS// failure still fails the run// - `organization.updated` never inserts an org the mirror does not hold// - a run with no cursor reads from the backfill's replay boundary, and// with no boundary either reads nothing (the backfill has not run)// - a run pages from the persisted cursor, commits after every page, and// STOPS when another run moves the cursor under it — with NOTHING from// the contended page written (a lagging run cannot resurrect a// membership the leading run already deleted)// - a run that reads the stream to its end records the drain as of its// start (the authorization path's caught-up check); a run that read// nothing, or yielded the stream, records none// - the webhook accepts only a genuinely signed delivery, never applies// it, and refuses everything when no signing secret is configured// --------------------------------------------------------------------------- import { createHmac } from "node:crypto"; import { describe, expect, it } from "@effect/vitest";import { sql } from "drizzle-orm";import { Effect, Exit, Layer, Option } from "effect";import { HttpRouter, HttpServer } from "effect/unstable/http";import type { Organization, OrganizationMembership, User } from "@workos-inc/node/worker"; import { MemberDirectory, type MemberStatus } from "@executor-js/api/server"; import { DbService } from "../db/db";import { UserStoreService } from "./context";import { WorkOSError } from "./errors";import { cloudMemberDirectoryLayer } from "./member-directory";import { mirrorSignIn } from "./mirror-feeders";import { authorizeOrganization } from "./organization";import { WorkOSClient, type WorkOSClientService, type WorkOSListEventsOptions } from "./workos";import { planEvent, syncWorkOsEvents, type WorkOsEventOutcome, type WorkOsEventsSyncReport, type WorkOsMirroredEvent,} from "./workos-events-sync";import { WorkOsMirror, WorkOsMirrorWrite, mirrorMembershipFromWorkOs } from "./workos-mirror";import { WORKOS_WEBHOOK_PATH, makeWorkOsWebhookRoute } from "./workos-webhook"; const T1 = "2026-01-01T00:00:00.000Z";const T2 = "2026-01-02T00:00:00.000Z";const T3 = "2026-01-03T00:00:00.000Z"; // Synthetic identities only; every test mints its own ids so the shared test// database never couples two tests.const freshId = (prefix: string) => `${prefix}_${crypto.randomUUID().replaceAll("-", "")}`; const workosUser = (id: string, overrides: Partial<User> = {}): User => ({ object: "user", id, email: `${id}@placeholder.test`, emailVerified: true, firstName: "Ada", lastName: "Placeholder", profilePictureUrl: null, lastSignInAt: T1, locale: null, createdAt: T1, updatedAt: T1, externalId: null, metadata: {}, ...overrides,}); const workosMembership = ( userId: string, organizationId: string, overrides: Partial<OrganizationMembership> = {},): OrganizationMembership => ({ object: "organization_membership", id: `om_${userId}_${organizationId}`, userId, organizationId, organizationName: `Org ${organizationId}`, status: "active", directoryManaged: false, createdAt: T1, updatedAt: T1, customAttributes: {}, role: { slug: "member" }, ...overrides,}); const workosOrganization = (id: string, name: string, updatedAt = T1): Organization => ({ object: "organization", id, name, allowProfilesOutsideOrganization: false, domains: [], createdAt: T1, updatedAt, externalId: null, metadata: {},}); const userEvent = ( event: "user.created" | "user.updated" | "user.deleted", data: User, id = freshId("event"), createdAt = T1,): WorkOsMirroredEvent => ({ id, event, data, createdAt, context: undefined,}); const membershipEvent = ( event: | "organization_membership.created" | "organization_membership.updated" | "organization_membership.deleted", data: OrganizationMembership, id = freshId("event"), createdAt = T1,): WorkOsMirroredEvent => ({ id, event, data, createdAt, context: undefined,}); const organizationEvent = ( event: "organization.updated" | "organization.deleted", data: Organization, id = freshId("event"), createdAt = T1,): WorkOsMirroredEvent => ({ id, event, data, createdAt, context: undefined,}); /** * A `WorkOSClient` whose every method is one of `methods`; anything else is * an unexpected call and dies, so a reconciler that silently adds a WorkOS * read fails the test instead of passing on a fake. */const stubWorkOS = (methods: Partial<WorkOSClientService>) => Layer.succeed( WorkOSClient, new Proxy({} as WorkOSClientService, { get: (_target, prop) => (methods as Record<PropertyKey, unknown>)[prop] ?? (() => Effect.die(`unexpected WorkOSClient.${String(prop)} call`)), }), ); /** * `getUser` for every member a test's membership events name: the * reconciler reads a profile for a member the mirror holds none for, and * the strict stub above would die on it. Records each read in `reads`. */const profiles = (reads: string[] = []): Partial<WorkOSClientService> => ({ getUser: (userId) => Effect.sync(() => { reads.push(userId); return workosUser(userId); }),}); const DbLive = DbService.Live;// The authorization checks below always read the mirror, never WorkOS.const MirrorServices = Layer.mergeAll( WorkOsMirror.Live, UserStoreService.Live, cloudMemberDirectoryLayer,).pipe(Layer.provideMerge(DbLive)); type Services = WorkOsMirror | UserStoreService | MemberDirectory | DbService | WorkOSClient; const run = <A, E>( body: Effect.Effect<A, E, Services>, workos: Layer.Layer<WorkOSClient> = stubWorkOS({}),) => Effect.runPromise( body.pipe(Effect.provide(Layer.mergeAll(MirrorServices, workos)), Effect.scoped), ); const seedOrganization = (id: string) => Effect.flatMap(UserStoreService.asEffect(), (users) => users.use("upsertOrganization", (s) => s.upsertOrganization({ id, name: `Org ${id}`, updatedAt: new Date(T1) }), ), ); const readOrganization = (id: string) => Effect.flatMap(UserStoreService.asEffect(), (users) => users.use("getOrganization", (s) => s.getOrganization(id)), ); const readMembership = ( accountId: string, organizationId: string, statuses?: readonly MemberStatus[],) => Effect.flatMap(MemberDirectory.asEffect(), (directory) => directory.membership(accountId, organizationId, statuses), ); /** * Apply one event the way a run does — plan it, then apply it as a one-event * page under the cursor CAS — and report the event's outcome. The cursor is * instance-wide; each apply moves it to a fresh id, which is what a run does. */const applyEvent = (event: WorkOsMirroredEvent) => Effect.gen(function* () { const mirror = yield* WorkOsMirror; const write = yield* planEvent(event); const prev = yield* mirror.getCursor(); const outcomes = yield* mirror.applyPage(prev, freshId("event"), [write]); expect(Option.isSome(outcomes), "no other run contends in a single-event apply").toBe(true); const outcome: WorkOsEventOutcome | undefined = Option.getOrElse(outcomes, () => [])[0]; expect(outcome, "one write, one outcome").toBeDefined(); return outcome ?? "absent"; }); describe("applyEvent", () => { it("mirrors a user, refreshes it, refuses an older update, and deletes it", async () => { const org = freshId("org"); const userId = freshId("user"); const result = await run( Effect.gen(function* () { yield* seedOrganization(org); yield* applyEvent( membershipEvent("organization_membership.created", workosMembership(userId, org)), ); const created = yield* applyEvent( userEvent("user.created", workosUser(userId, { firstName: "Grace", updatedAt: T2 })), ); const afterCreate = yield* readMembership(userId, org); const updated = yield* applyEvent( userEvent("user.updated", workosUser(userId, { firstName: "Newer", updatedAt: T3 })), ); const afterUpdate = yield* readMembership(userId, org); const stale = yield* applyEvent( userEvent("user.updated", workosUser(userId, { firstName: "Stale", updatedAt: T1 })), ); const afterStale = yield* readMembership(userId, org); // The delete is stamped with the EVENT's time (T3), after every // payload above; the SDK payload's own `updatedAt` predates it. const deleted = yield* applyEvent( userEvent("user.deleted", workosUser(userId), freshId("event"), T3), ); const afterDelete = yield* readMembership(userId, org); const tombstoned = yield* readMembership(userId, org, ["inactive"]); // A profile update that happened before the delete but lands after it. const lateUpdate = yield* applyEvent( userEvent("user.updated", workosUser(userId, { firstName: "Late", updatedAt: T2 })), ); const afterLate = yield* readMembership(userId, org, ["inactive"]); return { created, afterCreate, updated, afterUpdate, stale, afterStale, deleted, afterDelete, tombstoned, lateUpdate, afterLate, }; }), stubWorkOS(profiles()), ); expect(result.created).toBe("applied"); expect(result.afterCreate?.name).toBe("Grace Placeholder"); expect(result.updated).toBe("applied"); expect(result.afterUpdate?.name).toBe("Newer Placeholder"); expect(result.stale, "an event older than the stored row is reported stale").toBe("stale"); expect(result.afterStale?.name, "and leaves the newer row untouched").toBe("Newer Placeholder"); expect(result.deleted).toBe("applied"); expect(result.afterDelete, "deleting the user tombstones its membership").toBeNull(); expect(result.tombstoned, "the row stays, inactive, with the profile cleared").toMatchObject({ status: "inactive", name: null, email: null, }); expect(result.lateUpdate, "a payload older than the deletion is refused").toBe("stale"); expect(result.afterLate).toMatchObject({ status: "inactive", name: null }); }); it("mirrors a membership, updates its role, refuses an older update, and deletes it", async () => { const org = freshId("org"); const userId = freshId("user"); const result = await run( Effect.gen(function* () { yield* seedOrganization(org); const created = yield* applyEvent( membershipEvent( "organization_membership.created", workosMembership(userId, org, { status: "pending" }), ), ); const afterCreate = yield* readMembership(userId, org); const updated = yield* applyEvent( membershipEvent( "organization_membership.updated", workosMembership(userId, org, { role: { slug: "admin" }, updatedAt: T3, }), ), ); const afterUpdate = yield* readMembership(userId, org); const stale = yield* applyEvent( membershipEvent( "organization_membership.updated", workosMembership(userId, org, { role: { slug: "member" }, status: "inactive", updatedAt: T2, }), ), ); const afterStale = yield* readMembership(userId, org); // The delete is stamped with the EVENT's time (T3), after every // payload above; the SDK payload's own `updatedAt` predates it. const deleted = yield* applyEvent( membershipEvent( "organization_membership.deleted", workosMembership(userId, org), freshId("event"), T3, ), ); const afterDelete = yield* readMembership(userId, org); const deletedAgain = yield* applyEvent( membershipEvent( "organization_membership.deleted", workosMembership(userId, org), freshId("event"), T3, ), ); // A membership update that happened before the delete but lands // after it (a lagging feeder, an out-of-order delivery): refused, the // tombstone stands. const lateUpdate = yield* applyEvent( membershipEvent( "organization_membership.updated", workosMembership(userId, org, { role: { slug: "admin" }, updatedAt: T2, }), ), ); const afterLate = yield* readMembership(userId, org, ["inactive"]); return { created, afterCreate, updated, afterUpdate, stale, afterStale, deleted, afterDelete, deletedAgain, lateUpdate, afterLate, }; }), stubWorkOS(profiles()), ); expect(result.created).toBe("applied"); expect(result.afterCreate).toMatchObject({ membershipId: `om_${userId}_${org}`, status: "pending", role: "member", }); expect(result.updated).toBe("applied"); expect(result.afterUpdate).toMatchObject({ status: "active", role: "admin", }); expect(result.stale).toBe("stale"); expect(result.afterStale).toMatchObject({ status: "active", role: "admin", }); expect(result.deleted).toBe("applied"); expect(result.afterDelete, "a tombstone reads as no membership").toBeNull(); expect(result.deletedAgain, "a replayed delete changes nothing").toBe("absent"); expect(result.lateUpdate, "a payload older than the deletion is refused").toBe("stale"); expect(result.afterLate).toMatchObject({ status: "inactive", role: "admin", }); }); it("tombstones a membership the mirror has never seen, mirroring its organization first, so the backfill cannot insert it live", async () => { const org = freshId("org"); const userId = freshId("user"); const reads: string[] = []; const result = await run( Effect.gen(function* () { // The deletion lands before any feeder wrote the membership or the // org; the org is read from WorkOS so the tombstone row can exist. const deleted = yield* applyEvent( membershipEvent( "organization_membership.deleted", workosMembership(userId, org), freshId("event"), T2, ), ); const organization = yield* readOrganization(org); const tombstone = yield* readMembership(userId, org, ["inactive"]); // The backfill, listing WorkOS as it was before the deletion, writes // the membership afterwards: refused, the tombstone stands. const mirror = yield* WorkOsMirror; const backfilled = yield* mirror.upsertMembership( mirrorMembershipFromWorkOs(workosMembership(userId, org)), ); const afterBackfill = yield* readMembership(userId, org); return { deleted, organization, tombstone, backfilled, afterBackfill }; }), stubWorkOS({ getOrganization: (id) => { reads.push(id); return Effect.succeed(workosOrganization(id, "Dashboard Org")); }, }), ); expect(reads, "exactly one WorkOS read, for the unknown org").toEqual([org]); expect(result.deleted, "the delete leaves a tombstone behind").toBe("applied"); expect(result.organization?.name).toBe("Dashboard Org"); expect(result.tombstone).toMatchObject({ membershipId: `om_${userId}_${org}`, status: "inactive", }); expect(result.backfilled, "the pre-deletion payload is refused").toBe(false); expect(result.afterBackfill, "and the member is not live").toBeNull(); }); it("mirrors the organization first when a membership names one the mirror has never seen", async () => { const org = freshId("org"); const userId = freshId("user"); const reads: string[] = []; const result = await run( Effect.gen(function* () { const outcome = yield* applyEvent( membershipEvent("organization_membership.created", workosMembership(userId, org)), ); const organization = yield* readOrganization(org); const membership = yield* readMembership(userId, org); return { outcome, organization, membership }; }), stubWorkOS({ ...profiles(reads), getOrganization: (id) => { reads.push(id); return Effect.succeed(workosOrganization(id, "Dashboard Org")); }, }), );Lines 1–500 of 1313. Load more
// --------------------------------------------------------------------------- // The membership mirror's RECONCILER (`workos-events-sync.ts`) and the // webhook that pokes it (`workos-webhook.ts`), against the real PGlite // Postgres every cloud unit test runs on (scripts/test-globalsetup.ts). // WorkOS is a fake `WorkOSClient` for the Events API (the emulator has no // events route); the signature check runs the REAL client's verifier over a // locally computed HMAC, because that check is the webhook's only // authentication. // // What this pins: // - every followed event type lands in the mirror: user created/updated/ // deleted, membership created/updated/deleted, organization renamed // - an older event never regresses a newer row (`stale`) — an older // organization rename included — a replayed delete is `absent`, and // `organization.deleted` MARKS the org deleted (refusing every membership // authorization) without purging anything; // replayed, or after cloud's own flow marked it first, it is `absent` // - `organization.deleted` for an org the mirror has never seen MINTS a // tombstone row, so a login that fetched a membership of it before the // deletion cannot mint the org or the membership afterwards // - a delete TOMBSTONES its row as of the event's `createdAt`, so an older // payload replayed after it is `stale` and the row stays inactive — even // when the delete arrives before the mirror has ever seen the membership // (the reconciler ahead of the backfill): the tombstone is minted, with // its organization, so the backfill cannot insert the row live // - a membership created or updated for a member the mirror holds no // profile for reads the profile from WorkOS (one `getUser`, only then), // so a pre-boundary user who joins a scanned org is searchable by name // and email; a member WorkOS no longer has is mirrored bare, and a // transient failure fails the run // - a membership for an organization the mirror has never seen mirrors // the org first (one WorkOS read), so the foreign key holds; one whose // org WorkOS no longer has marks the org deleted instead (minting the // tombstone) and the cursor still advances, while a transient WorkOS // failure still fails the run // - `organization.updated` never inserts an org the mirror does not hold // - a run with no cursor reads from the backfill's replay boundary, and // with no boundary either reads nothing (the backfill has not run) // - a run pages from the persisted cursor, commits after every page, and // STOPS when another run moves the cursor under it — with NOTHING from // the contended page written (a lagging run cannot resurrect a // membership the leading run already deleted) // - a run that reads the stream to its end records the drain as of its // start (the authorization path's caught-up check); a run that read // nothing, or yielded the stream, records none // - the webhook accepts only a genuinely signed delivery, never applies // it, and refuses everything when no signing secret is configured // --------------------------------------------------------------------------- import { createHmac } from "node:crypto"; import { describe, expect, it } from "@effect/vitest"; import { sql } from "drizzle-orm"; import { Effect, Exit, Layer, Option } from "effect"; import { HttpRouter, HttpServer } from "effect/unstable/http"; import type { Organization, OrganizationMembership, User } from "@workos-inc/node/worker"; import { MemberDirectory, type MemberStatus } from "@executor-js/api/server"; import { DbService } from "../db/db"; import { UserStoreService } from "./context"; import { WorkOSError } from "./errors"; import { cloudMemberDirectoryLayer } from "./member-directory"; import { mirrorSignIn } from "./mirror-feeders"; import { authorizeOrganization } from "./organization"; import { WorkOSClient, type WorkOSClientService, type WorkOSListEventsOptions } from "./workos"; import { planEvent, syncWorkOsEvents, type WorkOsEventOutcome, type WorkOsEventsSyncReport, type WorkOsMirroredEvent, } from "./workos-events-sync"; import { WorkOsMirror, WorkOsMirrorWrite, mirrorMembershipFromWorkOs } from "./workos-mirror"; import { WORKOS_WEBHOOK_PATH, makeWorkOsWebhookRoute } from "./workos-webhook"; const T1 = "2026-01-01T00:00:00.000Z"; const T2 = "2026-01-02T00:00:00.000Z"; const T3 = "2026-01-03T00:00:00.000Z"; // Synthetic identities only; every test mints its own ids so the shared test // database never couples two tests. const freshId = (prefix: string) => `${prefix}_${crypto.randomUUID().replaceAll("-", "")}`; const workosUser = (id: string, overrides: Partial<User> = {}): User => ({ object: "user", id, email: `${id}@placeholder.test`, emailVerified: true, firstName: "Ada", lastName: "Placeholder", profilePictureUrl: null, lastSignInAt: T1, locale: null, createdAt: T1, updatedAt: T1, externalId: null, metadata: {}, ...overrides, }); const workosMembership = ( userId: string, organizationId: string, overrides: Partial<OrganizationMembership> = {}, ): OrganizationMembership => ({ object: "organization_membership", id: `om_${userId}_${organizationId}`, userId, organizationId, organizationName: `Org ${organizationId}`, status: "active", directoryManaged: false, createdAt: T1, updatedAt: T1, customAttributes: {}, role: { slug: "member" }, ...overrides, }); const workosOrganization = (id: string, name: string, updatedAt = T1): Organization => ({ object: "organization", id, name, allowProfilesOutsideOrganization: false, domains: [], createdAt: T1, updatedAt, externalId: null, metadata: {}, }); const userEvent = ( event: "user.created" | "user.updated" | "user.deleted", data: User, id = freshId("event"), createdAt = T1, ): WorkOsMirroredEvent => ({ id, event, data, createdAt, context: undefined, }); const membershipEvent = ( event: | "organization_membership.created" | "organization_membership.updated" | "organization_membership.deleted", data: OrganizationMembership, id = freshId("event"), createdAt = T1, ): WorkOsMirroredEvent => ({ id, event, data, createdAt, context: undefined, }); const organizationEvent = ( event: "organization.updated" | "organization.deleted", data: Organization, id = freshId("event"), createdAt = T1, ): WorkOsMirroredEvent => ({ id, event, data, createdAt, context: undefined, }); /** * A `WorkOSClient` whose every method is one of `methods`; anything else is * an unexpected call and dies, so a reconciler that silently adds a WorkOS * read fails the test instead of passing on a fake. */ const stubWorkOS = (methods: Partial<WorkOSClientService>) => Layer.succeed( WorkOSClient, new Proxy({} as WorkOSClientService, { get: (_target, prop) => (methods as Record<PropertyKey, unknown>)[prop] ?? (() => Effect.die(`unexpected WorkOSClient.${String(prop)} call`)), }), ); /** * `getUser` for every member a test's membership events name: the * reconciler reads a profile for a member the mirror holds none for, and * the strict stub above would die on it. Records each read in `reads`. */ const profiles = (reads: string[] = []): Partial<WorkOSClientService> => ({ getUser: (userId) => Effect.sync(() => { reads.push(userId); return workosUser(userId); }), }); const DbLive = DbService.Live; // The authorization checks below always read the mirror, never WorkOS. const MirrorServices = Layer.mergeAll( WorkOsMirror.Live, UserStoreService.Live, cloudMemberDirectoryLayer, ).pipe(Layer.provideMerge(DbLive)); type Services = WorkOsMirror | UserStoreService | MemberDirectory | DbService | WorkOSClient; const run = <A, E>( body: Effect.Effect<A, E, Services>, workos: Layer.Layer<WorkOSClient> = stubWorkOS({}), ) => Effect.runPromise( body.pipe(Effect.provide(Layer.mergeAll(MirrorServices, workos)), Effect.scoped), ); const seedOrganization = (id: string) => Effect.flatMap(UserStoreService.asEffect(), (users) => users.use("upsertOrganization", (s) => s.upsertOrganization({ id, name: `Org ${id}`, updatedAt: new Date(T1) }), ), ); const readOrganization = (id: string) => Effect.flatMap(UserStoreService.asEffect(), (users) => users.use("getOrganization", (s) => s.getOrganization(id)), ); const readMembership = ( accountId: string, organizationId: string, statuses?: readonly MemberStatus[], ) => Effect.flatMap(MemberDirectory.asEffect(), (directory) => directory.membership(accountId, organizationId, statuses), ); /** * Apply one event the way a run does — plan it, then apply it as a one-event * page under the cursor CAS — and report the event's outcome. The cursor is * instance-wide; each apply moves it to a fresh id, which is what a run does. */ const applyEvent = (event: WorkOsMirroredEvent) => Effect.gen(function* () { const mirror = yield* WorkOsMirror; const write = yield* planEvent(event); const prev = yield* mirror.getCursor(); const outcomes = yield* mirror.applyPage(prev, freshId("event"), [write]); expect(Option.isSome(outcomes), "no other run contends in a single-event apply").toBe(true); const outcome: WorkOsEventOutcome | undefined = Option.getOrElse(outcomes, () => [])[0]; expect(outcome, "one write, one outcome").toBeDefined(); return outcome ?? "absent"; }); describe("applyEvent", () => { it("mirrors a user, refreshes it, refuses an older update, and deletes it", async () => { const org = freshId("org"); const userId = freshId("user"); const result = await run( Effect.gen(function* () { yield* seedOrganization(org); yield* applyEvent( membershipEvent("organization_membership.created", workosMembership(userId, org)), ); const created = yield* applyEvent( userEvent("user.created", workosUser(userId, { firstName: "Grace", updatedAt: T2 })), ); const afterCreate = yield* readMembership(userId, org); const updated = yield* applyEvent( userEvent("user.updated", workosUser(userId, { firstName: "Newer", updatedAt: T3 })), ); const afterUpdate = yield* readMembership(userId, org); const stale = yield* applyEvent( userEvent("user.updated", workosUser(userId, { firstName: "Stale", updatedAt: T1 })), ); const afterStale = yield* readMembership(userId, org); // The delete is stamped with the EVENT's time (T3), after every // payload above; the SDK payload's own `updatedAt` predates it. const deleted = yield* applyEvent( userEvent("user.deleted", workosUser(userId), freshId("event"), T3), ); const afterDelete = yield* readMembership(userId, org); const tombstoned = yield* readMembership(userId, org, ["inactive"]); // A profile update that happened before the delete but lands after it. const lateUpdate = yield* applyEvent( userEvent("user.updated", workosUser(userId, { firstName: "Late", updatedAt: T2 })), ); const afterLate = yield* readMembership(userId, org, ["inactive"]); return { created, afterCreate, updated, afterUpdate, stale, afterStale, deleted, afterDelete, tombstoned, lateUpdate, afterLate, }; }), stubWorkOS(profiles()), ); expect(result.created).toBe("applied"); expect(result.afterCreate?.name).toBe("Grace Placeholder"); expect(result.updated).toBe("applied"); expect(result.afterUpdate?.name).toBe("Newer Placeholder"); expect(result.stale, "an event older than the stored row is reported stale").toBe("stale"); expect(result.afterStale?.name, "and leaves the newer row untouched").toBe("Newer Placeholder"); expect(result.deleted).toBe("applied"); expect(result.afterDelete, "deleting the user tombstones its membership").toBeNull(); expect(result.tombstoned, "the row stays, inactive, with the profile cleared").toMatchObject({ status: "inactive", name: null, email: null, }); expect(result.lateUpdate, "a payload older than the deletion is refused").toBe("stale"); expect(result.afterLate).toMatchObject({ status: "inactive", name: null }); }); it("mirrors a membership, updates its role, refuses an older update, and deletes it", async () => { const org = freshId("org"); const userId = freshId("user"); const result = await run( Effect.gen(function* () { yield* seedOrganization(org); const created = yield* applyEvent( membershipEvent( "organization_membership.created", workosMembership(userId, org, { status: "pending" }), ), ); const afterCreate = yield* readMembership(userId, org); const updated = yield* applyEvent( membershipEvent( "organization_membership.updated", workosMembership(userId, org, { role: { slug: "admin" }, updatedAt: T3, }), ), ); const afterUpdate = yield* readMembership(userId, org); const stale = yield* applyEvent( membershipEvent( "organization_membership.updated", workosMembership(userId, org, { role: { slug: "member" }, status: "inactive", updatedAt: T2, }), ), ); const afterStale = yield* readMembership(userId, org); // The delete is stamped with the EVENT's time (T3), after every // payload above; the SDK payload's own `updatedAt` predates it. const deleted = yield* applyEvent( membershipEvent( "organization_membership.deleted", workosMembership(userId, org), freshId("event"), T3, ), ); const afterDelete = yield* readMembership(userId, org); const deletedAgain = yield* applyEvent( membershipEvent( "organization_membership.deleted", workosMembership(userId, org), freshId("event"), T3, ), ); // A membership update that happened before the delete but lands // after it (a lagging feeder, an out-of-order delivery): refused, the // tombstone stands. const lateUpdate = yield* applyEvent( membershipEvent( "organization_membership.updated", workosMembership(userId, org, { role: { slug: "admin" }, updatedAt: T2, }), ), ); const afterLate = yield* readMembership(userId, org, ["inactive"]); return { created, afterCreate, updated, afterUpdate, stale, afterStale, deleted, afterDelete, deletedAgain, lateUpdate, afterLate, }; }), stubWorkOS(profiles()), ); expect(result.created).toBe("applied"); expect(result.afterCreate).toMatchObject({ membershipId: `om_${userId}_${org}`, status: "pending", role: "member", }); expect(result.updated).toBe("applied"); expect(result.afterUpdate).toMatchObject({ status: "active", role: "admin", }); expect(result.stale).toBe("stale"); expect(result.afterStale).toMatchObject({ status: "active", role: "admin", }); expect(result.deleted).toBe("applied"); expect(result.afterDelete, "a tombstone reads as no membership").toBeNull(); expect(result.deletedAgain, "a replayed delete changes nothing").toBe("absent"); expect(result.lateUpdate, "a payload older than the deletion is refused").toBe("stale"); expect(result.afterLate).toMatchObject({ status: "inactive", role: "admin", }); }); it("tombstones a membership the mirror has never seen, mirroring its organization first, so the backfill cannot insert it live", async () => { const org = freshId("org"); const userId = freshId("user"); const reads: string[] = []; const result = await run( Effect.gen(function* () { // The deletion lands before any feeder wrote the membership or the // org; the org is read from WorkOS so the tombstone row can exist. const deleted = yield* applyEvent( membershipEvent( "organization_membership.deleted", workosMembership(userId, org), freshId("event"), T2, ), ); const organization = yield* readOrganization(org); const tombstone = yield* readMembership(userId, org, ["inactive"]); // The backfill, listing WorkOS as it was before the deletion, writes // the membership afterwards: refused, the tombstone stands. const mirror = yield* WorkOsMirror; const backfilled = yield* mirror.upsertMembership( mirrorMembershipFromWorkOs(workosMembership(userId, org)), ); const afterBackfill = yield* readMembership(userId, org); return { deleted, organization, tombstone, backfilled, afterBackfill }; }), stubWorkOS({ getOrganization: (id) => { reads.push(id); return Effect.succeed(workosOrganization(id, "Dashboard Org")); }, }), ); expect(reads, "exactly one WorkOS read, for the unknown org").toEqual([org]); expect(result.deleted, "the delete leaves a tombstone behind").toBe("applied"); expect(result.organization?.name).toBe("Dashboard Org"); expect(result.tombstone).toMatchObject({ membershipId: `om_${userId}_${org}`, status: "inactive", }); expect(result.backfilled, "the pre-deletion payload is refused").toBe(false); expect(result.afterBackfill, "and the member is not live").toBeNull(); }); it("mirrors the organization first when a membership names one the mirror has never seen", async () => { const org = freshId("org"); const userId = freshId("user"); const reads: string[] = []; const result = await run( Effect.gen(function* () { const outcome = yield* applyEvent( membershipEvent("organization_membership.created", workosMembership(userId, org)), ); const organization = yield* readOrganization(org); const membership = yield* readMembership(userId, org); return { outcome, organization, membership }; }), stubWorkOS({ ...profiles(reads), getOrganization: (id) => { reads.push(id); return Effect.succeed(workosOrganization(id, "Dashboard Org")); }, }), );