Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions apps/cloud/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
HttpApiClient,
HttpApiEndpoint,
HttpApiGroup,
HttpApiSwagger,
OpenApi,
} from "effect/unstable/httpapi";
import {
FetchHttpClient,
Expand Down Expand Up @@ -47,6 +49,7 @@ const ProtectedGroup = HttpApiGroup.make("protected")
}),
);
const ProtectedTestApi = HttpApi.make("protectedApi").add(ProtectedGroup);
const DocumentationTestApi = ProtectedTestApi.add(AuthGroup).add(OrgGroup);

// ---------------------------------------------------------------------------
// Stub handlers
Expand Down Expand Up @@ -127,9 +130,19 @@ const AutumnTestRoutesLive = HttpRouter.add(
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));
Expand Down Expand Up @@ -190,6 +203,35 @@ layer(TestClientLayer)("handleApiRequest", (it) => {
}),
);

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<string, unknown> };
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();
Expand Down
23 changes: 23 additions & 0 deletions apps/cloud/src/api/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Effect, Layer } from "effect";
import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
import { HttpApiSwagger, OpenApi } from "effect/unstable/httpapi";

import { CloudAuthApi, CloudAuthPublicApi } from "../auth/api";
import { OrgApi } from "../org/api";

import { ProtectedCloudApi } from "./protected-layers";

export const CloudOpenApi = ProtectedCloudApi.add(CloudAuthPublicApi).add(CloudAuthApi).add(OrgApi);

const spec = OpenApi.fromApi(CloudOpenApi);

export const CloudOpenApiJsonLive = HttpRouter.add(
"GET",
"/openapi.json",
Effect.succeed(HttpServerResponse.jsonUnsafe(spec)),
);

export const CloudDocsLive = Layer.mergeAll(
HttpApiSwagger.layer(CloudOpenApi, { path: "/docs" }),
CloudOpenApiJsonLive,
);
4 changes: 1 addition & 3 deletions apps/cloud/src/api/protected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// because `makeExecutionStack` imports `cloudflare:workers`, which the test
// harness can't load in the workerd test runtime.

import { HttpApiSwagger } from "effect/unstable/httpapi";
import { HttpRouter, HttpServerRequest } from "effect/unstable/http";
import { Effect, Layer } from "effect";

Expand All @@ -24,7 +23,7 @@ import { DbService } from "../services/db";
import { makeExecutionStack } from "../services/execution-stack";
import { HttpResponseError } from "./error-response";
import { RequestScopedServicesLive } from "./layers";
import { ProtectedCloudApi, ProtectedCloudApiLive, RouterConfig } from "./protected-layers";
import { ProtectedCloudApiLive, RouterConfig } from "./protected-layers";
import { requestScopedMiddleware } from "./request-scoped";

// Pre-compute the per-plugin `Effect.provideService(extensionService,
Expand Down Expand Up @@ -207,7 +206,6 @@ export const makeProtectedApiLive = (rsLive: Layer.Layer<DbService | UserStoreSe
return ProtectedCloudApiLive.pipe(
Layer.provide(protectedMiddleware),
Layer.provideMerge(ApiKeyService.WorkOS),
Layer.provideMerge(HttpApiSwagger.layer(ProtectedCloudApi, { path: "/docs" })),
Layer.provideMerge(RouterConfig),
);
};
Expand Down
2 changes: 2 additions & 0 deletions apps/cloud/src/api/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { UserStoreService } from "../auth/context";
import { DbService } from "../services/db";

import { AutumnRoutesLive } from "./autumn";
import { CloudDocsLive } from "./docs";
import { ApiErrorLoggingLive } from "./error-logging";
import {
BootSharedServices,
Expand All @@ -29,6 +30,7 @@ export const makeApiLive = (requestScopedLive: Layer.Layer<DbService | UserStore
Layer.mergeAll(
makeNonProtectedApiLive(requestScopedLive),
makeOrgApiLive(requestScopedLive),
CloudDocsLive,
makeProtectedApiLive(requestScopedLive),
AutumnRoutesLive,
ApiErrorLoggingLive,
Expand Down
Loading