Skip to content
Source
Sign inSign up

clientConfig.test.ts

147 lines6.0 KB

1// @effect-diagnostics anyUnknownInErrorContext:off - ScratchStack.deploy is typed with `any` in its error channel by Alchemy.
2import { inMemoryState } from "alchemy/State";
3import * as Test from "@alchemy.run/test/Vitest";
4import * as ConfigProvider from "effect/ConfigProvider";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";
8import * as Path from "effect/Path";
9import * as Redacted from "effect/Redacted";
10import { describe, expect, it } from "vite-plus/test";
11import * as NodeServices from "@effect/platform-node/NodeServices";
12​
13import { PublishClientConfig, reconcileEnvFile, tokenDigest } from "./clientConfig.ts";
14​
15describe("reconcileEnvFile", () => {
16 it("replaces stale values and keeps unrelated lines", () => {
17 const existing = "KEEP=yes\nA=old\n# comment\n";
18 expect(reconcileEnvFile(existing, { A: "new", B: "2" })).toBe(
19 "KEEP=yes\nA=new\n# comment\nB=2\n",
20 );
21 });
22​
23 it("appends a newline before adding to a file without one", () => {
24 expect(reconcileEnvFile("A=1", { B: "2" })).toBe("A=1\nB=2\n");
25 });
26​
27 it("collapses duplicate assignments to one, since parseEnv lets the last win", () => {
28 expect(reconcileEnvFile("A=old\nKEEP=1\nA=older\n", { A: "new" })).toBe("A=new\nKEEP=1\n");
29 });
30​
31 it("recognises every assignment form parseEnv accepts", () => {
32 expect(reconcileEnvFile("A=old\n export A = stale\nexport A=older\n", { A: "new" })).toBe(
33 "A=new\n",
34 );
35 });
36​
37 it("replaces a quoted multi-line value whole", () => {
38 expect(reconcileEnvFile('A="one\ntwo"\nKEEP=1\n', { A: "new" })).toBe("A=new\nKEEP=1\n");
39 expect(reconcileEnvFile("A='x'\nKEEP=1\n", { A: "new" })).toBe("A=new\nKEEP=1\n");
40 });
41​
42 it("keeps later lines when a quoted value never closes", () => {
43 expect(reconcileEnvFile('A="one\nKEEP=1\n', { A: "new" })).toBe("A=new\nKEEP=1\n");
44 });
45​
46 it("leaves a commented-out assignment alone", () => {
47 expect(reconcileEnvFile("#A=old\n", { A: "new" })).toBe("#A=old\nA=new\n");
48 });
49});
50​
51const { test } = Test.make({
52 providers: Layer.empty,
53 state: inMemoryState(),
54});
55​
56const clientConfig = (token: string) => ({
57 url: "https://relay.example.com",
58 mobileTracingUrl: "https://axiom.example.com/v1/traces",
59 mobileTracingDataset: "relay-traces",
60 mobileTracingToken: Redacted.make(`mobile-${token}`),
61 clientTracingUrl: "https://axiom.example.com/v1/traces",
62 clientTracingDataset: "relay-traces",
63 clientTracingToken: Redacted.make(`client-${token}`),
64 tokenDigest: tokenDigest([Redacted.make(`mobile-${token}`), Redacted.make(`client-${token}`)]),
65});
66​
67describe("PublishClientConfig", () => {
68 test.provider("writes the env file on deploy and skips an unchanged redeploy", (stack) =>
69 Effect.gen(function* () {
70 const fs = yield* FileSystem.FileSystem;
71 const path = yield* Path.Path;
72 const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-relay-client-config-" });
73 const target = path.join(dir, "client.env");
74 yield* fs.writeFileString(target, "KEEP=yes\n");
75 const configured = Effect.provide(
76 ConfigProvider.layer(
77 ConfigProvider.fromUnknown({ T3CODE_RELAY_CLIENT_CONFIG_ENV: target }),
78 ),
79 );
80​
81 yield* stack.deploy(PublishClientConfig(clientConfig("v1"))).pipe(configured);
82 const first = yield* fs.readFileString(target);
83 expect(first).toContain("KEEP=yes\n");
84 expect(first).toContain("T3CODE_RELAY_URL=https://relay.example.com\n");
85 expect(first).toContain("T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=client-v1\n");
86 expect(first).toContain("T3CODE_MOBILE_OTLP_TRACES_TOKEN=mobile-v1\n");
87​
88 yield* fs.writeFileString(target, `${first}MANUAL=1\n`);
89 yield* stack.deploy(PublishClientConfig(clientConfig("v1"))).pipe(configured);
90 expect(yield* fs.readFileString(target)).toContain("MANUAL=1\n");
91​
92 yield* stack.deploy(PublishClientConfig(clientConfig("v2"))).pipe(configured);
93 const third = yield* fs.readFileString(target);
94 expect(third).toContain("T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=client-v2\n");
95 expect(third).not.toContain("client-v1");
96 expect(third).toContain("KEEP=yes\n");
97 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
98 );
99​
100 test.provider("fails when the worker has no url rather than publishing an empty relay", (stack) =>
101 Effect.gen(function* () {
102 const fs = yield* FileSystem.FileSystem;
103 const path = yield* Path.Path;
104 const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-relay-client-config-" });
105 const target = path.join(dir, "client.env");
106 const exit = yield* stack
107 .deploy(PublishClientConfig({ ...clientConfig("v1"), url: undefined }))
108 .pipe(
109 Effect.provide(
110 ConfigProvider.layer(
111 ConfigProvider.fromUnknown({ T3CODE_RELAY_CLIENT_CONFIG_ENV: target }),
112 ),
113 ),
114 Effect.exit,
115 );
116 expect(exit._tag).toBe("Failure");
117 expect(String(exit)).toContain("RelayUrlUnavailableError");
118 expect(yield* fs.exists(target)).toBe(false);
119 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
120 );
121​
122 test.provider("refuses a value with a line break rather than corrupting the file", (stack) =>
123 Effect.gen(function* () {
124 const fs = yield* FileSystem.FileSystem;
125 const path = yield* Path.Path;
126 const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-relay-client-config-" });
127 const target = path.join(dir, "client.env");
128 const exit = yield* stack
129 .deploy(
130 PublishClientConfig({
131 ...clientConfig("v1"),
132 clientTracingDataset: "relay-traces\nINJECTED=1",
133 }),
134 )
135 .pipe(
136 Effect.provide(
137 ConfigProvider.layer(
138 ConfigProvider.fromUnknown({ T3CODE_RELAY_CLIENT_CONFIG_ENV: target }),
139 ),
140 ),
141 Effect.exit,
142 );
143 expect(exit._tag).toBe("Failure");
144 expect(yield* fs.exists(target)).toBe(false);
145 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
146 );
147});