394 lines · 13.5 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Cloud's PostHog-backed enterprise-managed rollout gate.//// The gate is exercised through its production seam: the real// `makePostHogEnterpriseManagedRollout` with an injected `fetch` and an// injected `waitUntil`, exactly as `auth/jwks-cache.node.test.ts` drives the// JWKS client. Nothing is module-mocked, so the request these tests read is the// request PostHog would receive.// --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest";import { Effect } from "effect"; import type { EnterpriseManagedRolloutContext } from "@executor-js/sdk"; import {  ENTERPRISE_MANAGED_AUTH_FLAG_KEY,  cloudEnterpriseManagedRollout,  makePostHogEnterpriseManagedRollout,} from "./ema-rollout"; const HOST = "https://us.i.posthog.com";const PROJECT_KEY = "phc_test_project_key"; const CONTEXT: EnterpriseManagedRolloutContext = {  userId: "user_01ABC",  organizationId: "org_01XYZ",  integration: "slack" as EnterpriseManagedRolloutContext["integration"],}; interface CapturedRequest {  readonly url: string;  readonly body: Record<string, unknown>;  /** The bound the gate put on this call, when it set one. */  readonly signal: AbortSignal | null;} interface FetchHarness {  readonly fetch: typeof globalThis.fetch;  readonly requests: () => readonly CapturedRequest[];  /** Resolve every request the gate detached, so an assertion cannot race it. */  readonly settle: () => Promise<void>;  readonly waitUntil: (work: Promise<unknown>) => void;} /** * Stands in for PostHog. `respond` decides what each call returns; throwing * from it models a transport failure, and an aborted signal models the timeout. */const makeFetchHarness = (  respond: (request: CapturedRequest) => Response | Promise<Response>,): FetchHarness => {  const requests: CapturedRequest[] = [];  const detached: Promise<unknown>[] = [];  return {    requests: () => requests,    settle: async () => {      await Promise.all(detached);    },    waitUntil: (work) => {      detached.push(work);    },    fetch: async (input, init) => {      const raw = typeof init?.body === "string" ? init.body : "{}";      const captured: CapturedRequest = {        url: String(input),        // oxlint-disable-next-line executor/no-json-parse -- boundary: test fixture reads back the exact JSON body the gate serialized for PostHog        body: JSON.parse(raw) as Record<string, unknown>,        signal: init?.signal instanceof AbortSignal ? init.signal : null,      };      requests.push(captured);      return respond(captured);    },  };}; const jsonResponse = (body: unknown, status = 200): Response =>  new Response(JSON.stringify(body), {    status,    headers: { "content-type": "application/json" },  }); const flagsBody = (enabled: boolean) => ({  flags: {    [ENTERPRISE_MANAGED_AUTH_FLAG_KEY]: {      key: ENTERPRISE_MANAGED_AUTH_FLAG_KEY,      enabled,      reason: { code: "condition_match" },    },  },  errorsWhileComputingFlags: false,}); const gate = (harness: FetchHarness, timeoutMs?: number) =>  makePostHogEnterpriseManagedRollout({    projectKey: PROJECT_KEY,    host: HOST,    fetch: harness.fetch,    waitUntil: harness.waitUntil,    ...(timeoutMs === undefined ? {} : { timeoutMs }),  }); describe("flag evaluation", () => {  it.effect("keys the rollout on the user and carries the org as group context", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => jsonResponse(flagsBody(true)));       const decision = yield* gate(harness).decide(CONTEXT);       expect(decision).toEqual({ kind: "enabled" });      const [request] = harness.requests();      expect(request?.url).toBe(`${HOST}/flags?v=2`);      expect(        request?.body.distinct_id,        "the rollout unit is the user, matching posthog.identify(user.id) in the browser",      ).toBe(CONTEXT.userId);      expect(        request?.body.groups,        "the org rides along so group targeting stays available without changing the rollout unit",      ).toEqual({ organization: CONTEXT.organizationId });      expect(request?.body.api_key).toBe(PROJECT_KEY);      expect(request?.signal, "the evaluation is always bounded").not.toBeNull();    }),  );   it.effect("omits group context when the host named no organization", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => jsonResponse(flagsBody(true)));       yield* gate(harness).decide({ ...CONTEXT, organizationId: null });       expect(harness.requests()[0]?.body.groups).toBeUndefined();    }),  );   it.effect("withholds when the flag is off for this user", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => jsonResponse(flagsBody(false)));       expect(yield* gate(harness).decide(CONTEXT)).toEqual({        kind: "withheld",        reason: "disabled",      });    }),  );   it.effect("withholds when PostHog returned no verdict for the flag at all", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() =>        // What a quota-limited project answers.        jsonResponse({          flags: {},          errorsWhileComputingFlags: false,          quotaLimited: ["feature_flags"],        }),      );       expect(yield* gate(harness).decide(CONTEXT)).toEqual({        kind: "withheld",        reason: "disabled",      });    }),  );}); describe("failing closed", () => {  it.effect("withholds on a non-2xx answer", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => new Response("nope", { status: 503 }));       expect(yield* gate(harness).decide(CONTEXT)).toEqual({        kind: "withheld",        reason: "evaluation-unavailable",      });    }),  );   it.effect("withholds on a transport failure", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => {        // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: models `fetch` rejecting with a TypeError, which is exactly how a Worker sees a dead upstream        throw new TypeError("network error");      });       expect(yield* gate(harness).decide(CONTEXT)).toEqual({        kind: "withheld",        reason: "evaluation-unavailable",      });    }),  );   it.effect("withholds when the evaluation times out", () =>    Effect.gen(function* () {      // Never answers. Only the gate's own AbortSignal ends this call, so the      // test fails by hanging if the gate ever stops bounding the evaluation.      const harness = makeFetchHarness(        (request) =>          new Promise<Response>((_resolve, reject) => {            request.signal?.addEventListener("abort", () =>              // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: fetch-compatible fixture mirrors the platform's abort rejection semantics              reject(new DOMException("aborted", "AbortError")),            );          }),      );       expect(        yield* gate(harness, 1).decide(CONTEXT),        "a slow flag service must degrade the rollout, never hold up a connect",      ).toEqual({ kind: "withheld", reason: "evaluation-unavailable" });    }),  );   it.effect("withholds on a body that is not a flags response", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => jsonResponse({ flags: "not-an-object" }));       expect(yield* gate(harness).decide(CONTEXT)).toEqual({        kind: "withheld",        reason: "evaluation-unavailable",      });    }),  );   it.effect("withholds when there is no acting user to roll out by", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => jsonResponse(flagsBody(true)));       expect(yield* gate(harness).decide({ ...CONTEXT, userId: null })).toEqual({        kind: "withheld",        reason: "evaluation-unavailable",      });      expect(harness.requests().length, "and asks PostHog nothing").toBe(0);    }),  );   it.effect("withholds when the deployment has no PostHog configuration", () =>    Effect.gen(function* () {      // The cloud test env carries no VITE_PUBLIC_POSTHOG_KEY, so this builds      // the misconfigured-deployment gate — which is deliberately NOT the same      // as a host that injects no gate at all.      expect(yield* cloudEnterpriseManagedRollout().decide(CONTEXT)).toEqual({        kind: "withheld",        reason: "evaluation-unavailable",      });    }),  );}); describe("rollout events", () => {  it.effect("captures the connect outcome with the flag decision attached", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => jsonResponse({ status: 1 }));      const rollout = gate(harness);       yield* rollout.record({        kind: "connected",        context: CONTEXT,        decision: { kind: "enabled" },      });      yield* Effect.promise(() => harness.settle());       const [request] = harness.requests();      expect(request?.url).toBe(`${HOST}/i/v0/e/`);      expect(request?.body.event).toBe("ema_connect_connected");      expect(request?.body.distinct_id).toBe(CONTEXT.userId);      const properties = request?.body.properties as Record<string, unknown>;      expect(properties.ema_flag_enabled).toBe(true);      expect(properties[`$feature/${ENTERPRISE_MANAGED_AUTH_FLAG_KEY}`]).toBe(true);      expect(properties.$groups).toEqual({        organization: CONTEXT.organizationId,      });      expect(properties.integration_slug).toBe("slack");    }),  );   it.effect("carries the withheld reason on the attempt that never ran", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => jsonResponse({ status: 1 }));       yield* gate(harness).record({        kind: "attempted",        context: CONTEXT,        decision: { kind: "withheld", reason: "evaluation-unavailable" },      });      yield* Effect.promise(() => harness.settle());       const properties = harness.requests()[0]?.body.properties as Record<string, unknown>;      expect(harness.requests()[0]?.body.event).toBe("ema_connect_attempted");      expect(properties.ema_flag_enabled).toBe(false);      expect(properties.ema_flag_withheld_reason).toBe("evaluation-unavailable");    }),  );   it.effect("carries the identity provider's own error code on a blocked connect", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => jsonResponse({ status: 1 }));       yield* gate(harness).record({        kind: "blocked-by-admin",        context: CONTEXT,        decision: { kind: "enabled" },        oauthErrorCode: "unauthorized_client",      });      yield* Effect.promise(() => harness.settle());       const [request] = harness.requests();      expect(request?.body.event).toBe("ema_connect_blocked_by_admin");      const properties = request?.body.properties as Record<string, unknown> | undefined;      expect(properties?.oauth_error_code).toBe("unauthorized_client");    }),  );   it.effect("sends nothing beyond identity, the integration and the flag decision", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => jsonResponse({ status: 1 }));       yield* gate(harness).record({        kind: "blocked-by-admin",        context: CONTEXT,        decision: { kind: "enabled" },        oauthErrorCode: "access_denied",      });      yield* Effect.promise(() => harness.settle());       const properties = harness.requests()[0]?.body.properties as Record<string, unknown>;      expect(        Object.keys(properties).sort(),        "a closed property set is what keeps a token or an assertion from ever being added by accident",      ).toEqual(        [          "$feature/mcp-enterprise-managed-auth",          "$groups",          "ema_flag_enabled",          "integration_slug",          "oauth_error_code",        ].sort(),      );    }),  );   it.effect("hands the capture to the platform instead of awaiting it", () =>    Effect.gen(function* () {      let resolveCapture: (() => void) | undefined;      const harness = makeFetchHarness(        () =>          new Promise<Response>((resolve) => {            resolveCapture = () => resolve(jsonResponse({ status: 1 }));          }),      );       // Returns while the capture is still in flight: an analytics call can      // never be on the critical path of a connect.      yield* gate(harness).record({        kind: "attempted",        context: CONTEXT,        decision: { kind: "enabled" },      });       expect(harness.requests().length).toBe(1);      resolveCapture?.();      yield* Effect.promise(() => harness.settle());    }),  );   it.effect("survives a capture that fails outright", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => {        // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: models the ingest endpoint being unreachable        throw new TypeError("network error");      });       yield* gate(harness).record({        kind: "connected",        context: CONTEXT,        decision: { kind: "enabled" },      });      yield* Effect.promise(() => harness.settle());    }),  );   it.effect("records nothing when there is no person to attach the event to", () =>    Effect.gen(function* () {      const harness = makeFetchHarness(() => jsonResponse({ status: 1 }));       yield* gate(harness).record({        kind: "attempted",        context: { ...CONTEXT, userId: null },        decision: { kind: "withheld", reason: "evaluation-unavailable" },      });       expect(harness.requests().length).toBe(0);    }),  );});