@enactprotocol/secrets 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/package.json +30 -0
- package/src/dagger/index.ts +19 -0
- package/src/dagger/secret-object.ts +126 -0
- package/src/dagger/uri-parser.ts +202 -0
- package/src/env/index.ts +54 -0
- package/src/env/manager.ts +251 -0
- package/src/env/parser.ts +240 -0
- package/src/env/reader.ts +105 -0
- package/src/env/writer.ts +118 -0
- package/src/index.ts +120 -0
- package/src/keyring.ts +136 -0
- package/src/resolver.ts +184 -0
- package/src/types.ts +194 -0
- package/tests/dagger/secret-object.test.ts +217 -0
- package/tests/dagger/uri-parser.test.ts +194 -0
- package/tests/env/manager.test.ts +220 -0
- package/tests/env/parser.test.ts +352 -0
- package/tests/env/reader-writer.test.ts +257 -0
- package/tests/fixtures/complex.env +26 -0
- package/tests/fixtures/empty.env +0 -0
- package/tests/fixtures/simple.env +4 -0
- package/tests/keyring.test.ts +250 -0
- package/tests/mocks/keyring.ts +164 -0
- package/tests/resolver.test.ts +287 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createEnvContent,
|
|
4
|
+
parseEnvContent,
|
|
5
|
+
parseEnvFile,
|
|
6
|
+
removeEnvVar,
|
|
7
|
+
serializeEnvFile,
|
|
8
|
+
updateEnvVar,
|
|
9
|
+
} from "../../src/env/parser";
|
|
10
|
+
|
|
11
|
+
describe("env/parser", () => {
|
|
12
|
+
describe("parseEnvContent", () => {
|
|
13
|
+
it("should parse simple key=value pairs", () => {
|
|
14
|
+
const content = `FOO=bar
|
|
15
|
+
BAZ=qux`;
|
|
16
|
+
const result = parseEnvContent(content);
|
|
17
|
+
|
|
18
|
+
expect(result.FOO).toBe("bar");
|
|
19
|
+
expect(result.BAZ).toBe("qux");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should ignore comments", () => {
|
|
23
|
+
const content = `# This is a comment
|
|
24
|
+
FOO=bar
|
|
25
|
+
# Another comment
|
|
26
|
+
BAZ=qux`;
|
|
27
|
+
const result = parseEnvContent(content);
|
|
28
|
+
|
|
29
|
+
expect(Object.keys(result)).toHaveLength(2);
|
|
30
|
+
expect(result.FOO).toBe("bar");
|
|
31
|
+
expect(result.BAZ).toBe("qux");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should ignore empty lines", () => {
|
|
35
|
+
const content = `FOO=bar
|
|
36
|
+
|
|
37
|
+
BAZ=qux
|
|
38
|
+
|
|
39
|
+
`;
|
|
40
|
+
const result = parseEnvContent(content);
|
|
41
|
+
|
|
42
|
+
expect(Object.keys(result)).toHaveLength(2);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should handle values with = signs", () => {
|
|
46
|
+
const content = "DATABASE_URL=postgres://user:pass@host:5432/db";
|
|
47
|
+
const result = parseEnvContent(content);
|
|
48
|
+
|
|
49
|
+
expect(result.DATABASE_URL).toBe("postgres://user:pass@host:5432/db");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should handle double-quoted values", () => {
|
|
53
|
+
const content = `MESSAGE="Hello World"`;
|
|
54
|
+
const result = parseEnvContent(content);
|
|
55
|
+
|
|
56
|
+
expect(result.MESSAGE).toBe("Hello World");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should handle single-quoted values", () => {
|
|
60
|
+
const content = `MESSAGE='Hello World'`;
|
|
61
|
+
const result = parseEnvContent(content);
|
|
62
|
+
|
|
63
|
+
expect(result.MESSAGE).toBe("Hello World");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should handle inline comments for unquoted values", () => {
|
|
67
|
+
const content = "FOO=bar # this is a comment";
|
|
68
|
+
const result = parseEnvContent(content);
|
|
69
|
+
|
|
70
|
+
expect(result.FOO).toBe("bar");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should not treat # in quoted values as comments", () => {
|
|
74
|
+
const content = `MESSAGE="Hello #World"`;
|
|
75
|
+
const result = parseEnvContent(content);
|
|
76
|
+
|
|
77
|
+
expect(result.MESSAGE).toBe("Hello #World");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should handle escape sequences in double quotes", () => {
|
|
81
|
+
const content = `MESSAGE="Line1\\nLine2"`;
|
|
82
|
+
const result = parseEnvContent(content);
|
|
83
|
+
|
|
84
|
+
expect(result.MESSAGE).toBe("Line1\nLine2");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should handle escaped quotes in double-quoted values", () => {
|
|
88
|
+
const content = `MESSAGE="Say \\"Hello\\""`;
|
|
89
|
+
const result = parseEnvContent(content);
|
|
90
|
+
|
|
91
|
+
expect(result.MESSAGE).toBe('Say "Hello"');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should handle empty values", () => {
|
|
95
|
+
const content = `EMPTY=
|
|
96
|
+
FOO=bar`;
|
|
97
|
+
const result = parseEnvContent(content);
|
|
98
|
+
|
|
99
|
+
expect(result.EMPTY).toBe("");
|
|
100
|
+
expect(result.FOO).toBe("bar");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should handle values with spaces (unquoted)", () => {
|
|
104
|
+
const content = "MESSAGE=Hello World";
|
|
105
|
+
const result = parseEnvContent(content);
|
|
106
|
+
|
|
107
|
+
// Unquoted values may include spaces until inline comment
|
|
108
|
+
expect(result.MESSAGE).toBe("Hello World");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should treat lines without = as invalid", () => {
|
|
112
|
+
const content = `VALID=value
|
|
113
|
+
INVALID_LINE
|
|
114
|
+
ANOTHER=valid`;
|
|
115
|
+
const result = parseEnvContent(content);
|
|
116
|
+
|
|
117
|
+
expect(Object.keys(result)).toHaveLength(2);
|
|
118
|
+
expect(result.VALID).toBe("value");
|
|
119
|
+
expect(result.ANOTHER).toBe("valid");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("parseEnvFile", () => {
|
|
124
|
+
it("should preserve original line structure", () => {
|
|
125
|
+
const content = `# Header comment
|
|
126
|
+
FOO=bar
|
|
127
|
+
|
|
128
|
+
# Section
|
|
129
|
+
BAZ=qux`;
|
|
130
|
+
const result = parseEnvFile(content);
|
|
131
|
+
|
|
132
|
+
expect(result.lines).toHaveLength(5);
|
|
133
|
+
expect(result.lines[0]?.type).toBe("comment");
|
|
134
|
+
expect(result.lines[1]?.type).toBe("variable");
|
|
135
|
+
expect(result.lines[2]?.type).toBe("empty");
|
|
136
|
+
expect(result.lines[3]?.type).toBe("comment");
|
|
137
|
+
expect(result.lines[4]?.type).toBe("variable");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should store raw line content", () => {
|
|
141
|
+
const content = "FOO=bar";
|
|
142
|
+
const result = parseEnvFile(content);
|
|
143
|
+
|
|
144
|
+
expect(result.lines[0]?.raw).toBe("FOO=bar");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should return both vars and lines", () => {
|
|
148
|
+
const content = `FOO=bar
|
|
149
|
+
BAZ=qux`;
|
|
150
|
+
const result = parseEnvFile(content);
|
|
151
|
+
|
|
152
|
+
expect(result.vars.FOO).toBe("bar");
|
|
153
|
+
expect(result.vars.BAZ).toBe("qux");
|
|
154
|
+
expect(result.lines).toHaveLength(2);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("serializeEnvFile", () => {
|
|
159
|
+
it("should serialize a parsed env file back to string", () => {
|
|
160
|
+
const content = `FOO=bar
|
|
161
|
+
BAZ=qux`;
|
|
162
|
+
const parsed = parseEnvFile(content);
|
|
163
|
+
const serialized = serializeEnvFile(parsed);
|
|
164
|
+
|
|
165
|
+
expect(serialized).toBe(content);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should preserve comments", () => {
|
|
169
|
+
const content = `# Comment
|
|
170
|
+
FOO=bar`;
|
|
171
|
+
const parsed = parseEnvFile(content);
|
|
172
|
+
const serialized = serializeEnvFile(parsed);
|
|
173
|
+
|
|
174
|
+
expect(serialized).toBe(content);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should preserve empty lines", () => {
|
|
178
|
+
const content = `FOO=bar
|
|
179
|
+
|
|
180
|
+
BAZ=qux`;
|
|
181
|
+
const parsed = parseEnvFile(content);
|
|
182
|
+
const serialized = serializeEnvFile(parsed);
|
|
183
|
+
|
|
184
|
+
expect(serialized).toBe(content);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should quote values with spaces", () => {
|
|
188
|
+
const parsed = parseEnvFile("FOO=bar");
|
|
189
|
+
parsed.vars.FOO = "hello world";
|
|
190
|
+
|
|
191
|
+
const serialized = serializeEnvFile(parsed);
|
|
192
|
+
expect(serialized).toBe('FOO="hello world"');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should quote values with = signs", () => {
|
|
196
|
+
const parsed = parseEnvFile("URL=test");
|
|
197
|
+
parsed.vars.URL = "host=localhost";
|
|
198
|
+
|
|
199
|
+
const serialized = serializeEnvFile(parsed);
|
|
200
|
+
expect(serialized).toBe('URL="host=localhost"');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should escape quotes in quoted values", () => {
|
|
204
|
+
const parsed = parseEnvFile("MSG=test");
|
|
205
|
+
parsed.vars.MSG = 'Say "Hello"';
|
|
206
|
+
|
|
207
|
+
const serialized = serializeEnvFile(parsed);
|
|
208
|
+
expect(serialized).toBe('MSG="Say \\"Hello\\""');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("createEnvContent", () => {
|
|
213
|
+
it("should create env content from object", () => {
|
|
214
|
+
const vars = {
|
|
215
|
+
FOO: "bar",
|
|
216
|
+
BAZ: "qux",
|
|
217
|
+
};
|
|
218
|
+
const result = createEnvContent(vars);
|
|
219
|
+
|
|
220
|
+
expect(result).toContain("FOO=bar");
|
|
221
|
+
expect(result).toContain("BAZ=qux");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should quote values with special characters", () => {
|
|
225
|
+
const vars = {
|
|
226
|
+
MESSAGE: "Hello World",
|
|
227
|
+
};
|
|
228
|
+
const result = createEnvContent(vars);
|
|
229
|
+
|
|
230
|
+
expect(result).toBe('MESSAGE="Hello World"');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should handle empty object", () => {
|
|
234
|
+
const result = createEnvContent({});
|
|
235
|
+
expect(result).toBe("");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("updateEnvVar", () => {
|
|
240
|
+
it("should update existing variable", () => {
|
|
241
|
+
const content = `FOO=old
|
|
242
|
+
BAZ=qux`;
|
|
243
|
+
const parsed = parseEnvFile(content);
|
|
244
|
+
const updated = updateEnvVar(parsed, "FOO", "new");
|
|
245
|
+
|
|
246
|
+
expect(updated.vars.FOO).toBe("new");
|
|
247
|
+
expect(updated.lines).toHaveLength(2);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should add new variable if not exists", () => {
|
|
251
|
+
const content = "FOO=bar";
|
|
252
|
+
const parsed = parseEnvFile(content);
|
|
253
|
+
const updated = updateEnvVar(parsed, "NEW_VAR", "new-value");
|
|
254
|
+
|
|
255
|
+
expect(updated.vars.NEW_VAR).toBe("new-value");
|
|
256
|
+
expect(updated.lines).toHaveLength(2);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should preserve other lines", () => {
|
|
260
|
+
const content = `# Comment
|
|
261
|
+
FOO=bar
|
|
262
|
+
|
|
263
|
+
BAZ=qux`;
|
|
264
|
+
const parsed = parseEnvFile(content);
|
|
265
|
+
const updated = updateEnvVar(parsed, "FOO", "new");
|
|
266
|
+
|
|
267
|
+
expect(updated.lines).toHaveLength(4);
|
|
268
|
+
expect(updated.lines[0]?.type).toBe("comment");
|
|
269
|
+
expect(updated.lines[2]?.type).toBe("empty");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should update line's raw content", () => {
|
|
273
|
+
const content = "FOO=old";
|
|
274
|
+
const parsed = parseEnvFile(content);
|
|
275
|
+
const updated = updateEnvVar(parsed, "FOO", "new");
|
|
276
|
+
const line = updated.lines[0];
|
|
277
|
+
|
|
278
|
+
expect(line?.raw).toBe("FOO=new");
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("removeEnvVar", () => {
|
|
283
|
+
it("should remove existing variable", () => {
|
|
284
|
+
const content = `FOO=bar
|
|
285
|
+
BAZ=qux`;
|
|
286
|
+
const parsed = parseEnvFile(content);
|
|
287
|
+
const updated = removeEnvVar(parsed, "FOO");
|
|
288
|
+
|
|
289
|
+
expect(updated.vars.FOO).toBeUndefined();
|
|
290
|
+
expect(updated.vars.BAZ).toBe("qux");
|
|
291
|
+
expect(updated.lines).toHaveLength(1);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should preserve comments and empty lines", () => {
|
|
295
|
+
const content = `# Comment
|
|
296
|
+
FOO=bar
|
|
297
|
+
|
|
298
|
+
BAZ=qux`;
|
|
299
|
+
const parsed = parseEnvFile(content);
|
|
300
|
+
const updated = removeEnvVar(parsed, "FOO");
|
|
301
|
+
|
|
302
|
+
expect(updated.lines).toHaveLength(3);
|
|
303
|
+
expect(updated.lines[0]?.type).toBe("comment");
|
|
304
|
+
expect(updated.lines[1]?.type).toBe("empty");
|
|
305
|
+
expect(updated.lines[2]?.type).toBe("variable");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should return same object if key not found", () => {
|
|
309
|
+
const content = "FOO=bar";
|
|
310
|
+
const parsed = parseEnvFile(content);
|
|
311
|
+
const updated = removeEnvVar(parsed, "NONEXISTENT");
|
|
312
|
+
|
|
313
|
+
expect(Object.keys(updated.vars)).toHaveLength(1);
|
|
314
|
+
expect(updated.vars.FOO).toBe("bar");
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe("complex .env files", () => {
|
|
319
|
+
it("should parse complex fixture", () => {
|
|
320
|
+
const content = `# Database configuration
|
|
321
|
+
DATABASE_URL="postgres://user:pass@localhost:5432/mydb"
|
|
322
|
+
DATABASE_POOL_SIZE=10
|
|
323
|
+
|
|
324
|
+
# API Keys
|
|
325
|
+
STRIPE_API_KEY=sk_test_12345 # development key
|
|
326
|
+
STRIPE_WEBHOOK_SECRET="whsec_abc123"
|
|
327
|
+
|
|
328
|
+
# Feature Flags
|
|
329
|
+
ENABLE_NEW_FEATURE=true
|
|
330
|
+
DEPRECATED_FEATURE=false
|
|
331
|
+
|
|
332
|
+
# Multiline-like content
|
|
333
|
+
JSON_CONFIG='{"key": "value"}'
|
|
334
|
+
MESSAGE="Line 1\\nLine 2"`;
|
|
335
|
+
|
|
336
|
+
const result = parseEnvFile(content);
|
|
337
|
+
|
|
338
|
+
expect(result.vars.DATABASE_URL).toBe("postgres://user:pass@localhost:5432/mydb");
|
|
339
|
+
expect(result.vars.DATABASE_POOL_SIZE).toBe("10");
|
|
340
|
+
expect(result.vars.STRIPE_API_KEY).toBe("sk_test_12345");
|
|
341
|
+
expect(result.vars.STRIPE_WEBHOOK_SECRET).toBe("whsec_abc123");
|
|
342
|
+
expect(result.vars.ENABLE_NEW_FEATURE).toBe("true");
|
|
343
|
+
expect(result.vars.JSON_CONFIG).toBe('{"key": "value"}');
|
|
344
|
+
expect(result.vars.MESSAGE).toBe("Line 1\nLine 2");
|
|
345
|
+
|
|
346
|
+
// Check structure is preserved
|
|
347
|
+
expect(result.lines.filter((l) => l.type === "comment")).toHaveLength(4);
|
|
348
|
+
expect(result.lines.filter((l) => l.type === "empty")).toHaveLength(3);
|
|
349
|
+
expect(result.lines.filter((l) => l.type === "variable")).toHaveLength(8);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
getGlobalEnvPath,
|
|
7
|
+
getLocalEnvPath,
|
|
8
|
+
loadLocalEnv,
|
|
9
|
+
localEnvExists,
|
|
10
|
+
readEnvFile,
|
|
11
|
+
readEnvVars,
|
|
12
|
+
} from "../../src/env/reader";
|
|
13
|
+
import {
|
|
14
|
+
deleteEnvVar,
|
|
15
|
+
deleteLocalEnvVar,
|
|
16
|
+
setEnvVar,
|
|
17
|
+
setLocalEnvVar,
|
|
18
|
+
writeEnvFile,
|
|
19
|
+
writeEnvVars,
|
|
20
|
+
} from "../../src/env/writer";
|
|
21
|
+
|
|
22
|
+
describe("env/reader", () => {
|
|
23
|
+
const testDir = join(tmpdir(), `enact-test-${Date.now()}`);
|
|
24
|
+
const testEnvPath = join(testDir, ".env");
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
mkdirSync(testDir, { recursive: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
if (existsSync(testDir)) {
|
|
32
|
+
rmSync(testDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("getGlobalEnvPath", () => {
|
|
37
|
+
it("should return path to ~/.enact/.env", () => {
|
|
38
|
+
const path = getGlobalEnvPath();
|
|
39
|
+
expect(path).toContain(".enact");
|
|
40
|
+
expect(path).toContain(".env");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("getLocalEnvPath", () => {
|
|
45
|
+
it("should return path relative to cwd", () => {
|
|
46
|
+
const path = getLocalEnvPath(testDir);
|
|
47
|
+
expect(path).toBe(join(testDir, ".enact", ".env"));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("readEnvFile", () => {
|
|
52
|
+
it("should read and parse an existing .env file", () => {
|
|
53
|
+
writeFileSync(testEnvPath, "FOO=bar\nBAZ=qux");
|
|
54
|
+
|
|
55
|
+
const result = readEnvFile(testEnvPath);
|
|
56
|
+
|
|
57
|
+
expect(result.vars.FOO).toBe("bar");
|
|
58
|
+
expect(result.vars.BAZ).toBe("qux");
|
|
59
|
+
expect(result.lines).toHaveLength(2);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should return empty object for non-existent file", () => {
|
|
63
|
+
const result = readEnvFile(join(testDir, "nonexistent.env"));
|
|
64
|
+
|
|
65
|
+
expect(result.vars).toEqual({});
|
|
66
|
+
expect(result.lines).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("readEnvVars", () => {
|
|
71
|
+
it("should read .env file as key-value object", () => {
|
|
72
|
+
writeFileSync(testEnvPath, "FOO=bar\nBAZ=qux");
|
|
73
|
+
|
|
74
|
+
const result = readEnvVars(testEnvPath);
|
|
75
|
+
|
|
76
|
+
expect(result).toEqual({ FOO: "bar", BAZ: "qux" });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should return empty object for non-existent file", () => {
|
|
80
|
+
const result = readEnvVars(join(testDir, "nonexistent.env"));
|
|
81
|
+
expect(result).toEqual({});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("loadLocalEnv", () => {
|
|
86
|
+
it("should load local .env file", () => {
|
|
87
|
+
const localDir = join(testDir, ".enact");
|
|
88
|
+
mkdirSync(localDir, { recursive: true });
|
|
89
|
+
writeFileSync(join(localDir, ".env"), "LOCAL_KEY=local-value");
|
|
90
|
+
|
|
91
|
+
const result = loadLocalEnv(testDir);
|
|
92
|
+
|
|
93
|
+
expect(result.LOCAL_KEY).toBe("local-value");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should return empty object if local .env doesn't exist", () => {
|
|
97
|
+
const result = loadLocalEnv(testDir);
|
|
98
|
+
expect(result).toEqual({});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("localEnvExists", () => {
|
|
103
|
+
it("should return true if local .env exists", () => {
|
|
104
|
+
const localDir = join(testDir, ".enact");
|
|
105
|
+
mkdirSync(localDir, { recursive: true });
|
|
106
|
+
writeFileSync(join(localDir, ".env"), "KEY=value");
|
|
107
|
+
|
|
108
|
+
expect(localEnvExists(testDir)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should return false if local .env doesn't exist", () => {
|
|
112
|
+
expect(localEnvExists(testDir)).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("env/writer", () => {
|
|
118
|
+
const testDir = join(tmpdir(), `enact-test-writer-${Date.now()}`);
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
mkdirSync(testDir, { recursive: true });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
if (existsSync(testDir)) {
|
|
126
|
+
rmSync(testDir, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("writeEnvFile", () => {
|
|
131
|
+
it("should write parsed env file to disk", () => {
|
|
132
|
+
const envPath = join(testDir, "test.env");
|
|
133
|
+
const parsed = {
|
|
134
|
+
vars: { FOO: "bar", BAZ: "qux" },
|
|
135
|
+
lines: [
|
|
136
|
+
{ type: "variable" as const, raw: "FOO=bar", key: "FOO", value: "bar" },
|
|
137
|
+
{ type: "variable" as const, raw: "BAZ=qux", key: "BAZ", value: "qux" },
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
writeEnvFile(envPath, parsed);
|
|
142
|
+
const result = readEnvVars(envPath);
|
|
143
|
+
|
|
144
|
+
expect(result).toEqual({ FOO: "bar", BAZ: "qux" });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should create parent directories if needed", () => {
|
|
148
|
+
const envPath = join(testDir, "nested", "deep", ".env");
|
|
149
|
+
|
|
150
|
+
writeEnvFile(envPath, { vars: { KEY: "value" }, lines: [] });
|
|
151
|
+
|
|
152
|
+
expect(existsSync(join(testDir, "nested", "deep"))).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("writeEnvVars", () => {
|
|
157
|
+
it("should write key-value object to .env file", () => {
|
|
158
|
+
const envPath = join(testDir, "test.env");
|
|
159
|
+
|
|
160
|
+
writeEnvVars(envPath, { FOO: "bar", BAZ: "qux" });
|
|
161
|
+
const result = readEnvVars(envPath);
|
|
162
|
+
|
|
163
|
+
expect(result).toEqual({ FOO: "bar", BAZ: "qux" });
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("setEnvVar", () => {
|
|
168
|
+
it("should add a new variable to existing file", () => {
|
|
169
|
+
const envPath = join(testDir, "test.env");
|
|
170
|
+
writeFileSync(envPath, "FOO=bar");
|
|
171
|
+
|
|
172
|
+
setEnvVar(envPath, "BAZ", "qux");
|
|
173
|
+
const result = readEnvVars(envPath);
|
|
174
|
+
|
|
175
|
+
expect(result).toEqual({ FOO: "bar", BAZ: "qux" });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should update an existing variable", () => {
|
|
179
|
+
const envPath = join(testDir, "test.env");
|
|
180
|
+
writeFileSync(envPath, "FOO=old");
|
|
181
|
+
|
|
182
|
+
setEnvVar(envPath, "FOO", "new");
|
|
183
|
+
const result = readEnvVars(envPath);
|
|
184
|
+
|
|
185
|
+
expect(result.FOO).toBe("new");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should create file if it doesn't exist", () => {
|
|
189
|
+
const envPath = join(testDir, "newfile.env");
|
|
190
|
+
|
|
191
|
+
setEnvVar(envPath, "KEY", "value");
|
|
192
|
+
const result = readEnvVars(envPath);
|
|
193
|
+
|
|
194
|
+
expect(result.KEY).toBe("value");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should preserve comments and formatting", () => {
|
|
198
|
+
const envPath = join(testDir, "test.env");
|
|
199
|
+
writeFileSync(envPath, "# Comment\nFOO=bar\n\nBAZ=qux");
|
|
200
|
+
|
|
201
|
+
setEnvVar(envPath, "FOO", "updated");
|
|
202
|
+
const parsed = readEnvFile(envPath);
|
|
203
|
+
|
|
204
|
+
expect(parsed.lines[0]?.type).toBe("comment");
|
|
205
|
+
expect(parsed.lines[2]?.type).toBe("empty");
|
|
206
|
+
expect(parsed.vars.FOO).toBe("updated");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("deleteEnvVar", () => {
|
|
211
|
+
it("should delete an existing variable", () => {
|
|
212
|
+
const envPath = join(testDir, "test.env");
|
|
213
|
+
writeFileSync(envPath, "FOO=bar\nBAZ=qux");
|
|
214
|
+
|
|
215
|
+
const deleted = deleteEnvVar(envPath, "FOO");
|
|
216
|
+
const result = readEnvVars(envPath);
|
|
217
|
+
|
|
218
|
+
expect(deleted).toBe(true);
|
|
219
|
+
expect(result.FOO).toBeUndefined();
|
|
220
|
+
expect(result.BAZ).toBe("qux");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should return false for non-existent variable", () => {
|
|
224
|
+
const envPath = join(testDir, "test.env");
|
|
225
|
+
writeFileSync(envPath, "FOO=bar");
|
|
226
|
+
|
|
227
|
+
const deleted = deleteEnvVar(envPath, "NONEXISTENT");
|
|
228
|
+
|
|
229
|
+
expect(deleted).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("setLocalEnvVar", () => {
|
|
234
|
+
it("should set a variable in local .env", () => {
|
|
235
|
+
setLocalEnvVar("LOCAL_KEY", "local-value", testDir);
|
|
236
|
+
const result = loadLocalEnv(testDir);
|
|
237
|
+
|
|
238
|
+
expect(result.LOCAL_KEY).toBe("local-value");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should create .enact directory if needed", () => {
|
|
242
|
+
setLocalEnvVar("KEY", "value", testDir);
|
|
243
|
+
|
|
244
|
+
expect(existsSync(join(testDir, ".enact"))).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("deleteLocalEnvVar", () => {
|
|
249
|
+
it("should delete a variable from local .env", () => {
|
|
250
|
+
setLocalEnvVar("KEY", "value", testDir);
|
|
251
|
+
const deleted = deleteLocalEnvVar("KEY", testDir);
|
|
252
|
+
|
|
253
|
+
expect(deleted).toBe(true);
|
|
254
|
+
expect(loadLocalEnv(testDir).KEY).toBeUndefined();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Complex .env file with various formats
|
|
2
|
+
|
|
3
|
+
# Quoted values
|
|
4
|
+
SINGLE_QUOTED='value with spaces'
|
|
5
|
+
DOUBLE_QUOTED="another value with spaces"
|
|
6
|
+
|
|
7
|
+
# Escaped sequences in double quotes
|
|
8
|
+
ESCAPED_NEWLINE="line1\nline2"
|
|
9
|
+
ESCAPED_TAB="col1\tcol2"
|
|
10
|
+
|
|
11
|
+
# Values with special characters
|
|
12
|
+
URL_WITH_PARAMS=https://api.example.com?key=value&other=123
|
|
13
|
+
PATH_VALUE=/usr/local/bin:/home/user/bin
|
|
14
|
+
|
|
15
|
+
# Inline comments
|
|
16
|
+
SETTING=value # this is a comment
|
|
17
|
+
|
|
18
|
+
# Empty and whitespace
|
|
19
|
+
EMPTY_VALUE=
|
|
20
|
+
SPACES_AROUND = trimmed
|
|
21
|
+
|
|
22
|
+
# Values with equals sign
|
|
23
|
+
CONNECTION_STRING=host=localhost;port=5432;db=mydb
|
|
24
|
+
|
|
25
|
+
# Multi-word unquoted (should work)
|
|
26
|
+
SIMPLE_TEXT=hello world
|
|
File without changes
|