| 1 | import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment"; |
| 2 | import * as Cloudflare from "@/Cloudflare/index.ts"; |
| 3 | import * as Test from "@/Test/Alchemy"; |
| 4 | import * as workers from "@distilled.cloud/cloudflare/workers"; |
| 5 | import { describe, expect } from "alchemy-test"; |
| 6 | import * as Effect from "effect/Effect"; |
| 7 | import { MinimumLogLevel } from "effect/References"; |
| 8 | import { expectUrlContains } from "../Utils/Http.ts"; |
| 9 | import 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). |
| 13 | const CONFIG_STR_VALUE = (process.env.CONFIG_STR = "config-string-value"); |
| 14 | const CONFIG_NUM_VALUE = (process.env.CONFIG_NUM = "1234"); |
| 15 | const CONFIG_REDACTED_VALUE = (process.env.CONFIG_REDACTED = |
| 16 | "config-redacted-value"); |
| 17 | const 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. |
| 21 | const CONFIG_NESTED_HOST_VALUE = (process.env.CONFIG_NESTED_HOST = |
| 22 | "config-nested-host-value"); |
| 23 | |
| 24 | const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ |
| 25 | providers: Cloudflare.providers(), |
| 26 | }); |
| 27 | |
| 28 | const logLevel = Effect.provideService( |
| 29 | MinimumLogLevel, |
| 30 | process.env.DEBUG ? "Debug" : "Info", |
| 31 | ); |
| 32 | |
| 33 | const stack = beforeAll(deploy(Stack)); |
| 34 | afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack)); |
| 35 | |
| 36 | describe.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 | ); |