1import * as Railway from "@/Railway";
2import { RPC_PATH_PREFIX, RPC_TOKEN_HEADER } from "@/Railway/rpc-token.ts";
3import * as Test from "@/Test/Alchemy";
4import { expect } from "alchemy-test";
5import * as Data from "effect/Data";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9import * as HttpClient from "effect/http/HttpClient";
10import * as HttpClientRequest from "effect/http/HttpClientRequest";
11import { Api, ApiLive } from "./fixtures/rpc-api.ts";
12import Caller from "./fixtures/rpc-caller.ts";
13import Greeter from "./fixtures/rpc-greeter.ts";
14import Query from "./fixtures/rpc-query.ts";
15
16const { test } = Test.make({ providers: Railway.providers() });
17
18const logLevel = Effect.provideService(
19 MinimumLogLevel,
20 process.env.DEBUG ? "Debug" : "Info",
21);
22
23class NotReady extends Data.TaggedError("NotReady")<{
24 status: number;
25 body?: string;
26}> {
27 override get message() {
28 return this.body === undefined
29 ? `status ${this.status}`
30 : `status ${this.status}: ${this.body}`;
31 }
32}
33
34const getText = (url: string) =>
35 Effect.gen(function* () {
36 const client = yield* HttpClient.HttpClient;
37 return yield* client.get(url).pipe(
38 Effect.timeout("15 seconds"),
39 Effect.flatMap((res) =>
40 res.status === 200
41 ? res.text
42 : res.text.pipe(
43 Effect.flatMap((body) =>
44 Effect.fail(new NotReady({ status: res.status, body })),
45 ),
46 ),
47 ),
48 Effect.retry({
49 schedule: Schedule.spaced("4 seconds"),
50 times: 10,
51 }),
52 );
53 });
54
55test.provider.skip(
56 "Function-to-Function schemaless RPC stays on the private mesh (canvas start command max 96KB)",
57 (stack) =>
58 Effect.gen(function* () {
59 yield* stack.destroy();
60
61 const created = yield* stack.deploy(
62 Effect.gen(function* () {
63 const greeter = yield* Greeter;
64 const caller = yield* Caller;
65 return { greeter, caller };
66 }),
67 );
68
69 expect(created.greeter.dnsName).toEqual(
70 `${created.greeter.name}.railway.internal`,
71 );
72 expect(created.greeter.dnsName.endsWith(".railway.internal")).toEqual(
73 true,
74 );
75 expect(created.greeter.rpcToken.length).toBeGreaterThanOrEqual(32);
76 expect(created.greeter.url).toEqual(expect.any(String));
77 expect(created.caller.url).toEqual(expect.any(String));
78
79 const greeterPublic = yield* getText(created.greeter.url!);
80 expect(greeterPublic).toEqual("greeter");
81
82 const client = yield* HttpClient.HttpClient;
83 const publicRpc = yield* client.execute(
84 HttpClientRequest.post(
85 `${created.greeter.url}${RPC_PATH_PREFIX}greet`,
86 ).pipe(
87 HttpClientRequest.bodyText(
88 JSON.stringify(["sam"]),
89 "application/json",
90 ),
91 HttpClientRequest.setHeader(
92 RPC_TOKEN_HEADER,
93 created.greeter.rpcToken,
94 ),
95 ),
96 );
97 expect(publicRpc.status).toEqual(401);
98
99 const greeting = yield* getText(created.caller.url!);
100 expect(greeting).toEqual("hello sam");
101
102 yield* stack.destroy();
103 }).pipe(logLevel),
104 {
105 tags: [
106 "provider:railway",
107 "provider:railway:bind",
108 "provider:railway:function",
109 "live",
110 ],
111 timeout: 120_000,
112 },
113);
114
115test.provider.skip(
116 "Service and Function bind each other in tagged form over the private mesh (canvas start command max 96KB)",
117 (stack) =>
118 Effect.gen(function* () {
119 yield* stack.destroy();
120
121 const created = yield* stack.deploy(
122 Effect.gen(function* () {
123 const query = yield* Query;
124 const api = yield* Api;
125 return { query, api };
126 }).pipe(Effect.provide(ApiLive)),
127 );
128
129 expect(created.query.url).toEqual(expect.any(String));
130 expect(created.api.url).toEqual(expect.any(String));
131
132 yield* Effect.log(
133 `tagged rpc urls query=${created.query.url} api=${created.api.url}`,
134 );
135
136 // Probe the Service origin first (Docker). The Function canvas
137 // used to hang GET `/` while this side was already serving.
138 const fromService = yield* getText(created.api.url!);
139 expect(fromService).toEqual("api");
140
141 const fromFunction = yield* getText(created.query.url!);
142 expect(fromFunction).toEqual("query");
143
144 const viaFunction = yield* getText(`${created.query.url}/pong`);
145 expect(viaFunction).toEqual("pong");
146
147 const viaService = yield* getText(`${created.api.url}/hello`);
148 expect(viaService).toEqual("hello sam");
149
150 const client = yield* HttpClient.HttpClient;
151 const functionRpc = yield* client.execute(
152 HttpClientRequest.post(
153 `${created.query.url}${RPC_PATH_PREFIX}greet`,
154 ).pipe(
155 HttpClientRequest.bodyText(
156 JSON.stringify(["sam"]),
157 "application/json",
158 ),
159 HttpClientRequest.setHeader(RPC_TOKEN_HEADER, created.query.rpcToken),
160 ),
161 );
162 expect(functionRpc.status).toEqual(401);
163
164 const serviceRpc = yield* client.execute(
165 HttpClientRequest.post(`${created.api.url}${RPC_PATH_PREFIX}ping`).pipe(
166 HttpClientRequest.bodyText("[]", "application/json"),
167 HttpClientRequest.setHeader(RPC_TOKEN_HEADER, created.api.rpcToken),
168 ),
169 );
170 expect(serviceRpc.status).toEqual(401);
171
172 yield* stack.destroy();
173 }).pipe(logLevel),
174 {
175 tags: [
176 "provider:railway",
177 "provider:railway:bind",
178 "provider:railway:function",
179 "provider:railway:service",
180 "live",
181 ],
182 timeout: 120_000,
183 },
184);