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 { suiteProject } from "./suiteProject.ts";
6import * as Test from "@/Test/Alchemy";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { MinimumLogLevel } from "effect/References";
10import * as Schedule from "effect/Schedule";
11
12const { test } = Test.make({ providers: Railway.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19const readEnvironment = Query.fn((id: string, projectId: string) => {
20 const env = RailwaySdk.environment({ id, projectId });
21 return {
22 id: env.id,
23 name: env.name,
24 projectId: env.projectId,
25 deletedAt: env.deletedAt,
26 };
27});
28
29const readEnvironmentDeletedAt = Query.fn((id: string) => ({
30 deletedAt: RailwaySdk.environment({ id }).deletedAt,
31}));
32
33const waitUntilEnvGone = (environmentId: string) =>
34 readEnvironmentDeletedAt(environmentId).pipe(
35 Effect.map((env) =>
36 env.deletedAt != null ? ("gone" as const) : ("found" as const),
37 ),
38 Effect.catchTag("RailwayNotFound", () => Effect.succeed("gone" as const)),
39 Effect.repeat({
40 schedule: Schedule.spaced("1 second"),
41 until: (status) => status === "gone",
42 times: 10,
43 }),
44 );
45
46test.provider(
47 "create, update, list, and delete an extra environment",
48 (stack) =>
49 Effect.gen(function* () {
50 yield* stack.destroy();
51
52 const created = yield* stack.deploy(
53 Effect.gen(function* () {
54 const project = yield* suiteProject;
55 const environment = yield* Railway.Environment("Staging", {
56 project,
57 });
58 return { project, environment };
59 }),
60 );
61
62 expect(created.project.projectId).toEqual(expect.any(String));
63 expect(created.project.environmentId).toEqual(expect.any(String));
64 expect(created.environment.environmentId).toEqual(expect.any(String));
65 expect(created.environment.environmentId.length).toBeGreaterThan(0);
66 expect(created.environment.environmentId).not.toEqual(
67 created.project.environmentId,
68 );
69 expect(created.environment.projectId).toEqual(created.project.projectId);
70 expect(created.environment.name).toEqual(expect.any(String));
71 expect(created.environment.name.length).toBeGreaterThan(0);
72 expect(created.environment.name.length).toBeLessThanOrEqual(32);
73 expect(created.environment.name).toMatch(/^[a-z][a-z0-9-]*$/);
74 expect(created.environment.isEphemeral).toEqual(false);
75 expect(created.environment.url).toEqual(
76 `https://railway.com/project/${created.project.projectId}?environmentId=${created.environment.environmentId}`,
77 );
78
79 const fetched = yield* readEnvironment(
80 created.environment.environmentId,
81 created.project.projectId,
82 );
83 expect(fetched.id).toEqual(created.environment.environmentId);
84 expect(fetched.name).toEqual(created.environment.name);
85 expect(fetched.projectId).toEqual(created.project.projectId);
86 expect(fetched.deletedAt).toBeNull();
87
88 const production = yield* readEnvironment(
89 created.project.environmentId,
90 created.project.projectId,
91 );
92 expect(production.id).toEqual(created.project.environmentId);
93 expect(production.deletedAt).toBeNull();
94
95 const provider = yield* Provider.findProvider(Railway.Environment);
96 const listed = yield* provider.list();
97 const found = listed.find(
98 (env) => env.environmentId === created.environment.environmentId,
99 );
100 expect(found).toBeDefined();
101 expect(found?.name).toEqual(created.environment.name);
102 expect(found?.projectId).toEqual(created.project.projectId);
103 expect(
104 listed.find(
105 (env) => env.environmentId === created.project.environmentId,
106 ),
107 ).toBeUndefined();
108
109 const nextName =
110 created.environment.name.slice(0, -1) +
111 (created.environment.name.endsWith("z") ? "y" : "z");
112
113 const updated = yield* stack.deploy(
114 Effect.gen(function* () {
115 const project = yield* suiteProject;
116 const environment = yield* Railway.Environment("Staging", {
117 project,
118 name: nextName,
119 });
120 return { project, environment };
121 }),
122 );
123
124 expect(updated.project.projectId).toEqual(created.project.projectId);
125 expect(updated.environment.environmentId).toEqual(
126 created.environment.environmentId,
127 );
128 expect(updated.environment.name).toEqual(nextName);
129 expect(updated.environment.projectId).toEqual(created.project.projectId);
130 expect(updated.environment.environmentId).not.toEqual(
131 updated.project.environmentId,
132 );
133
134 const fetchedUpdate = yield* readEnvironment(
135 updated.environment.environmentId,
136 updated.project.projectId,
137 );
138 expect(fetchedUpdate.id).toEqual(updated.environment.environmentId);
139 expect(fetchedUpdate.name).toEqual(nextName);
140
141 yield* stack.destroy();
142
143 const envGone = yield* waitUntilEnvGone(
144 created.environment.environmentId,
145 );
146 expect(envGone).toEqual("gone");
147 }).pipe(logLevel),
148 {
149 tags: [
150 "provider:railway",
151 "provider:railway:project",
152 "provider:railway:projectenvironment",
153 "live",
154 ],
155 timeout: 120_000,
156 },
157);