1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { EnvironmentHttpApi } from "@t3tools/contracts";
3import { describe, expect, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Etag from "effect/http/Etag";
7import * as HttpPlatform from "effect/http/HttpPlatform";
8import * as HttpRouter from "effect/http/HttpRouter";
9import * as HttpApi from "effect/http-api/HttpApi";
10import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
11
12import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
13import * as AuthHttp from "../auth/http.ts";
14import * as CloudLink from "./CloudLink.ts";
15import * as ConnectHttp from "./http.ts";
16
17class ConnectTestApi extends HttpApi.make("environment").add(EnvironmentHttpApi.groups.connect) {}
18
19// The signed relay routes need no session, so a CloudLink that fails each
20// request shows how the transport answers that failure.
21type HealthFailure = Effect.Error<
22 ReturnType<CloudLink.CloudLink["Service"]["answerHealthRequest"]>
23>;
24
25const answerHealthWith = async (failure: HealthFailure) => {
26 const layerRoutes = HttpApiBuilder.layer(ConnectTestApi).pipe(
27 Layer.provide(ConnectHttp.layer),
28 Layer.provide(
29 Layer.mock(CloudLink.CloudLink)({
30 answerHealthRequest: () => Effect.fail(failure),
31 }),
32 ),
33 // The session-gated routes are declared too; this request never reaches them.
34 Layer.provide(AuthHttp.layerAuthenticatedAuth),
35 Layer.provide(Layer.mock(EnvironmentAuth.EnvironmentAuth)({})),
36 Layer.provideMerge(
37 HttpPlatform.layer.pipe(
38 Layer.provideMerge(NodeServices.layer),
39 Layer.provideMerge(Etag.layerWeak),
40 ),
41 ),
42 );
43 const { handler, dispose } = HttpRouter.toWebHandler(layerRoutes, {
44 disableLogger: true,
45 });
46 try {
47 const response = await handler(
48 new Request("http://127.0.0.1/api/t3-connect/health", {
49 method: "POST",
50 headers: { "content-type": "application/json" },
51 body: JSON.stringify({ proof: "proof" }),
52 }),
53 );
54 return { status: response.status, body: (await response.json()) as unknown };
55 } finally {
56 await dispose();
57 }
58};
59
60describe("connect routes", () => {
61 it.each([
62 {
63 failure: new CloudLink.CloudLinkProofRejectedError({ request: "health" }),
64 status: 401,
65 body: { _tag: "EnvironmentHttpUnauthorizedError", message: "Invalid cloud health request." },
66 },
67 {
68 failure: new CloudLink.CloudLinkProofReplayedError({ request: "health" }),
69 status: 409,
70 body: {
71 _tag: "EnvironmentHttpConflictError",
72 message: "Cloud health request was already consumed.",
73 },
74 },
75 {
76 failure: new CloudLink.CloudLinkInternalError({
77 operation: "answer-health",
78 cause: new Error("disk full"),
79 }),
80 status: 500,
81 body: {
82 _tag: "EnvironmentHttpInternalServerError",
83 message: "Could not answer cloud health request.",
84 },
85 },
86 {
87 failure: new EnvironmentAuth.ServerAuthCloudMintPublicKeyMissingError({}),
88 status: 500,
89 body: {
90 _tag: "EnvironmentHttpInternalServerError",
91 message: "Cloud mint public key is not installed for this environment.",
92 },
93 },
94 ])("answers $failure._tag with HTTP $status", async ({ failure, status, body }) => {
95 const response = await answerHealthWith(failure);
96 expect(response).toEqual({ status, body });
97 });
98});