1import { Query } from "@distilled.cloud/core/query";
2import { Railway as RailwaySdk } from "@distilled.cloud/railway";
3import * as Provider from "@/Provider";
4import * as Railway from "@/Railway";
5import { suitePartition } from "./suiteProject.ts";
6import * as Test from "@/Test/Alchemy";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import * as Redacted from "effect/Redacted";
10import { MinimumLogLevel } from "effect/References";
11import * as Schedule from "effect/Schedule";
12
13const { test } = Test.make({ providers: Railway.providers() });
14
15const logLevel = Effect.provideService(
16 MinimumLogLevel,
17 process.env.DEBUG ? "Debug" : "Info",
18);
19
20const VALUE_A = Redacted.make("alchemy-railway-var-a");
21const VALUE_B = Redacted.make("alchemy-railway-var-b");
22
23const asVariableMap = (value: unknown): Record<string, string> => {
24 if (value === null || typeof value !== "object" || Array.isArray(value)) {
25 return {};
26 }
27 const out: Record<string, string> = {};
28 for (const [key, item] of Object.entries(value)) {
29 if (typeof item === "string") {
30 out[key] = item;
31 }
32 }
33 return out;
34};
35
36const queryVariables = Query.fn(
37 (projectId: string, environmentId: string, serviceId?: string) =>
38 RailwaySdk.variables({
39 projectId,
40 environmentId,
41 ...(serviceId !== undefined ? { serviceId } : {}),
42 unrendered: true,
43 }),
44);
45
46const readVariables = (
47 projectId: string,
48 environmentId: string,
49 serviceId?: string,
50) =>
51 queryVariables(projectId, environmentId, serviceId).pipe(
52 Effect.map(asVariableMap),
53 Effect.catchTag("RailwayNotFound", () =>
54 Effect.succeed({} as Record<string, string>),
55 ),
56 );
57
58const waitUntilVariableGone = (
59 projectId: string,
60 environmentId: string,
61 name: string,
62) =>
63 readVariables(projectId, environmentId).pipe(
64 Effect.map((vars) =>
65 Object.hasOwn(vars, name) ? ("found" as const) : ("gone" as const),
66 ),
67 Effect.repeat({
68 schedule: Schedule.spaced("1 second"),
69 until: (status) => status === "gone",
70 times: 10,
71 }),
72 );
73
74const matchesPlain = (observed: string | undefined, expected: string) =>
75 observed === expected;
76
77test.provider(
78 "create, update, list, and delete a variable",
79 (stack) =>
80 Effect.gen(function* () {
81 yield* stack.destroy();
82
83 const created = yield* stack.deploy(
84 Effect.gen(function* () {
85 const { project, environment } = yield* suitePartition;
86 const variable = yield* Railway.Variable("DbUrl", {
87 project,
88 environment,
89 value: VALUE_A,
90 });
91 return { project, environment, variable };
92 }),
93 );
94
95 expect(created.variable.projectId).toEqual(created.project.projectId);
96 expect(created.variable.environmentId).toEqual(
97 created.environment.environmentId,
98 );
99 expect(created.variable.serviceId).toBeUndefined();
100 expect(created.variable.name).toEqual(expect.any(String));
101 expect(created.variable.name.length).toBeGreaterThan(0);
102 expect(created.variable.digest).toEqual(expect.any(String));
103 expect(created.variable.digest.length).toBeGreaterThan(0);
104 expect(Object.hasOwn(created.variable, "value")).toBe(false);
105
106 const fetched = yield* readVariables(
107 created.variable.projectId,
108 created.variable.environmentId,
109 );
110 expect(Object.hasOwn(fetched, created.variable.name)).toBe(true);
111 expect(
112 matchesPlain(fetched[created.variable.name], Redacted.value(VALUE_A)),
113 ).toBe(true);
114
115 const provider = yield* Provider.findProvider(Railway.Variable);
116 const listed = yield* provider.list();
117 const found = listed.find(
118 (variable) =>
119 variable.projectId === created.variable.projectId &&
120 variable.environmentId === created.variable.environmentId &&
121 variable.name === created.variable.name,
122 );
123 expect(found).toBeDefined();
124 expect(found?.digest).toEqual(created.variable.digest);
125 expect(found !== undefined && Object.hasOwn(found, "value")).toBe(false);
126
127 const updated = yield* stack.deploy(
128 Effect.gen(function* () {
129 const { project, environment } = yield* suitePartition;
130 const variable = yield* Railway.Variable("DbUrl", {
131 project,
132 environment,
133 value: VALUE_B,
134 });
135 return { project, environment, variable };
136 }),
137 );
138
139 expect(updated.variable.projectId).toEqual(created.variable.projectId);
140 expect(updated.variable.environmentId).toEqual(
141 created.variable.environmentId,
142 );
143 expect(updated.variable.name).toEqual(created.variable.name);
144 expect(updated.variable.digest).toEqual(expect.any(String));
145 expect(updated.variable.digest).not.toEqual(created.variable.digest);
146 expect(Object.hasOwn(updated.variable, "value")).toBe(false);
147
148 const refetched = yield* readVariables(
149 updated.variable.projectId,
150 updated.variable.environmentId,
151 );
152 expect(Object.hasOwn(refetched, updated.variable.name)).toBe(true);
153 expect(
154 matchesPlain(refetched[updated.variable.name], Redacted.value(VALUE_B)),
155 ).toBe(true);
156 expect(
157 matchesPlain(refetched[updated.variable.name], Redacted.value(VALUE_A)),
158 ).toBe(false);
159
160 yield* stack.destroy();
161
162 const variableGone = yield* waitUntilVariableGone(
163 created.variable.projectId,
164 created.variable.environmentId,
165 created.variable.name,
166 );
167 expect(variableGone).toEqual("gone");
168 }).pipe(logLevel),
169 {
170 tags: [
171 "provider:railway",
172 "provider:railway:project",
173 "provider:railway:projectenvironment",
174 "provider:railway:variable",
175 "live",
176 ],
177 timeout: 120_000,
178 },
179);