1import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
2import * as Cloudflare from "@/Cloudflare/index.ts";
3import * as Test from "@/Test/Alchemy";
4import * as workers from "@distilled.cloud/cloudflare/workers";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import { expectUrlContains } from "../Utils/Http.ts";
9import Stack from "./fixtures/env/stack.ts";
10
11// Populate process.env before deploy so the worker fixture's
12// `Config.xxx(...)` reads resolve at deploy time (default provider).
13const CONFIG_STR_VALUE = (process.env.CONFIG_STR = "config-string-value");
14const CONFIG_NUM_VALUE = (process.env.CONFIG_NUM = "1234");
15const CONFIG_REDACTED_VALUE = (process.env.CONFIG_REDACTED =
16 "config-redacted-value");
17const CONFIG_REDACTED_INIT_VALUE = (process.env.CONFIG_REDACTED_INIT =
18 "config-redacted-init-value");
19// Read via `Config.String("HOST").pipe(Config.nested("CONFIG_NESTED"))` —
20// binds under the flattened `CONFIG_NESTED_HOST` key.
21const CONFIG_NESTED_HOST_VALUE = (process.env.CONFIG_NESTED_HOST =
22 "config-nested-host-value");
23
24const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
25 providers: Cloudflare.providers(),
26});
27
28const logLevel = Effect.provideService(
29 MinimumLogLevel,
30 process.env.DEBUG ? "Debug" : "Info",
31);
32
33const stack = beforeAll(deploy(Stack));
34afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
35
36describe.concurrent(
37 "Cloudflare.Worker env bindings",
38 { tags: ["provider:cloudflare", "provider:cloudflare:worker", "live"] },
39 () => {
40 test(
41 "async worker round-trips every supported binding shape",
42 Effect.gen(function* () {
43 const { asyncUrl } = yield* stack;
44 expect(asyncUrl).toBeTypeOf("string");
45 yield* Effect.log(asyncUrl);
46
47 const body = yield* expectUrlContains(asyncUrl, '"STR":"hello"', {
48 timeout: "60 seconds",
49 label: "async env-worker response",
50 });
51 expect(JSON.parse(body)).toEqual({
52 STR: "hello",
53 NUM: 42,
54 BOOL: true,
55 NULL: null,
56 OBJ: { nested: { value: "ok" }, count: 7 },
57 ARR: [1, 2, 3],
58 OUTPUT_STR: "output-str",
59 RANDOM_IS_HEX: true,
60 SECRET_STR: "shh",
61 SECRET_JSON: { token: "abc", scopes: ["read", "write"] },
62 CONFIG_STR: CONFIG_STR_VALUE,
63 CONFIG_NUM: Number(CONFIG_NUM_VALUE),
64 CONFIG_REDACTED: CONFIG_REDACTED_VALUE,
65 VERSION_METADATA: {
66 id: expect.any(String),
67 tag: expect.any(String),
68 timestamp: expect.any(String),
69 },
70 });
71 }).pipe(logLevel),
72 );
73
74 test.provider(
75 "Output env values classify by resolved value on the wire",
76 () =>
77 Effect.gen(function* () {
78 const { asyncWorkerName } = yield* stack;
79 const { accountId } = yield* yield* CloudflareEnvironment;
80
81 const settings = yield* workers.getScriptScriptAndVersionSetting({
82 accountId,
83 scriptName: asyncWorkerName,
84 });
85 // Output<Redacted<string>> (Alchemy.Random) must deploy encrypted,
86 // never as a plaintext json binding.
87 expect(settings.bindings).toContainEqual(
88 expect.objectContaining({ type: "secret_text", name: "RANDOM" }),
89 );
90 // Output<string> resolves to a string, so it lands as plain_text.
91 expect(settings.bindings).toContainEqual(
92 expect.objectContaining({ type: "plain_text", name: "OUTPUT_STR" }),
93 );
94 }).pipe(logLevel),
95 );
96
97 test(
98 "effect worker round-trips env: literals and Redacted via WorkerEnvironment",
99 Effect.gen(function* () {
100 const { effectUrl } = yield* stack;
101
102 const body = yield* expectUrlContains(
103 `${effectUrl}/env`,
104 '"STR":"hello"',
105 { timeout: "60 seconds", label: "effect env-worker /env" },
106 );
107 expect(JSON.parse(body)).toEqual({
108 STR: "hello",
109 NUM: 42,
110 BOOL: true,
111 NULL: null,
112 OBJ: { nested: { value: "ok" }, count: 7 },
113 ARR: [1, 2, 3],
114 OUTPUT_STR: "output-str",
115 SECRET_STR: "shh",
116 SECRET_JSON: { token: "abc", scopes: ["read", "write"] },
117 });
118 }).pipe(logLevel),
119 );
120
121 test(
122 "effect worker resolves the yielded VersionMetadata binding",
123 Effect.gen(function* () {
124 const { effectUrl } = yield* stack;
125
126 const body = yield* expectUrlContains(`${effectUrl}/version`, '"id"', {
127 timeout: "60 seconds",
128 label: "effect env-worker /version",
129 });
130 expect(JSON.parse(body)).toEqual({
131 id: expect.any(String),
132 tag: expect.any(String),
133 timestamp: expect.any(String),
134 });
135 }).pipe(logLevel),
136 );
137
138 test(
139 "effect worker re-resolves Config.xxx at request time inside a nested effect",
140 Effect.gen(function* () {
141 const { effectUrl } = yield* stack;
142
143 const body = yield* expectUrlContains(
144 `${effectUrl}/config-runtime`,
145 '"CONFIG_STR"',
146 { timeout: "60 seconds", label: "effect env-worker /config-runtime" },
147 );
148 expect(JSON.parse(body)).toEqual({
149 CONFIG_STR: CONFIG_STR_VALUE,
150 CONFIG_NUM: Number(CONFIG_NUM_VALUE),
151 CONFIG_NUM_WITH_DEFAULT: Number(CONFIG_NUM_VALUE),
152 CONFIG_UNSET_WITH_DEFAULT: 3000,
153 CONFIG_REDACTED_INIT: CONFIG_REDACTED_INIT_VALUE,
154 CONFIG_REDACTED_INIT_IS_REDACTED: true,
155 CONFIG_ALL_OBJ: {
156 str: CONFIG_STR_VALUE,
157 num: Number(CONFIG_NUM_VALUE),
158 redacted: CONFIG_REDACTED_INIT_VALUE,
159 redactedIsRedacted: true,
160 },
161 CONFIG_ALL_TUPLE: [CONFIG_STR_VALUE, Number(CONFIG_NUM_VALUE)],
162 CONFIG_NESTED_HOST: CONFIG_NESTED_HOST_VALUE,
163 });
164 }).pipe(logLevel),
165 );
166
167 test(
168 "effect worker round-trips Config.xxx bindings captured in Init",
169 Effect.gen(function* () {
170 const { effectUrl } = yield* stack;
171
172 const body = yield* expectUrlContains(
173 `${effectUrl}/config`,
174 '"CONFIG_STR"',
175 { timeout: "60 seconds", label: "effect env-worker /config" },
176 );
177 expect(JSON.parse(body)).toEqual({
178 CONFIG_STR: CONFIG_STR_VALUE,
179 CONFIG_NUM: Number(CONFIG_NUM_VALUE),
180 CONFIG_REDACTED: CONFIG_REDACTED_VALUE,
181 CONFIG_REDACTED_INIT: CONFIG_REDACTED_INIT_VALUE,
182 CONFIG_REDACTED_INIT_IS_REDACTED: true,
183 CONFIG_ALL_OBJ: {
184 str: CONFIG_STR_VALUE,
185 num: Number(CONFIG_NUM_VALUE),
186 redacted: CONFIG_REDACTED_INIT_VALUE,
187 redactedIsRedacted: true,
188 },
189 CONFIG_ALL_TUPLE: [CONFIG_STR_VALUE, Number(CONFIG_NUM_VALUE)],
190 CONFIG_NESTED_HOST: CONFIG_NESTED_HOST_VALUE,
191 });
192 }).pipe(logLevel),
193 );
194 },
195);