1import * as AWS from "@/AWS";
2import * as Alchemy from "@/index.ts";
3import * as State from "@/State";
4import * as Test from "@/Test/Alchemy";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9import * as HttpClient from "effect/http/HttpClient";
10import SecretsTestFunctionLive, {
11 CONFIG_SECRET_ENV_KEY,
12 LITERAL_SECRET_VALUE,
13 NUMBER_VAR_VALUE,
14 OBJECT_VAR_VALUE,
15 STRING_VAR_VALUE,
16 SecretsTestFunction,
17} from "./fixtures/handler.ts";
18
19/**
20 * `Config.Redacted("CONFIG_SECRET")` resolves against the active
21 * `ConfigProvider` at deploy time. The default provider reads from
22 * `process.env`, so populate it before `beforeAll(deploy(Stack))`
23 * compiles the stack.
24 */
25const CONFIG_SECRET_VALUE = "sk-from-aws-config-source-xyz";
26process.env[CONFIG_SECRET_ENV_KEY] = CONFIG_SECRET_VALUE;
27
28const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
29 providers: AWS.providers(),
30});
31
32const logLevel = Effect.provideService(
33 MinimumLogLevel,
34 process.env.DEBUG ? "Debug" : "Info",
35);
36
37const Stack = Alchemy.Stack(
38 "AlchemySecretLambdaStack",
39 {
40 providers: AWS.providers(),
41 state: State.localState(),
42 },
43 Effect.gen(function* () {
44 const fn = yield* SecretsTestFunction;
45 return {
46 url: fn.functionUrl.as<string>(),
47 };
48 }).pipe(Effect.provide(SecretsTestFunctionLive)),
49);
50
51// The fixture is one Lambda + Function URL, but fresh IAM/Lambda propagation
52// can exceed 90s under the full c128 sweep. Keep the hook budget below the
53// factory hard wall while every readiness attempt remains independently
54// bounded below.
55const stack = beforeAll(deploy(Stack), { timeout: 210_000 });
56afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack), { timeout: 180_000 });
57
58// Lambda Function URLs cold-start (DNS, IAM propagation, init) can take
59// well over a minute on a fresh deploy under parallel load. Budget a
60// generous retry window for the very first request, then reuse the
61// warm URL for subsequent calls.
62const readinessSchedule = Schedule.max([
63 Schedule.fixed("2 seconds"),
64 Schedule.recurs(20),
65]);
66
67const getJson = (url: string) =>
68 HttpClient.get(url).pipe(
69 Effect.timeout("4 seconds"),
70 Effect.flatMap((res) =>
71 res.status === 200
72 ? Effect.flatMap(res.json, (body) => Effect.succeed(body))
73 : Effect.fail(new Error(`Request failed: ${res.status}`)),
74 ),
75 Effect.retry({ schedule: readinessSchedule }),
76 );
77
78test(
79 "Config.Redacted with literal default round-trips to Lambda runtime as Redacted<string>",
80 Effect.gen(function* () {
81 const { url } = yield* stack;
82 expect(url).toBeTypeOf("string");
83
84 const baseUrl = url.replace(/\/+$/, "");
85 const body = (yield* getJson(`${baseUrl}/secret/literal`)) as {
86 isRedacted: boolean;
87 value: string;
88 };
89 expect(body).toEqual({
90 isRedacted: true,
91 value: LITERAL_SECRET_VALUE,
92 });
93 }).pipe(logLevel),
94 { tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 45_000 },
95);
96
97test(
98 "Config.Redacted resolved from env round-trips through Lambda env",
99 Effect.gen(function* () {
100 const { url } = yield* stack;
101 const baseUrl = url.replace(/\/+$/, "");
102
103 const body = (yield* getJson(`${baseUrl}/secret/config`)) as {
104 isRedacted: boolean;
105 value: string;
106 };
107 expect(body).toEqual({
108 isRedacted: true,
109 value: CONFIG_SECRET_VALUE,
110 });
111 }).pipe(logLevel),
112 { tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 45_000 },
113);
114
115test(
116 "Config.String round-trips to Lambda runtime as a string",
117 Effect.gen(function* () {
118 const { url } = yield* stack;
119 const baseUrl = url.replace(/\/+$/, "");
120
121 const body = (yield* getJson(`${baseUrl}/var/string`)) as {
122 type: string;
123 value: unknown;
124 };
125 expect(body).toEqual({ type: "string", value: STRING_VAR_VALUE });
126 }).pipe(logLevel),
127 { tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 45_000 },
128);
129
130test(
131 "Config.Number round-trips to Lambda runtime preserving the number type",
132 Effect.gen(function* () {
133 const { url } = yield* stack;
134 const baseUrl = url.replace(/\/+$/, "");
135
136 const body = (yield* getJson(`${baseUrl}/var/number`)) as {
137 type: string;
138 value: unknown;
139 };
140 expect(body).toEqual({ type: "number", value: NUMBER_VAR_VALUE });
141 }).pipe(logLevel),
142 { tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 45_000 },
143);
144
145test(
146 "Config.String with object default round-trips to Lambda runtime preserving nested shape",
147 Effect.gen(function* () {
148 const { url } = yield* stack;
149 const baseUrl = url.replace(/\/+$/, "");
150
151 const body = (yield* getJson(`${baseUrl}/var/object`)) as {
152 type: string;
153 value: unknown;
154 };
155 expect(body).toEqual({ type: "object", value: OBJECT_VAR_VALUE });
156 }).pipe(logLevel),
157 { tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 45_000 },
158);