1313 lines · 48.0 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// 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.