1import { Query } from "@distilled.cloud/core/query";
2import { Railway as RailwayApi } from "@distilled.cloud/railway";
3import * as Railway from "@/Railway";
4import { suitePartition } from "./suiteProject.ts";
5import * as Test from "@/Test/Alchemy";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: Railway.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18const readTcpProxies = Query.fn((environmentId: string, serviceId: string) =>
19 RailwayApi.tcpProxies({ environmentId, serviceId }).pipe(
20 Query.map((proxy) => ({
21 id: proxy.id,
22 domain: proxy.domain,
23 proxyPort: proxy.proxyPort,
24 applicationPort: proxy.applicationPort,
25 deletedAt: proxy.deletedAt,
26 syncStatus: proxy.syncStatus,
27 })),
28 ),
29);
30
31const createService = Query.fn(
32 (input: { projectId: string; environmentId: string; image: string }) => ({
33 id: RailwayApi.serviceCreate({
34 input: {
35 projectId: input.projectId,
36 environmentId: input.environmentId,
37 source: { image: input.image },
38 },
39 }).id,
40 }),
41);
42
43const listLive = (environmentId: string, serviceId: string) =>
44 readTcpProxies(environmentId, serviceId).pipe(
45 Effect.map((items) =>
46 items
47 .filter(
48 (proxy) => proxy.deletedAt == null && proxy.syncStatus !== "DELETED",
49 )
50 .map((proxy) => ({
51 ...proxy,
52 domain: proxy.domain.replace(/\.+$/, ""),
53 })),
54 ),
55 Effect.catchTag("RailwayNotFound", () => Effect.succeed([])),
56 );
57
58const waitUntilProxyGone = (
59 environmentId: string,
60 serviceId: string,
61 id: string,
62) =>
63 listLive(environmentId, serviceId).pipe(
64 Effect.map((items) =>
65 items.some((proxy) => proxy.id === id)
66 ? ("found" as const)
67 : ("gone" as const),
68 ),
69 Effect.repeat({
70 schedule: Schedule.spaced("1 second"),
71 until: (status) => status === "gone",
72 times: 10,
73 }),
74 );
75
76const createTargetService = (projectId: string, environmentId: string) =>
77 createService({ projectId, environmentId, image: "redis:7-alpine" });
78
79test.provider(
80 "create, update, and delete a tcp proxy",
81 (stack) =>
82 Effect.gen(function* () {
83 yield* stack.destroy();
84
85 const { project, environment } = yield* stack.deploy(suitePartition);
86
87 const service = yield* createTargetService(
88 project.projectId,
89 environment.environmentId,
90 );
91
92 const created = yield* stack.deploy(
93 Effect.gen(function* () {
94 const { project: site, environment } = yield* suitePartition;
95 const proxy = yield* Railway.TcpProxy("DbProxy", {
96 service: { serviceId: service.id },
97 environment,
98 applicationPort: 6379,
99 });
100 return { project: site, environment, proxy };
101 }),
102 );
103
104 expect(created.proxy.id).toEqual(expect.any(String));
105 expect(created.proxy.id.length).toBeGreaterThan(0);
106 expect(created.proxy.domain).toEqual(expect.any(String));
107 expect(created.proxy.domain.length).toBeGreaterThan(0);
108 expect(created.proxy.proxyPort).toEqual(expect.any(Number));
109 expect(created.proxy.proxyPort).toBeGreaterThan(0);
110 expect(created.proxy.applicationPort).toEqual(6379);
111 expect(created.proxy.serviceId).toEqual(service.id);
112 expect(created.proxy.environmentId).toEqual(environment.environmentId);
113
114 const listed = yield* listLive(environment.environmentId, service.id);
115 const fetched = listed.find((proxy) => proxy.id === created.proxy.id);
116 expect(fetched).toBeDefined();
117 expect(fetched?.domain).toEqual(created.proxy.domain);
118 expect(fetched?.proxyPort).toEqual(created.proxy.proxyPort);
119 expect(fetched?.applicationPort).toEqual(6379);
120
121 const updated = yield* stack.deploy(
122 Effect.gen(function* () {
123 const { project: site, environment } = yield* suitePartition;
124 const proxy = yield* Railway.TcpProxy("DbProxy", {
125 service: { serviceId: service.id },
126 environment,
127 applicationPort: 6379,
128 });
129 return { project: site, environment, proxy };
130 }),
131 );
132
133 expect(updated.proxy.id).toEqual(created.proxy.id);
134 expect(updated.proxy.domain).toEqual(created.proxy.domain);
135 expect(updated.proxy.proxyPort).toEqual(created.proxy.proxyPort);
136 expect(updated.proxy.applicationPort).toEqual(6379);
137 expect(updated.project.projectId).toEqual(project.projectId);
138
139 yield* stack.destroy();
140
141 const proxyGone = yield* waitUntilProxyGone(
142 environment.environmentId,
143 service.id,
144 created.proxy.id,
145 );
146 expect(proxyGone).toEqual("gone");
147 }).pipe(logLevel),
148 {
149 tags: [
150 "provider:railway",
151 "provider:railway:project",
152 "provider:railway:projectenvironment",
153 "provider:railway:tcpproxy",
154 "live",
155 ],
156 timeout: 120_000,
157 },
158);
159
160test.provider(
161 "replace when applicationPort changes",
162 (stack) =>
163 Effect.gen(function* () {
164 yield* stack.destroy();
165
166 const { project, environment } = yield* stack.deploy(suitePartition);
167
168 const service = yield* createTargetService(
169 project.projectId,
170 environment.environmentId,
171 );
172
173 const created = yield* stack.deploy(
174 Effect.gen(function* () {
175 const { project: site, environment } = yield* suitePartition;
176 const proxy = yield* Railway.TcpProxy("DbProxy", {
177 postgres: { serviceId: service.id },
178 environment,
179 applicationPort: 6379,
180 });
181 return { project: site, environment, proxy };
182 }),
183 );
184
185 const replaced = yield* stack.deploy(
186 Effect.gen(function* () {
187 const { project: site, environment } = yield* suitePartition;
188 const proxy = yield* Railway.TcpProxy("DbProxy", {
189 postgres: { serviceId: service.id },
190 environment,
191 applicationPort: 5432,
192 });
193 return { project: site, environment, proxy };
194 }),
195 );
196
197 expect(replaced.proxy.applicationPort).toEqual(5432);
198 expect(replaced.proxy.id).not.toEqual(created.proxy.id);
199 expect(replaced.proxy.domain).toEqual(expect.any(String));
200 expect(replaced.proxy.proxyPort).toEqual(expect.any(Number));
201
202 const listed = yield* listLive(environment.environmentId, service.id);
203 const next = listed.find((proxy) => proxy.id === replaced.proxy.id);
204 expect(next).toBeDefined();
205 expect(next?.applicationPort).toEqual(5432);
206 expect(listed.some((proxy) => proxy.id === created.proxy.id)).toEqual(
207 false,
208 );
209
210 yield* stack.destroy();
211
212 const proxyGone = yield* waitUntilProxyGone(
213 environment.environmentId,
214 service.id,
215 replaced.proxy.id,
216 );
217 expect(proxyGone).toEqual("gone");
218 }).pipe(logLevel),
219 {
220 tags: [
221 "provider:railway",
222 "provider:railway:project",
223 "provider:railway:projectenvironment",
224 "provider:railway:tcpproxy",
225 "live",
226 ],
227 timeout: 120_000,
228 },
229);