import { HttpApi, HttpApiBuilder, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiSwagger, OpenApi, } from "effect/unstable/httpapi"; import { FetchHttpClient, HttpClient, HttpRouter, HttpServer, HttpServerResponse, } from "effect/unstable/http"; import { expect, layer } from "@effect/vitest"; import { Cause, Effect, Layer, Schema } from "effect"; import { toErrorServerResponse } from "./api/error-response"; const SourceResponse = Schema.Struct({ source: Schema.String }); // --------------------------------------------------------------------------- // Test APIs — mirror the prod paths but with stub handlers. // --------------------------------------------------------------------------- const OrgGroup = HttpApiGroup.make("org").add( HttpApiEndpoint.get("ping", "/org/ping", { success: SourceResponse }), ); const OrgTestApi = HttpApi.make("orgApi").add(OrgGroup); const AuthGroup = HttpApiGroup.make("auth").add( HttpApiEndpoint.get("me", "/auth/me", { success: SourceResponse }), ); const AuthTestApi = HttpApi.make("authApi").add(AuthGroup); const ProtectedGroup = HttpApiGroup.make("protected") .add(HttpApiEndpoint.get("scope", "/scope", { success: SourceResponse })) .add( HttpApiEndpoint.get("sources", "/scopes/:scopeId/sources", { params: { scopeId: Schema.String }, success: SourceResponse, }), ) .add( HttpApiEndpoint.post("resume", "/executions/:executionId/resume", { params: { executionId: Schema.String }, success: SourceResponse, }), ); const ProtectedTestApi = HttpApi.make("protectedApi").add(ProtectedGroup); const DocumentationTestApi = ProtectedTestApi.add(AuthGroup).add(OrgGroup); // --------------------------------------------------------------------------- // Stub handlers // --------------------------------------------------------------------------- const OrgTestHandlers = HttpApiBuilder.group(OrgTestApi, "org", (handlers) => handlers.handle("ping", () => Effect.succeed({ source: "org" })), ); const AuthHandlers = HttpApiBuilder.group(AuthTestApi, "auth", (handlers) => handlers.handle("me", () => Effect.succeed({ source: "auth" })), ); const ProtectedHandlers = HttpApiBuilder.group(ProtectedTestApi, "protected", (handlers) => handlers .handle("scope", () => Effect.succeed({ source: "protected" })) .handle("sources", ({ params }) => Effect.succeed({ source: params.scopeId })) .handle("resume", () => Effect.succeed({ source: "protected" })), ); // --------------------------------------------------------------------------- // Per-test mode switch — controls a router-level gate that mirrors the prod // `ExecutionStackMiddleware`'s short-circuit branches without standing up // the full WorkOS / executor stack. // --------------------------------------------------------------------------- type ProtectedMode = "ok" | "none" | "error" | "bad-status" | "defect"; const testState: { mode: ProtectedMode } = { mode: "ok" }; const resetState = () => { testState.mode = "ok"; }; // `Effect.suspend` so `testState.mode` is read per request — the // middleware function itself runs once at addAll time, wrapping each // route's handler. Without `suspend`, only the build-time mode ("ok") // would be observed. const TestProtectedGate = HttpRouter.middleware()((httpEffect) => Effect.suspend(() => { if (testState.mode === "none") { return Effect.succeed( HttpServerResponse.jsonUnsafe( { error: "No organization in session", code: "no_organization" }, { status: 403 }, ), ); } if (testState.mode === "error") { return Effect.succeed(HttpServerResponse.jsonUnsafe({ error: "boom" }, { status: 500 })); } if (testState.mode === "bad-status") { return Effect.succeed( HttpServerResponse.jsonUnsafe({ source: "protected" }, { status: 400 }), ); } if (testState.mode === "defect") { return Effect.succeed(toErrorServerResponse(Cause.die("secret-bearing failure detail"))); } return httpEffect; }), ).layer; // --------------------------------------------------------------------------- // Wire test APIs as route layers + autumn route, mirroring prod's structure. // --------------------------------------------------------------------------- const RouterConfig = Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 }); const OrgTestLive = HttpApiBuilder.layer(OrgTestApi).pipe(Layer.provide(OrgTestHandlers)); const AuthTestLive = HttpApiBuilder.layer(AuthTestApi).pipe(Layer.provide(AuthHandlers)); const ProtectedTestLive = HttpApiBuilder.layer(ProtectedTestApi).pipe( Layer.provide(ProtectedHandlers), Layer.provide(TestProtectedGate), ); const AutumnTestRoutesLive = HttpRouter.add( "*", "/autumn/*", Effect.succeed(HttpServerResponse.jsonUnsafe({ source: "autumn" })), ); const TestDocsLive = Layer.mergeAll( HttpApiSwagger.layer(DocumentationTestApi, { path: "/docs" }), HttpRouter.add( "GET", "/openapi.json", Effect.succeed(HttpServerResponse.jsonUnsafe(OpenApi.fromApi(DocumentationTestApi))), ), ); const TestApiLive = Layer.mergeAll( OrgTestLive, AuthTestLive, TestDocsLive, ProtectedTestLive, AutumnTestRoutesLive, ).pipe(Layer.provideMerge(RouterConfig), Layer.provideMerge(HttpServer.layerServices)); const requestHandler = HttpRouter.toWebHandler(TestApiLive, { disableLogger: true }).handler; // --------------------------------------------------------------------------- // Client setup — route HttpClient calls through the web handler in-process // so the suite runs in any runtime (workerd's `NodeHttpServer.layerTest` // crashes the isolate). // --------------------------------------------------------------------------- const TestApi = HttpApi.make("testApi") .add(OrgGroup) .add(AuthGroup) .add( HttpApiGroup.make("autumn").add( HttpApiEndpoint.get("plans", "/autumn/plans", { success: SourceResponse }), ), ) .add(ProtectedGroup); const TEST_BASE_URL = "http://test.local"; const fetchViaHandler: typeof globalThis.fetch = (input, init) => requestHandler(input instanceof Request ? input : new Request(input, init)); const TestClientLayer = FetchHttpClient.layer.pipe( Layer.provide(Layer.succeed(FetchHttpClient.Fetch)(fetchViaHandler)), ); const getClient = () => HttpApiClient.make(TestApi, { baseUrl: TEST_BASE_URL }); layer(TestClientLayer)("handleApiRequest", (it) => { it.effect("routes /org/* to the org API handler", () => Effect.gen(function* () { resetState(); const client = yield* getClient(); const result = yield* client.org.ping(); expect(result).toEqual({ source: "org" }); }), ); it.effect("routes /auth/* to the auth API handler", () => Effect.gen(function* () { resetState(); const client = yield* getClient(); const result = yield* client.auth.me(); expect(result).toEqual({ source: "auth" }); }), ); it.effect("routes /autumn/* to the autumn handler", () => Effect.gen(function* () { resetState(); const client = yield* getClient(); const result = yield* client.autumn.plans(); expect(result).toEqual({ source: "autumn" }); }), ); it.effect("serves docs without the protected gate", () => Effect.gen(function* () { resetState(); testState.mode = "none"; const response = yield* HttpClient.get(`${TEST_BASE_URL}/docs`); expect(response.status).toBe(200); const body = yield* response.text; expect(body).toContain("swagger-ui"); expect(body).toContain("/auth/me"); expect(body).toContain("/org/ping"); expect(body).toContain("/scope"); }), ); it.effect("serves raw OpenAPI JSON without the protected gate", () => Effect.gen(function* () { resetState(); testState.mode = "none"; const response = yield* HttpClient.get(`${TEST_BASE_URL}/openapi.json`); expect(response.status).toBe(200); const body = (yield* response.json) as { readonly paths?: Record }; expect(body.paths).toHaveProperty("/auth/me"); expect(body.paths).toHaveProperty("/org/ping"); expect(body.paths).toHaveProperty("/scope"); }), ); it.effect("routes non-auth paths to protected handler", () => Effect.gen(function* () { resetState(); const client = yield* getClient(); const result = yield* client.protected.scope(); expect(result).toEqual({ source: "protected" }); }), ); it.effect("returns 403 when protected gate short-circuits with no organization", () => Effect.gen(function* () { resetState(); testState.mode = "none"; const response = yield* HttpClient.get(`${TEST_BASE_URL}/scope`); expect(response.status).toBe(403); const body = yield* response.json; expect(body).toEqual({ error: "No organization in session", code: "no_organization", }); }), ); it.effect("routes resume paths to protected handler", () => Effect.gen(function* () { resetState(); const client = yield* getClient(); const result = yield* client.protected.resume({ params: { executionId: "exec_1" } }); expect(result).toEqual({ source: "protected" }); }), ); it.effect("preserves protected path params", () => Effect.gen(function* () { resetState(); const client = yield* getClient(); const result = yield* client.protected.sources({ params: { scopeId: "org_1" } }); expect(result).toEqual({ source: "org_1" }); }), ); it.effect("returns protected response status as-is", () => Effect.gen(function* () { resetState(); testState.mode = "bad-status"; const response = yield* HttpClient.post(`${TEST_BASE_URL}/executions/exec_1/resume`); expect(response.status).toBe(400); }), ); it.effect("returns 500 JSON when protected gate returns 500", () => Effect.gen(function* () { resetState(); testState.mode = "error"; const response = yield* HttpClient.get(`${TEST_BASE_URL}/scope`); expect(response.status).toBe(500); const body = yield* response.json; expect(body).toEqual({ error: "boom" }); }), ); it.effect("maps protected defects to the generic API error body", () => Effect.gen(function* () { resetState(); testState.mode = "defect"; const response = yield* HttpClient.get(`${TEST_BASE_URL}/scope`); expect(response.status).toBe(500); const body = yield* response.json; expect(body).toEqual({ error: "Internal server error", code: "internal_server_error", }); }), ); });