@geekmidas/envkit 0.2.0 → 0.4.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.
Files changed (73) hide show
  1. package/dist/{EnvironmentBuilder-DfmYRBm-.mjs → EnvironmentBuilder-BSuHZm0y.mjs} +2 -4
  2. package/dist/EnvironmentBuilder-BSuHZm0y.mjs.map +1 -0
  3. package/dist/EnvironmentBuilder-DHfDXJUm.d.mts.map +1 -0
  4. package/dist/{EnvironmentBuilder-W2wku49g.cjs → EnvironmentBuilder-Djr1VsWM.cjs} +2 -4
  5. package/dist/EnvironmentBuilder-Djr1VsWM.cjs.map +1 -0
  6. package/dist/EnvironmentBuilder-Xuf2Dd9u.d.cts.map +1 -0
  7. package/dist/EnvironmentBuilder.cjs +1 -1
  8. package/dist/EnvironmentBuilder.mjs +1 -1
  9. package/dist/EnvironmentParser-Bt246UeP.cjs.map +1 -1
  10. package/dist/{EnvironmentParser-CVWU1ooT.d.mts → EnvironmentParser-CY8TosTN.d.mts} +2 -1
  11. package/dist/EnvironmentParser-CY8TosTN.d.mts.map +1 -0
  12. package/dist/{EnvironmentParser-tV-JjCg7.d.cts → EnvironmentParser-DtOL86NU.d.cts} +2 -1
  13. package/dist/EnvironmentParser-DtOL86NU.d.cts.map +1 -0
  14. package/dist/EnvironmentParser-c06agx31.mjs.map +1 -1
  15. package/dist/EnvironmentParser.d.cts +1 -1
  16. package/dist/EnvironmentParser.d.mts +1 -1
  17. package/dist/SnifferEnvironmentParser.cjs.map +1 -1
  18. package/dist/SnifferEnvironmentParser.d.cts +3 -2
  19. package/dist/SnifferEnvironmentParser.d.cts.map +1 -0
  20. package/dist/SnifferEnvironmentParser.d.mts +3 -2
  21. package/dist/SnifferEnvironmentParser.d.mts.map +1 -0
  22. package/dist/SnifferEnvironmentParser.mjs.map +1 -1
  23. package/dist/{SstEnvironmentBuilder-DEa3lTUB.mjs → SstEnvironmentBuilder-BEBFSUYr.mjs} +2 -2
  24. package/dist/SstEnvironmentBuilder-BEBFSUYr.mjs.map +1 -0
  25. package/dist/SstEnvironmentBuilder-CjURMGjW.d.mts.map +1 -0
  26. package/dist/SstEnvironmentBuilder-D4oSo_KX.d.cts.map +1 -0
  27. package/dist/{SstEnvironmentBuilder-BuFw1hCe.cjs → SstEnvironmentBuilder-wFnN2M5O.cjs} +2 -2
  28. package/dist/SstEnvironmentBuilder-wFnN2M5O.cjs.map +1 -0
  29. package/dist/SstEnvironmentBuilder.cjs +2 -2
  30. package/dist/SstEnvironmentBuilder.mjs +2 -2
  31. package/dist/credentials.cjs +66 -0
  32. package/dist/credentials.cjs.map +1 -0
  33. package/dist/credentials.d.cts +31 -0
  34. package/dist/credentials.d.cts.map +1 -0
  35. package/dist/credentials.d.mts +31 -0
  36. package/dist/credentials.d.mts.map +1 -0
  37. package/dist/credentials.mjs +62 -0
  38. package/dist/credentials.mjs.map +1 -0
  39. package/dist/index.cjs +1 -1
  40. package/dist/index.d.cts +1 -1
  41. package/dist/index.d.mts +1 -1
  42. package/dist/index.mjs +1 -1
  43. package/dist/sst.cjs +2 -2
  44. package/dist/sst.cjs.map +1 -1
  45. package/dist/sst.d.cts +1 -0
  46. package/dist/sst.d.cts.map +1 -0
  47. package/dist/sst.d.mts +1 -0
  48. package/dist/sst.d.mts.map +1 -0
  49. package/dist/sst.mjs +2 -2
  50. package/dist/sst.mjs.map +1 -1
  51. package/examples/basic-usage.ts +329 -333
  52. package/package.json +6 -1
  53. package/src/EnvironmentBuilder.ts +76 -80
  54. package/src/EnvironmentParser.ts +231 -231
  55. package/src/SnifferEnvironmentParser.ts +178 -178
  56. package/src/SstEnvironmentBuilder.ts +127 -127
  57. package/src/__tests__/ConfigParser.spec.ts +388 -388
  58. package/src/__tests__/EnvironmentBuilder.spec.ts +245 -265
  59. package/src/__tests__/EnvironmentParser.spec.ts +828 -828
  60. package/src/__tests__/SnifferEnvironmentParser.spec.ts +380 -326
  61. package/src/__tests__/SstEnvironmentBuilder.spec.ts +347 -367
  62. package/src/__tests__/credentials.integration.spec.ts +239 -0
  63. package/src/__tests__/credentials.spec.ts +136 -0
  64. package/src/__tests__/sst.spec.ts +390 -413
  65. package/src/credentials.ts +99 -0
  66. package/src/index.ts +11 -11
  67. package/src/sst.ts +24 -24
  68. package/sst-env.d.ts +0 -1
  69. package/tsconfig.json +9 -0
  70. package/dist/EnvironmentBuilder-DfmYRBm-.mjs.map +0 -1
  71. package/dist/EnvironmentBuilder-W2wku49g.cjs.map +0 -1
  72. package/dist/SstEnvironmentBuilder-BuFw1hCe.cjs.map +0 -1
  73. package/dist/SstEnvironmentBuilder-DEa3lTUB.mjs.map +0 -1
@@ -3,330 +3,384 @@ import { z } from 'zod/v4';
3
3
  import { SnifferEnvironmentParser } from '../SnifferEnvironmentParser';
4
4
 
5
5
  describe('SnifferEnvironmentParser', () => {
6
- describe('Environment variable tracking', () => {
7
- it('should track accessed environment variables', () => {
8
- const sniffer = new SnifferEnvironmentParser();
9
-
10
- sniffer.create((get) => ({
11
- appName: get('APP_NAME').string(),
12
- port: get('PORT').string().transform(Number),
13
- }));
14
-
15
- const envVars = sniffer.getEnvironmentVariables();
16
- expect(envVars).toEqual(['APP_NAME', 'PORT']);
17
- });
18
-
19
- it('should track variables in nested configurations', () => {
20
- const sniffer = new SnifferEnvironmentParser();
21
-
22
- sniffer.create((get) => ({
23
- database: {
24
- host: get('DB_HOST').string(),
25
- port: get('DB_PORT').string().transform(Number),
26
- },
27
- api: {
28
- key: get('API_KEY').string(),
29
- },
30
- }));
31
-
32
- const envVars = sniffer.getEnvironmentVariables();
33
- expect(envVars).toEqual(['API_KEY', 'DB_HOST', 'DB_PORT']);
34
- });
35
-
36
- it('should return sorted environment variable names', () => {
37
- const sniffer = new SnifferEnvironmentParser();
38
-
39
- sniffer.create((get) => ({
40
- zValue: get('Z_VALUE').string(),
41
- aValue: get('A_VALUE').string(),
42
- mValue: get('M_VALUE').string(),
43
- }));
44
-
45
- const envVars = sniffer.getEnvironmentVariables();
46
- expect(envVars).toEqual(['A_VALUE', 'M_VALUE', 'Z_VALUE']);
47
- });
48
-
49
- it('should deduplicate environment variable names', () => {
50
- const sniffer = new SnifferEnvironmentParser();
51
-
52
- sniffer.create((get) => ({
53
- value1: get('SHARED_VAR').string(),
54
- value2: get('SHARED_VAR').string(),
55
- value3: get('SHARED_VAR').string(),
56
- }));
57
-
58
- const envVars = sniffer.getEnvironmentVariables();
59
- expect(envVars).toEqual(['SHARED_VAR']);
60
- });
61
-
62
- it('should track variables accessed through coerce', () => {
63
- const sniffer = new SnifferEnvironmentParser();
64
-
65
- sniffer.create((get) => ({
66
- workers: get('NUM_WORKERS').coerce.number(),
67
- timeout: get('TIMEOUT').coerce.number(),
68
- }));
69
-
70
- const envVars = sniffer.getEnvironmentVariables();
71
- expect(envVars).toEqual(['NUM_WORKERS', 'TIMEOUT']);
72
- });
73
- });
74
-
75
- describe('Mock value parsing', () => {
76
- it('should never throw when parsing - returns mock values', () => {
77
- const sniffer = new SnifferEnvironmentParser();
78
-
79
- const config = sniffer.create((get) => ({
80
- required: get('REQUIRED_VAR').string(),
81
- alsoRequired: get('ALSO_REQUIRED').string(),
82
- }));
83
-
84
- // Should not throw even though env vars are not set
85
- expect(() => config.parse()).not.toThrow();
86
- });
87
-
88
- it('should return empty string for string schemas', () => {
89
- const sniffer = new SnifferEnvironmentParser();
90
-
91
- const config = sniffer
92
- .create((get) => ({
93
- value: get('STRING_VAR').string(),
94
- }))
95
- .parse();
96
-
97
- expect(config.value).toBe('');
98
- });
99
-
100
- it('should return 0 for number schemas', () => {
101
- const sniffer = new SnifferEnvironmentParser();
102
-
103
- const config = sniffer
104
- .create((get) => ({
105
- value: get('NUMBER_VAR').coerce.number(),
106
- }))
107
- .parse();
108
-
109
- expect(config.value).toBe(0);
110
- });
111
-
112
- it('should return false for boolean schemas', () => {
113
- const sniffer = new SnifferEnvironmentParser();
114
-
115
- const config = sniffer
116
- .create((get) => ({
117
- value: get('BOOL_VAR').coerce.boolean(),
118
- }))
119
- .parse();
120
-
121
- expect(config.value).toBe(false);
122
- });
123
-
124
- it('should return empty array for array schemas', () => {
125
- const sniffer = new SnifferEnvironmentParser();
126
-
127
- const config = sniffer
128
- .create((get) => ({
129
- value: get('ARRAY_VAR').array(z.string()),
130
- }))
131
- .parse();
132
-
133
- expect(config.value).toEqual([]);
134
- });
135
-
136
- it('should return undefined for optional schemas', () => {
137
- const sniffer = new SnifferEnvironmentParser();
138
-
139
- const config = sniffer
140
- .create((get) => ({
141
- value: get('OPTIONAL_VAR').string().optional(),
142
- }))
143
- .parse();
144
-
145
- expect(config.value).toBeUndefined();
146
- });
147
-
148
- it('should handle nested configurations', () => {
149
- const sniffer = new SnifferEnvironmentParser();
150
-
151
- const config = sniffer
152
- .create((get) => ({
153
- database: {
154
- host: get('DB_HOST').string(),
155
- port: get('DB_PORT').coerce.number(),
156
- },
157
- cache: {
158
- enabled: get('CACHE_ENABLED').coerce.boolean(),
159
- },
160
- }))
161
- .parse();
162
-
163
- expect(config).toEqual({
164
- database: {
165
- host: '',
166
- port: 0,
167
- },
168
- cache: {
169
- enabled: false,
170
- },
171
- });
172
- });
173
-
174
- it('should track variables with transforms', () => {
175
- const sniffer = new SnifferEnvironmentParser();
176
-
177
- sniffer.create((get) => ({
178
- origins: get('ALLOWED_ORIGINS')
179
- .string()
180
- .transform((v) => v.split(',')),
181
- port: get('PORT').string().transform(Number),
182
- }));
183
-
184
- // Should track the env vars even with transforms
185
- expect(sniffer.getEnvironmentVariables()).toEqual([
186
- 'ALLOWED_ORIGINS',
187
- 'PORT',
188
- ]);
189
- });
190
- });
191
-
192
- describe('Service registration simulation', () => {
193
- it('should allow simulated service registration to succeed', () => {
194
- const sniffer = new SnifferEnvironmentParser();
195
-
196
- // Simulate a service that would normally fail without env vars
197
- const mockService = {
198
- serviceName: 'database' as const,
199
- register(envParser: SnifferEnvironmentParser) {
200
- const config = envParser
201
- .create((get) => ({
202
- url: get('DATABASE_URL').string(),
203
- poolSize: get('DB_POOL_SIZE').coerce.number(),
204
- }))
205
- .parse();
206
-
207
- // Service uses parsed values to create connection
208
- return { url: config.url, poolSize: config.poolSize };
209
- },
210
- };
211
-
212
- // Should not throw
213
- expect(() => mockService.register(sniffer)).not.toThrow();
214
-
215
- // Should have tracked the env vars
216
- expect(sniffer.getEnvironmentVariables()).toEqual([
217
- 'DATABASE_URL',
218
- 'DB_POOL_SIZE',
219
- ]);
220
- });
221
-
222
- it('should work with multiple services', () => {
223
- const sniffer = new SnifferEnvironmentParser();
224
-
225
- const databaseService = {
226
- serviceName: 'db' as const,
227
- register(envParser: SnifferEnvironmentParser) {
228
- return envParser
229
- .create((get) => ({
230
- host: get('DB_HOST').string(),
231
- port: get('DB_PORT').coerce.number(),
232
- }))
233
- .parse();
234
- },
235
- };
236
-
237
- const cacheService = {
238
- serviceName: 'cache' as const,
239
- register(envParser: SnifferEnvironmentParser) {
240
- return envParser
241
- .create((get) => ({
242
- url: get('REDIS_URL').string(),
243
- ttl: get('CACHE_TTL').coerce.number(),
244
- }))
245
- .parse();
246
- },
247
- };
248
-
249
- // Register both services
250
- databaseService.register(sniffer);
251
- cacheService.register(sniffer);
252
-
253
- // Should have tracked all env vars from both services
254
- expect(sniffer.getEnvironmentVariables()).toEqual([
255
- 'CACHE_TTL',
256
- 'DB_HOST',
257
- 'DB_PORT',
258
- 'REDIS_URL',
259
- ]);
260
- });
261
-
262
- it('should handle async service registration', async () => {
263
- const sniffer = new SnifferEnvironmentParser();
264
-
265
- const asyncService = {
266
- serviceName: 'async' as const,
267
- async register(envParser: SnifferEnvironmentParser) {
268
- const config = envParser
269
- .create((get) => ({
270
- apiKey: get('API_KEY').string(),
271
- endpoint: get('API_ENDPOINT').string(),
272
- }))
273
- .parse();
274
-
275
- // Simulate async initialization
276
- await Promise.resolve();
277
- return config;
278
- },
279
- };
280
-
281
- await expect(asyncService.register(sniffer)).resolves.not.toThrow();
282
- expect(sniffer.getEnvironmentVariables()).toEqual([
283
- 'API_ENDPOINT',
284
- 'API_KEY',
285
- ]);
286
- });
287
- });
288
-
289
- describe('Edge cases', () => {
290
- it('should return empty array when no variables accessed', () => {
291
- const sniffer = new SnifferEnvironmentParser();
292
-
293
- sniffer.create(() => ({}));
294
-
295
- expect(sniffer.getEnvironmentVariables()).toEqual([]);
296
- });
297
-
298
- it('should handle deeply nested objects', () => {
299
- const sniffer = new SnifferEnvironmentParser();
300
-
301
- sniffer.create((get) => ({
302
- level1: {
303
- level2: {
304
- level3: {
305
- value: get('DEEP_VALUE').string(),
306
- },
307
- },
308
- },
309
- }));
310
-
311
- expect(sniffer.getEnvironmentVariables()).toEqual(['DEEP_VALUE']);
312
- });
313
-
314
- it('should handle multiple create calls', () => {
315
- const sniffer = new SnifferEnvironmentParser();
316
-
317
- sniffer.create((get) => ({
318
- first: get('FIRST_VAR').string(),
319
- }));
320
-
321
- sniffer.create((get) => ({
322
- second: get('SECOND_VAR').string(),
323
- }));
324
-
325
- // Should accumulate vars from both create calls
326
- expect(sniffer.getEnvironmentVariables()).toEqual([
327
- 'FIRST_VAR',
328
- 'SECOND_VAR',
329
- ]);
330
- });
331
- });
6
+ describe('Environment variable tracking', () => {
7
+ it('should track accessed environment variables', () => {
8
+ const sniffer = new SnifferEnvironmentParser();
9
+
10
+ sniffer.create((get) => ({
11
+ appName: get('APP_NAME').string(),
12
+ port: get('PORT').string().transform(Number),
13
+ }));
14
+
15
+ const envVars = sniffer.getEnvironmentVariables();
16
+ expect(envVars).toEqual(['APP_NAME', 'PORT']);
17
+ });
18
+
19
+ it('should track variables in nested configurations', () => {
20
+ const sniffer = new SnifferEnvironmentParser();
21
+
22
+ sniffer.create((get) => ({
23
+ database: {
24
+ host: get('DB_HOST').string(),
25
+ port: get('DB_PORT').string().transform(Number),
26
+ },
27
+ api: {
28
+ key: get('API_KEY').string(),
29
+ },
30
+ }));
31
+
32
+ const envVars = sniffer.getEnvironmentVariables();
33
+ expect(envVars).toEqual(['API_KEY', 'DB_HOST', 'DB_PORT']);
34
+ });
35
+
36
+ it('should return sorted environment variable names', () => {
37
+ const sniffer = new SnifferEnvironmentParser();
38
+
39
+ sniffer.create((get) => ({
40
+ zValue: get('Z_VALUE').string(),
41
+ aValue: get('A_VALUE').string(),
42
+ mValue: get('M_VALUE').string(),
43
+ }));
44
+
45
+ const envVars = sniffer.getEnvironmentVariables();
46
+ expect(envVars).toEqual(['A_VALUE', 'M_VALUE', 'Z_VALUE']);
47
+ });
48
+
49
+ it('should deduplicate environment variable names', () => {
50
+ const sniffer = new SnifferEnvironmentParser();
51
+
52
+ sniffer.create((get) => ({
53
+ value1: get('SHARED_VAR').string(),
54
+ value2: get('SHARED_VAR').string(),
55
+ value3: get('SHARED_VAR').string(),
56
+ }));
57
+
58
+ const envVars = sniffer.getEnvironmentVariables();
59
+ expect(envVars).toEqual(['SHARED_VAR']);
60
+ });
61
+
62
+ it('should track variables accessed through coerce', () => {
63
+ const sniffer = new SnifferEnvironmentParser();
64
+
65
+ sniffer.create((get) => ({
66
+ workers: get('NUM_WORKERS').coerce.number(),
67
+ timeout: get('TIMEOUT').coerce.number(),
68
+ }));
69
+
70
+ const envVars = sniffer.getEnvironmentVariables();
71
+ expect(envVars).toEqual(['NUM_WORKERS', 'TIMEOUT']);
72
+ });
73
+ });
74
+
75
+ describe('Mock value parsing', () => {
76
+ it('should never throw when parsing - returns mock values', () => {
77
+ const sniffer = new SnifferEnvironmentParser();
78
+
79
+ const config = sniffer.create((get) => ({
80
+ required: get('REQUIRED_VAR').string(),
81
+ alsoRequired: get('ALSO_REQUIRED').string(),
82
+ }));
83
+
84
+ // Should not throw even though env vars are not set
85
+ expect(() => config.parse()).not.toThrow();
86
+ });
87
+
88
+ it('should return empty string for string schemas', () => {
89
+ const sniffer = new SnifferEnvironmentParser();
90
+
91
+ const config = sniffer
92
+ .create((get) => ({
93
+ value: get('STRING_VAR').string(),
94
+ }))
95
+ .parse();
96
+
97
+ expect(config.value).toBe('');
98
+ });
99
+
100
+ it('should return 0 for number schemas', () => {
101
+ const sniffer = new SnifferEnvironmentParser();
102
+
103
+ const config = sniffer
104
+ .create((get) => ({
105
+ value: get('NUMBER_VAR').coerce.number(),
106
+ }))
107
+ .parse();
108
+
109
+ expect(config.value).toBe(0);
110
+ });
111
+
112
+ it('should return false for boolean schemas', () => {
113
+ const sniffer = new SnifferEnvironmentParser();
114
+
115
+ const config = sniffer
116
+ .create((get) => ({
117
+ value: get('BOOL_VAR').coerce.boolean(),
118
+ }))
119
+ .parse();
120
+
121
+ expect(config.value).toBe(false);
122
+ });
123
+
124
+ it('should return empty array for array schemas', () => {
125
+ const sniffer = new SnifferEnvironmentParser();
126
+
127
+ const config = sniffer
128
+ .create((get) => ({
129
+ value: get('ARRAY_VAR').array(z.string()),
130
+ }))
131
+ .parse();
132
+
133
+ expect(config.value).toEqual([]);
134
+ });
135
+
136
+ it('should return undefined for optional schemas', () => {
137
+ const sniffer = new SnifferEnvironmentParser();
138
+
139
+ const config = sniffer
140
+ .create((get) => ({
141
+ value: get('OPTIONAL_VAR').string().optional(),
142
+ }))
143
+ .parse();
144
+
145
+ expect(config.value).toBeUndefined();
146
+ });
147
+
148
+ it('should return null for nullable schemas', () => {
149
+ const sniffer = new SnifferEnvironmentParser();
150
+
151
+ const config = sniffer
152
+ .create((get) => ({
153
+ value: get('NULLABLE_VAR').string().nullable(),
154
+ }))
155
+ .parse();
156
+
157
+ expect(config.value).toBeNull();
158
+ });
159
+
160
+ it('should handle object schemas with default values', () => {
161
+ const sniffer = new SnifferEnvironmentParser();
162
+
163
+ const objectSchema = z.object({
164
+ name: z.string(),
165
+ count: z.number(),
166
+ });
167
+
168
+ const config = sniffer
169
+ .create((_get) => ({
170
+ value: objectSchema,
171
+ }))
172
+ .parse();
173
+
174
+ expect(config.value).toEqual({
175
+ name: '',
176
+ count: 0,
177
+ });
178
+ });
179
+
180
+ it('should handle nested object schemas', () => {
181
+ const sniffer = new SnifferEnvironmentParser();
182
+
183
+ const nestedSchema = z.object({
184
+ outer: z.object({
185
+ inner: z.string(),
186
+ }),
187
+ });
188
+
189
+ const config = sniffer
190
+ .create((_get) => ({
191
+ nested: nestedSchema,
192
+ }))
193
+ .parse();
194
+
195
+ expect(config.nested).toEqual({
196
+ outer: {
197
+ inner: '',
198
+ },
199
+ });
200
+ });
201
+
202
+ it('should handle nested configurations', () => {
203
+ const sniffer = new SnifferEnvironmentParser();
204
+
205
+ const config = sniffer
206
+ .create((get) => ({
207
+ database: {
208
+ host: get('DB_HOST').string(),
209
+ port: get('DB_PORT').coerce.number(),
210
+ },
211
+ cache: {
212
+ enabled: get('CACHE_ENABLED').coerce.boolean(),
213
+ },
214
+ }))
215
+ .parse();
216
+
217
+ expect(config).toEqual({
218
+ database: {
219
+ host: '',
220
+ port: 0,
221
+ },
222
+ cache: {
223
+ enabled: false,
224
+ },
225
+ });
226
+ });
227
+
228
+ it('should track variables with transforms', () => {
229
+ const sniffer = new SnifferEnvironmentParser();
230
+
231
+ sniffer.create((get) => ({
232
+ origins: get('ALLOWED_ORIGINS')
233
+ .string()
234
+ .transform((v) => v.split(',')),
235
+ port: get('PORT').string().transform(Number),
236
+ }));
237
+
238
+ // Should track the env vars even with transforms
239
+ expect(sniffer.getEnvironmentVariables()).toEqual([
240
+ 'ALLOWED_ORIGINS',
241
+ 'PORT',
242
+ ]);
243
+ });
244
+ });
245
+
246
+ describe('Service registration simulation', () => {
247
+ it('should allow simulated service registration to succeed', () => {
248
+ const sniffer = new SnifferEnvironmentParser();
249
+
250
+ // Simulate a service that would normally fail without env vars
251
+ const mockService = {
252
+ serviceName: 'database' as const,
253
+ register(envParser: SnifferEnvironmentParser) {
254
+ const config = envParser
255
+ .create((get) => ({
256
+ url: get('DATABASE_URL').string(),
257
+ poolSize: get('DB_POOL_SIZE').coerce.number(),
258
+ }))
259
+ .parse();
260
+
261
+ // Service uses parsed values to create connection
262
+ return { url: config.url, poolSize: config.poolSize };
263
+ },
264
+ };
265
+
266
+ // Should not throw
267
+ expect(() => mockService.register(sniffer)).not.toThrow();
268
+
269
+ // Should have tracked the env vars
270
+ expect(sniffer.getEnvironmentVariables()).toEqual([
271
+ 'DATABASE_URL',
272
+ 'DB_POOL_SIZE',
273
+ ]);
274
+ });
275
+
276
+ it('should work with multiple services', () => {
277
+ const sniffer = new SnifferEnvironmentParser();
278
+
279
+ const databaseService = {
280
+ serviceName: 'db' as const,
281
+ register(envParser: SnifferEnvironmentParser) {
282
+ return envParser
283
+ .create((get) => ({
284
+ host: get('DB_HOST').string(),
285
+ port: get('DB_PORT').coerce.number(),
286
+ }))
287
+ .parse();
288
+ },
289
+ };
290
+
291
+ const cacheService = {
292
+ serviceName: 'cache' as const,
293
+ register(envParser: SnifferEnvironmentParser) {
294
+ return envParser
295
+ .create((get) => ({
296
+ url: get('REDIS_URL').string(),
297
+ ttl: get('CACHE_TTL').coerce.number(),
298
+ }))
299
+ .parse();
300
+ },
301
+ };
302
+
303
+ // Register both services
304
+ databaseService.register(sniffer);
305
+ cacheService.register(sniffer);
306
+
307
+ // Should have tracked all env vars from both services
308
+ expect(sniffer.getEnvironmentVariables()).toEqual([
309
+ 'CACHE_TTL',
310
+ 'DB_HOST',
311
+ 'DB_PORT',
312
+ 'REDIS_URL',
313
+ ]);
314
+ });
315
+
316
+ it('should handle async service registration', async () => {
317
+ const sniffer = new SnifferEnvironmentParser();
318
+
319
+ const asyncService = {
320
+ serviceName: 'async' as const,
321
+ async register(envParser: SnifferEnvironmentParser) {
322
+ const config = envParser
323
+ .create((get) => ({
324
+ apiKey: get('API_KEY').string(),
325
+ endpoint: get('API_ENDPOINT').string(),
326
+ }))
327
+ .parse();
328
+
329
+ // Simulate async initialization
330
+ await Promise.resolve();
331
+ return config;
332
+ },
333
+ };
334
+
335
+ await expect(asyncService.register(sniffer)).resolves.not.toThrow();
336
+ expect(sniffer.getEnvironmentVariables()).toEqual([
337
+ 'API_ENDPOINT',
338
+ 'API_KEY',
339
+ ]);
340
+ });
341
+ });
342
+
343
+ describe('Edge cases', () => {
344
+ it('should return empty array when no variables accessed', () => {
345
+ const sniffer = new SnifferEnvironmentParser();
346
+
347
+ sniffer.create(() => ({}));
348
+
349
+ expect(sniffer.getEnvironmentVariables()).toEqual([]);
350
+ });
351
+
352
+ it('should handle deeply nested objects', () => {
353
+ const sniffer = new SnifferEnvironmentParser();
354
+
355
+ sniffer.create((get) => ({
356
+ level1: {
357
+ level2: {
358
+ level3: {
359
+ value: get('DEEP_VALUE').string(),
360
+ },
361
+ },
362
+ },
363
+ }));
364
+
365
+ expect(sniffer.getEnvironmentVariables()).toEqual(['DEEP_VALUE']);
366
+ });
367
+
368
+ it('should handle multiple create calls', () => {
369
+ const sniffer = new SnifferEnvironmentParser();
370
+
371
+ sniffer.create((get) => ({
372
+ first: get('FIRST_VAR').string(),
373
+ }));
374
+
375
+ sniffer.create((get) => ({
376
+ second: get('SECOND_VAR').string(),
377
+ }));
378
+
379
+ // Should accumulate vars from both create calls
380
+ expect(sniffer.getEnvironmentVariables()).toEqual([
381
+ 'FIRST_VAR',
382
+ 'SECOND_VAR',
383
+ ]);
384
+ });
385
+ });
332
386
  });