1import * as AWS from "@/AWS";
2import * as Provider from "@/Provider";
3import * as Test from "@/Test/Alchemy";
4import * as Lambda from "@distilled.cloud/aws/lambda";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8import { fileURLToPath } from "node:url";
9
10const timeoutHandlerPath = fileURLToPath(
11 new URL("./timeout-handler.ts", import.meta.url),
12);
13
14const { test } = Test.make({ providers: AWS.providers() });
15
16test.provider(
17 "create, update, list, replace, delete alias",
18 (stack) =>
19 Effect.gen(function* () {
20 yield* stack.destroy();
21
22 const program = ({
23 envVersion,
24 alias,
25 }: {
26 envVersion: string;
27 alias?: {
28 aliasName: string;
29 description?: string;
30 routingConfig?: Lambda.AliasRoutingConfiguration;
31 };
32 }) =>
33 Effect.gen(function* () {
34 const fn = yield* AWS.Lambda.Function("AliasFn", {
35 main: timeoutHandlerPath,
36 handler: "handler",
37 isExternal: true,
38 functionUrl: false,
39 env: {
40 VERSION: envVersion,
41 },
42 });
43
44 const version = yield* AWS.Lambda.Version("AliasVersion", {
45 function: fn,
46 });
47
48 const live = alias
49 ? yield* AWS.Lambda.Alias("LiveAlias", {
50 version,
51 aliasName: alias.aliasName,
52 description: alias.description,
53 routingConfig: alias.routingConfig,
54 })
55 : undefined;
56
57 return { fn, version, live };
58 });
59
60 const initial = yield* stack.deploy(program({ envVersion: "1" }));
61 const version1 = initial.version.version;
62
63 // --- create ---
64 const created = yield* stack.deploy(
65 program({
66 envVersion: "1",
67 alias: {
68 aliasName: "live",
69 description: "live v1",
70 },
71 }),
72 );
73 const createdAlias = created.live!;
74
75 expect(createdAlias.aliasName).toBe("live");
76 expect(createdAlias.functionVersion).toBe(version1);
77 expect(createdAlias.invokeArn).toContain(createdAlias.aliasArn);
78
79 // Assert the alias actually exists in the cloud.
80 const liveV1 = yield* getAliasOrUndefined(
81 created.fn.functionName,
82 "live",
83 );
84 expect(liveV1).toBeDefined();
85 expect(liveV1!.AliasArn).toBe(createdAlias.aliasArn);
86 expect(liveV1!.FunctionVersion).toBe(version1);
87 expect(liveV1!.Description).toBe("live v1");
88 expect(liveV1!.RoutingConfig?.AdditionalVersionWeights ?? {}).toEqual({});
89
90 // --- update (function version + weighted routing + description) ---
91 const updated = yield* stack.deploy(
92 program({
93 envVersion: "2",
94 alias: {
95 aliasName: "live",
96 description: "weighted live",
97 routingConfig: {
98 AdditionalVersionWeights: {
99 [version1]: 0.25,
100 },
101 },
102 },
103 }),
104 );
105 const version2 = updated.version.version;
106 const updatedAlias = updated.live!;
107
108 // Updating in place must keep the same ARN (not a replacement).
109 expect(updatedAlias.aliasArn).toBe(createdAlias.aliasArn);
110 expect(updatedAlias.functionVersion).toBe(version2);
111 expect(updatedAlias.description).toBe("weighted live");
112 expect(updatedAlias.routingConfig).toEqual({
113 AdditionalVersionWeights: {
114 [version1]: 0.25,
115 },
116 });
117
118 // Assert the cloud reflects the update.
119 const liveV2 = yield* getAliasOrUndefined(
120 updated.fn.functionName,
121 "live",
122 );
123 expect(liveV2).toBeDefined();
124 expect(liveV2!.AliasArn).toBe(createdAlias.aliasArn);
125 expect(liveV2!.FunctionVersion).toBe(version2);
126 expect(liveV2!.Description).toBe("weighted live");
127 expect(liveV2!.RoutingConfig?.AdditionalVersionWeights).toEqual({
128 [version1]: 0.25,
129 });
130
131 // --- update (clear description + routing) ---
132 const cleared = yield* stack.deploy(
133 program({
134 envVersion: "2",
135 alias: {
136 aliasName: "live",
137 },
138 }),
139 );
140 const clearedAlias = cleared.live!;
141
142 expect(clearedAlias.aliasArn).toBe(createdAlias.aliasArn);
143 expect(clearedAlias.functionVersion).toBe(version2);
144 expect(clearedAlias.description).toBeUndefined();
145 expect(clearedAlias.routingConfig).toBeUndefined();
146
147 // Assert the cloud cleared description and routing.
148 const liveCleared = yield* getAliasOrUndefined(
149 cleared.fn.functionName,
150 "live",
151 );
152 expect(liveCleared).toBeDefined();
153 expect(liveCleared!.Description ?? "").toBe("");
154 expect(
155 liveCleared!.RoutingConfig?.AdditionalVersionWeights ?? {},
156 ).toEqual({});
157
158 // --- replace (rename the alias) ---
159 // The provider's `diff` flags an aliasName change as a replacement, so a
160 // brand new alias is created and the old one deleted.
161 const replaced = yield* stack.deploy(
162 program({
163 envVersion: "2",
164 alias: {
165 aliasName: "stable",
166 description: "renamed",
167 },
168 }),
169 );
170 const replacedAlias = replaced.live!;
171
172 expect(replacedAlias.aliasName).toBe("stable");
173 // A replacement must mint a new ARN.
174 expect(replacedAlias.aliasArn).not.toBe(createdAlias.aliasArn);
175 expect(replacedAlias.functionVersion).toBe(version2);
176 expect(replacedAlias.description).toBe("renamed");
177
178 // The new alias exists in the cloud...
179 const stableAlias = yield* getAliasOrUndefined(
180 replaced.fn.functionName,
181 "stable",
182 );
183 expect(stableAlias).toBeDefined();
184 expect(stableAlias!.AliasArn).toBe(replacedAlias.aliasArn);
185 expect(stableAlias!.FunctionVersion).toBe(version2);
186 expect(stableAlias!.Description).toBe("renamed");
187
188 // ...and the old one was deleted as part of the replacement.
189 const oldLive = yield* getAliasOrUndefined(
190 replaced.fn.functionName,
191 "live",
192 );
193 expect(oldLive).toBeUndefined();
194
195 // --- list ---
196 const provider = yield* Provider.findProvider(AWS.Lambda.Alias);
197 const aliases = yield* provider.list();
198 expect(
199 aliases.some(
200 (alias) =>
201 alias.functionName === replaced.fn.functionName &&
202 alias.aliasName === "stable",
203 ),
204 ).toBe(true);
205
206 // --- delete ---
207 yield* stack.destroy();
208
209 const afterDestroy = yield* getAliasOrUndefined(
210 replaced.fn.functionName,
211 "stable",
212 );
213 expect(afterDestroy).toBeUndefined();
214
215 // ...and the host function itself is gone (bounded retry to ride out
216 // delete propagation).
217 yield* Lambda.getFunction({
218 FunctionName: replaced.fn.functionName,
219 }).pipe(
220 Effect.flatMap(() =>
221 Effect.fail(
222 new Error(`Function ${replaced.fn.functionName} still exists`),
223 ),
224 ),
225 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
226 Effect.retry({
227 schedule: Schedule.max([
228 Schedule.exponential(500),
229 Schedule.recurs(8),
230 ]),
231 }),
232 );
233 }).pipe(
234 Effect.tap(() => stack.destroy()),
235 Effect.onError(() => stack.destroy().pipe(Effect.ignore)),
236 ),
237 { tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 360_000 },
238);
239
240const getAliasOrUndefined = Effect.fn(function* (
241 functionName: string,
242 name: string,
243) {
244 return yield* Lambda.getAlias({
245 FunctionName: functionName,
246 Name: name,
247 }).pipe(
248 Effect.catchTag("ResourceNotFoundException", () =>
249 Effect.succeed(undefined),
250 ),
251 );
252});