Skip to content

Commit b12ecd9

Browse files
committed
2 parents ea110d9 + 30733bc commit b12ecd9

27 files changed

Lines changed: 1203 additions & 646 deletions

‎.github/workflows/ci.yml‎

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,6 @@ concurrency:
1111
cancel-in-progress: true
1212

1313
jobs:
14-
# format:
15-
# name: Format
16-
# runs-on: ubuntu-latest
17-
# steps:
18-
# - uses: actions/checkout@v4
19-
#
20-
# - uses: oven-sh/setup-bun@v2
21-
# with:
22-
# bun-version: 1.3.11
23-
#
24-
# - run: bun install --frozen-lockfile
25-
#
26-
# - run: bun run format:check
27-
2814
lint:
2915
name: Lint
3016
runs-on: ubuntu-latest

‎.github/workflows/format.yml‎

Lines changed: 0 additions & 28 deletions
This file was deleted.

‎.oxfmtrc.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"$schema": "./node_modules/oxfmt/configuration_schema.json",
3-
"ignorePatterns": []
3+
"ignorePatterns": ["**/routeTree.gen.ts"]
44
}

‎.oxlintrc.jsonc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@
1616
},
1717
],
1818
},
19-
"ignorePatterns": [".astro/"],
19+
"ignorePatterns": [".astro/", "**/routeTree.gen.ts"],
2020
}

‎apps/cloud/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"devDependencies": {
6262
"@cloudflare/vitest-pool-workers": "^0.14.3",
6363
"@cloudflare/workers-types": "^4.20250620.0",
64+
"@effect/platform-node": "catalog:",
6465
"@effect/vitest": "^0.29.0",
6566
"@electric-sql/pglite": "^0.4.3",
6667
"@electric-sql/pglite-socket": "^0.1.3",

‎apps/cloud/src/api.test.ts‎

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import {
2+
HttpApi,
3+
HttpApiBuilder,
4+
HttpApiClient,
5+
HttpApiEndpoint,
6+
HttpApiGroup,
7+
HttpApp,
8+
HttpClient,
9+
HttpServer,
10+
HttpServerResponse,
11+
} from "@effect/platform";
12+
import { NodeHttpServer } from "@effect/platform-node";
13+
import { expect, layer } from "@effect/vitest";
14+
import { Effect, Layer, Schema } from "effect";
15+
16+
import {
17+
ApiRequestHandler,
18+
AutumnRequestHandlerService,
19+
NonProtectedRequestHandlerService,
20+
ProtectedRequestHandlerService,
21+
TeamRequestHandlerService,
22+
} from "./api/router";
23+
24+
const SourceResponse = Schema.Struct({ source: Schema.String });
25+
26+
const TeamGroup = HttpApiGroup.make("team").add(
27+
HttpApiEndpoint.get("ping", "/team/ping").addSuccess(SourceResponse),
28+
);
29+
const TeamApi = HttpApi.make("teamApi").add(TeamGroup);
30+
const TeamHandlers = HttpApiBuilder.group(TeamApi, "team", (handlers) =>
31+
handlers.handle("ping", () => Effect.succeed({ source: "team" })),
32+
);
33+
34+
const AuthGroup = HttpApiGroup.make("auth").add(
35+
HttpApiEndpoint.get("me", "/auth/me").addSuccess(SourceResponse),
36+
);
37+
const AuthApi = HttpApi.make("authApi").add(AuthGroup);
38+
const AuthHandlers = HttpApiBuilder.group(AuthApi, "auth", (handlers) =>
39+
handlers.handle("me", () => Effect.succeed({ source: "auth" })),
40+
);
41+
42+
const ProtectedGroup = HttpApiGroup.make("protected")
43+
.add(HttpApiEndpoint.get("scope", "/scope").addSuccess(SourceResponse))
44+
.add(
45+
HttpApiEndpoint.post("resume", "/executions/:executionId/resume")
46+
.setPath(Schema.Struct({ executionId: Schema.String }))
47+
.addSuccess(SourceResponse),
48+
);
49+
const ProtectedApi = HttpApi.make("protectedApi").add(ProtectedGroup);
50+
const ProtectedHandlers = HttpApiBuilder.group(ProtectedApi, "protected", (handlers) =>
51+
handlers
52+
.handle("scope", () => Effect.succeed({ source: "protected" }))
53+
.handle("resume", () => Effect.succeed({ source: "protected" })),
54+
);
55+
56+
const TeamTestApp = Effect.flatMap(
57+
HttpApiBuilder.httpApp.pipe(
58+
Effect.provide(
59+
HttpApiBuilder.api(TeamApi).pipe(
60+
Layer.provide(TeamHandlers),
61+
Layer.provideMerge(HttpServer.layerContext),
62+
Layer.provideMerge(HttpApiBuilder.Router.Live),
63+
Layer.provideMerge(HttpApiBuilder.Middleware.layer),
64+
),
65+
),
66+
),
67+
(app) => app,
68+
).pipe(Effect.provide(HttpServer.layerContext));
69+
70+
const AuthTestApp = Effect.flatMap(
71+
HttpApiBuilder.httpApp.pipe(
72+
Effect.provide(
73+
HttpApiBuilder.api(AuthApi).pipe(
74+
Layer.provide(AuthHandlers),
75+
Layer.provideMerge(HttpServer.layerContext),
76+
Layer.provideMerge(HttpApiBuilder.Router.Live),
77+
Layer.provideMerge(HttpApiBuilder.Middleware.layer),
78+
),
79+
),
80+
),
81+
(app) => app,
82+
).pipe(Effect.provide(HttpServer.layerContext));
83+
84+
const ProtectedBaseTestApp = Effect.flatMap(
85+
HttpApiBuilder.httpApp.pipe(
86+
Effect.provide(
87+
HttpApiBuilder.api(ProtectedApi).pipe(
88+
Layer.provide(ProtectedHandlers),
89+
Layer.provideMerge(HttpServer.layerContext),
90+
Layer.provideMerge(HttpApiBuilder.Router.Live),
91+
Layer.provideMerge(HttpApiBuilder.Middleware.layer),
92+
),
93+
),
94+
),
95+
(app) => app,
96+
).pipe(Effect.provide(HttpServer.layerContext));
97+
98+
type ProtectedMode = "ok" | "none" | "error" | "bad-status";
99+
100+
const testState: {
101+
mode: ProtectedMode;
102+
} = {
103+
mode: "ok",
104+
};
105+
106+
const resetState = () => {
107+
testState.mode = "ok";
108+
};
109+
110+
const ProtectedTestApp = Effect.gen(function* () {
111+
if (testState.mode === "none") {
112+
return HttpServerResponse.unsafeJson(
113+
{ error: "No organization in session", code: "no_organization" },
114+
{ status: 403 },
115+
);
116+
}
117+
if (testState.mode === "error") {
118+
return HttpServerResponse.unsafeJson({ error: "boom" }, { status: 500 });
119+
}
120+
if (testState.mode === "bad-status") {
121+
return HttpServerResponse.unsafeJson({ source: "protected" }, { status: 400 });
122+
}
123+
return yield* ProtectedBaseTestApp;
124+
});
125+
126+
const TestRequestHandlersLive = Layer.mergeAll(
127+
Layer.succeed(TeamRequestHandlerService, { app: TeamTestApp }),
128+
Layer.succeed(NonProtectedRequestHandlerService, { app: AuthTestApp }),
129+
Layer.succeed(AutumnRequestHandlerService, {
130+
app: Effect.succeed(HttpServerResponse.unsafeJson({ source: "autumn" })),
131+
}),
132+
Layer.succeed(ProtectedRequestHandlerService, { app: ProtectedTestApp }),
133+
);
134+
135+
const requestHandler = Effect.runSync(Effect.provide(ApiRequestHandler, TestRequestHandlersLive));
136+
137+
const TestApi = HttpApi.make("testApi")
138+
.add(TeamGroup)
139+
.add(AuthGroup)
140+
.add(
141+
HttpApiGroup.make("autumn").add(
142+
HttpApiEndpoint.get("plans", "/autumn/plans").addSuccess(SourceResponse),
143+
),
144+
)
145+
.add(ProtectedGroup);
146+
147+
const TestServerLayer = HttpServer.serve(HttpApp.fromWebHandler(requestHandler)).pipe(
148+
Layer.provideMerge(NodeHttpServer.layerTest),
149+
);
150+
151+
const getClient = () => HttpApiClient.make(TestApi);
152+
153+
layer(TestServerLayer)("handleApiRequest", (it) => {
154+
it.effect("routes /team/* to the team API handler", () =>
155+
Effect.gen(function* () {
156+
resetState();
157+
const client = yield* getClient();
158+
const result = yield* client.team.ping();
159+
expect(result).toEqual({ source: "team" });
160+
}),
161+
);
162+
163+
it.effect("routes /auth/* to the auth API handler", () =>
164+
Effect.gen(function* () {
165+
resetState();
166+
const client = yield* getClient();
167+
const result = yield* client.auth.me();
168+
expect(result).toEqual({ source: "auth" });
169+
}),
170+
);
171+
172+
it.effect("routes /autumn/* to the autumn handler", () =>
173+
Effect.gen(function* () {
174+
resetState();
175+
const client = yield* getClient();
176+
const result = yield* client.autumn.plans();
177+
expect(result).toEqual({ source: "autumn" });
178+
}),
179+
);
180+
181+
it.effect("routes non-auth paths to protected handler", () =>
182+
Effect.gen(function* () {
183+
resetState();
184+
const client = yield* getClient();
185+
const result = yield* client.protected.scope();
186+
expect(result).toEqual({ source: "protected" });
187+
}),
188+
);
189+
190+
it.effect("returns 403 when protected handler returns no organization", () =>
191+
Effect.gen(function* () {
192+
resetState();
193+
testState.mode = "none";
194+
195+
const response = yield* HttpClient.get("/scope");
196+
expect(response.status).toBe(403);
197+
const body = yield* response.json;
198+
expect(body).toEqual({
199+
error: "No organization in session",
200+
code: "no_organization",
201+
});
202+
}),
203+
);
204+
205+
it.effect("routes resume paths to protected handler", () =>
206+
Effect.gen(function* () {
207+
resetState();
208+
const client = yield* getClient();
209+
const result = yield* client.protected.resume({ path: { executionId: "exec_1" } });
210+
expect(result).toEqual({ source: "protected" });
211+
}),
212+
);
213+
214+
it.effect("returns protected response status as-is", () =>
215+
Effect.gen(function* () {
216+
resetState();
217+
testState.mode = "bad-status";
218+
219+
const response = yield* HttpClient.post("/executions/exec_1/resume");
220+
expect(response.status).toBe(400);
221+
}),
222+
);
223+
224+
it.effect("returns 500 JSON when protected request handling throws", () =>
225+
Effect.gen(function* () {
226+
resetState();
227+
testState.mode = "error";
228+
229+
const response = yield* HttpClient.get("/scope");
230+
expect(response.status).toBe(500);
231+
const body = yield* response.json;
232+
expect(body).toEqual({ error: "boom" });
233+
}),
234+
);
235+
});

0 commit comments

Comments
 (0)