1import * as machines from "@distilled.cloud/fly-io/machines";
2import * as Fly from "@/Fly";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Redacted from "effect/Redacted";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: Fly.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18const VALUE_A = Redacted.make("alchemy-secret-a");
19const VALUE_B = Redacted.make("alchemy-secret-b");
20
21const waitUntilGone = (appName: string, secretName: string) =>
22 machines
23 .getSecret({
24 app_name: appName,
25 secret_name: secretName,
26 show_secrets: false,
27 })
28 .pipe(
29 Effect.as("found" as const),
30 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
31 Effect.repeat({
32 schedule: Schedule.spaced("1 second"),
33 until: (status) => status === "gone",
34 times: 10,
35 }),
36 );
37
38const waitAppGone = (appName: string) =>
39 machines.getApp({ app_name: appName }).pipe(
40 Effect.as("found" as const),
41 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
42 Effect.repeat({
43 schedule: Schedule.spaced("1 second"),
44 until: (status) => status === "gone",
45 times: 10,
46 }),
47 );
48
49test.provider(
50 "create, update, and delete a secret",
51 (stack) =>
52 Effect.gen(function* () {
53 yield* stack.destroy();
54
55 const created = yield* stack.deploy(
56 Effect.gen(function* () {
57 const app = yield* Fly.App("Site");
58 const secret = yield* Fly.Secret("DbUrl", {
59 app,
60 value: VALUE_A,
61 });
62 return { app, secret };
63 }),
64 );
65
66 expect(created.secret.appName).toEqual(created.app.appName);
67 expect(created.secret.name).toEqual(expect.any(String));
68 expect(created.secret.name.length).toBeGreaterThan(0);
69 expect(created.secret.digest).toEqual(expect.any(String));
70
71 const fetched = yield* machines.getSecret({
72 app_name: created.secret.appName,
73 secret_name: created.secret.name,
74 show_secrets: false,
75 });
76 expect(fetched.name).toEqual(created.secret.name);
77 expect(fetched.digest).toEqual(created.secret.digest);
78 expect(fetched.value).toBeUndefined();
79
80 const updated = yield* stack.deploy(
81 Effect.gen(function* () {
82 const app = yield* Fly.App("Site");
83 const secret = yield* Fly.Secret("DbUrl", {
84 app,
85 value: VALUE_B,
86 });
87 return { app, secret };
88 }),
89 );
90
91 expect(updated.secret.appName).toEqual(created.secret.appName);
92 expect(updated.secret.name).toEqual(created.secret.name);
93 expect(updated.secret.digest).toEqual(expect.any(String));
94 expect(updated.secret.digest).not.toEqual(created.secret.digest);
95
96 const refetched = yield* machines.getSecret({
97 app_name: updated.secret.appName,
98 secret_name: updated.secret.name,
99 show_secrets: false,
100 });
101 expect(refetched.name).toEqual(updated.secret.name);
102 expect(refetched.digest).toEqual(updated.secret.digest);
103 expect(refetched.digest).not.toEqual(created.secret.digest);
104 expect(refetched.value).toBeUndefined();
105
106 yield* stack.destroy();
107
108 const secretGone = yield* waitUntilGone(
109 created.secret.appName,
110 created.secret.name,
111 );
112 expect(secretGone).toEqual("gone");
113 const appGone = yield* waitAppGone(created.app.appName);
114 expect(appGone).toEqual("gone");
115 }).pipe(logLevel),
116 {
117 tags: [
118 "provider:fly",
119 "provider:fly:app",
120 "provider:fly:machine",
121 "provider:fly:secret",
122 "live",
123 ],
124 timeout: 120_000,
125 },
126);
127
128test.provider(
129 "replace when name changes",
130 (stack) =>
131 Effect.gen(function* () {
132 yield* stack.destroy();
133
134 const created = yield* stack.deploy(
135 Effect.gen(function* () {
136 const app = yield* Fly.App("Site");
137 const secret = yield* Fly.Secret("ReplaceSecret", {
138 app,
139 value: VALUE_A,
140 });
141 return { app, secret };
142 }),
143 );
144
145 const nextName =
146 created.secret.name.slice(0, -1) +
147 (created.secret.name.endsWith("z") ? "y" : "z");
148
149 const replaced = yield* stack.deploy(
150 Effect.gen(function* () {
151 const app = yield* Fly.App("Site");
152 const secret = yield* Fly.Secret("ReplaceSecret", {
153 app,
154 name: nextName,
155 value: VALUE_A,
156 });
157 return { app, secret };
158 }),
159 );
160
161 expect(replaced.secret.name).toEqual(nextName);
162 expect(replaced.secret.name).not.toEqual(created.secret.name);
163 expect(replaced.secret.appName).toEqual(created.secret.appName);
164
165 const fetched = yield* machines.getSecret({
166 app_name: replaced.secret.appName,
167 secret_name: replaced.secret.name,
168 show_secrets: false,
169 });
170 expect(fetched.name).toEqual(replaced.secret.name);
171 expect(fetched.value).toBeUndefined();
172
173 const oldGone = yield* waitUntilGone(
174 created.secret.appName,
175 created.secret.name,
176 );
177 expect(oldGone).toEqual("gone");
178
179 yield* stack.destroy();
180
181 const gone = yield* waitUntilGone(
182 replaced.secret.appName,
183 replaced.secret.name,
184 );
185 expect(gone).toEqual("gone");
186 }).pipe(logLevel),
187 {
188 tags: [
189 "provider:fly",
190 "provider:fly:app",
191 "provider:fly:machine",
192 "provider:fly:secret",
193 "live",
194 ],
195 timeout: 120_000,
196 },
197);
198
199test.provider(
200 "list enumerates the deployed secret",
201 (stack) =>
202 Effect.gen(function* () {
203 yield* stack.destroy();
204
205 const deployed = yield* stack.deploy(
206 Effect.gen(function* () {
207 const app = yield* Fly.App("Site");
208 const secret = yield* Fly.Secret("ListSecret", {
209 app,
210 value: VALUE_A,
211 });
212 return { app, secret };
213 }),
214 );
215
216 const provider = yield* Provider.findProvider(Fly.Secret);
217 const all = yield* provider.list();
218 const found = all.find(
219 (secret) =>
220 secret.appName === deployed.secret.appName &&
221 secret.name === deployed.secret.name,
222 );
223 expect(found).toBeDefined();
224 expect(found?.digest).toEqual(deployed.secret.digest);
225
226 yield* stack.destroy();
227
228 const gone = yield* waitUntilGone(
229 deployed.secret.appName,
230 deployed.secret.name,
231 );
232 expect(gone).toEqual("gone");
233 }).pipe(logLevel),
234 {
235 tags: [
236 "provider:fly",
237 "provider:fly:app",
238 "provider:fly:machine",
239 "provider:fly:secret",
240 "live",
241 ],
242 timeout: 120_000,
243 },
244);