env-secrets 0.1.10 → 0.3.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/.devcontainer/devcontainer.json +33 -0
- package/.dockerignore +9 -0
- package/.eslintignore +4 -2
- package/.github/dependabot.yml +4 -0
- package/.github/workflows/build-main.yml +6 -2
- package/.github/workflows/deploy-docs.yml +50 -0
- package/.github/workflows/e2e-tests.yaml +54 -0
- package/.github/workflows/lint.yaml +6 -2
- package/.github/workflows/release.yml +13 -3
- package/.github/workflows/snyk.yaml +5 -1
- package/.github/workflows/unittests.yaml +18 -6
- package/.lintstagedrc +2 -7
- package/.prettierignore +6 -0
- package/AGENTS.md +149 -0
- package/Dockerfile +14 -0
- package/README.md +507 -36
- package/__e2e__/README.md +160 -0
- package/__e2e__/index.test.ts +339 -0
- package/__e2e__/setup.ts +58 -0
- package/__e2e__/utils/debug-logger.ts +45 -0
- package/__e2e__/utils/test-utils.ts +645 -0
- package/__tests__/index.test.ts +573 -31
- package/__tests__/vaults/secretsmanager.test.ts +460 -0
- package/__tests__/vaults/utils.test.ts +183 -0
- package/__tests__/version.test.ts +8 -0
- package/dist/index.js +36 -10
- package/dist/vaults/secretsmanager.js +44 -43
- package/dist/vaults/utils.js +2 -2
- package/docker-compose.yaml +29 -0
- package/docs/AWS.md +257 -0
- package/jest.config.js +4 -1
- package/jest.e2e.config.js +8 -0
- package/package.json +18 -10
- package/src/index.ts +44 -10
- package/src/vaults/secretsmanager.ts +48 -48
- package/src/vaults/utils.ts +6 -4
- package/website/docs/advanced-usage.mdx +399 -0
- package/website/docs/best-practices.mdx +416 -0
- package/website/docs/cli-reference.mdx +204 -0
- package/website/docs/examples.mdx +960 -0
- package/website/docs/faq.mdx +302 -0
- package/website/docs/index.mdx +56 -0
- package/website/docs/installation.mdx +30 -0
- package/website/docs/overview.mdx +17 -0
- package/website/docs/production-deployment.mdx +622 -0
- package/website/docs/providers/aws-secrets-manager.mdx +28 -0
- package/website/docs/security.mdx +122 -0
- package/website/docs/troubleshooting.mdx +236 -0
- package/website/docs/tutorials/local-dev/devcontainer-localstack.mdx +31 -0
- package/website/docs/tutorials/local-dev/docker-compose.mdx +22 -0
- package/website/docs/tutorials/local-dev/nextjs.mdx +18 -0
- package/website/docs/tutorials/local-dev/node-python-go.mdx +39 -0
- package/website/docs/tutorials/local-dev/quickstart.mdx +23 -0
- package/website/docusaurus.config.ts +89 -0
- package/website/package.json +21 -0
- package/website/sidebars.ts +33 -0
- package/website/src/css/custom.css +1 -0
- package/website/static/img/env-secrets.png +0 -0
- package/website/static/img/favicon.ico +0 -0
- package/website/static/img/logo.svg +4 -0
- package/website/yarn.lock +8764 -0
package/__tests__/index.test.ts
CHANGED
|
@@ -1,37 +1,579 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { writeFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import Debug from 'debug';
|
|
4
|
+
|
|
5
|
+
// Mock external dependencies
|
|
6
|
+
jest.mock('commander');
|
|
7
|
+
jest.mock('node:child_process');
|
|
8
|
+
jest.mock('node:fs');
|
|
9
|
+
jest.mock('debug', () => jest.fn());
|
|
10
|
+
jest.mock('../src/vaults/secretsmanager', () => ({
|
|
11
|
+
secretsmanager: jest.fn()
|
|
12
|
+
}));
|
|
13
|
+
jest.mock('../src/vaults/utils', () => ({
|
|
14
|
+
objectToExport: jest.fn()
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock the version import
|
|
18
|
+
jest.mock('../src/version', () => ({
|
|
19
|
+
LIB_VERSION: '1.0.0'
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Import after mocking
|
|
23
|
+
import { secretsmanager } from '../src/vaults/secretsmanager';
|
|
24
|
+
import { objectToExport } from '../src/vaults/utils';
|
|
25
|
+
|
|
26
|
+
// Mock the actual module under test
|
|
27
|
+
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
|
|
28
|
+
const mockWriteFileSync = writeFileSync as jest.MockedFunction<
|
|
29
|
+
typeof writeFileSync
|
|
30
|
+
>;
|
|
31
|
+
const mockExistsSync = existsSync as jest.MockedFunction<typeof existsSync>;
|
|
32
|
+
const mockDebug = Debug as jest.MockedFunction<typeof Debug>;
|
|
33
|
+
const mockSecretsmanager = secretsmanager as jest.MockedFunction<
|
|
34
|
+
typeof secretsmanager
|
|
35
|
+
>;
|
|
36
|
+
const mockObjectToExport = objectToExport as jest.MockedFunction<
|
|
37
|
+
typeof objectToExport
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
describe('index.ts CLI functionality', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.clearAllMocks();
|
|
43
|
+
|
|
44
|
+
// Reset process.env
|
|
45
|
+
process.env = { ...process.env };
|
|
46
|
+
|
|
47
|
+
// Setup mock debug
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
const mockDebugInstance = jest.fn() as any;
|
|
50
|
+
mockDebug.mockReturnValue(mockDebugInstance);
|
|
15
51
|
});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
52
|
+
|
|
53
|
+
describe('AWS command action logic', () => {
|
|
54
|
+
it('should call secretsmanager with correct options', async () => {
|
|
55
|
+
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
56
|
+
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
57
|
+
|
|
58
|
+
const options = {
|
|
59
|
+
secret: 'my-secret',
|
|
60
|
+
profile: 'my-profile',
|
|
61
|
+
region: 'us-east-1'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Simulate the action logic
|
|
65
|
+
let env = await mockSecretsmanager(options);
|
|
66
|
+
env = Object.assign({}, process.env, env);
|
|
67
|
+
|
|
68
|
+
expect(mockSecretsmanager).toHaveBeenCalledWith(options);
|
|
69
|
+
expect(env).toEqual(
|
|
70
|
+
expect.objectContaining({
|
|
71
|
+
SECRET_KEY: 'secret_value'
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should merge secrets with process.env', async () => {
|
|
77
|
+
const mockSecrets = { SECRET_KEY: 'secret_value' };
|
|
78
|
+
const originalEnv = { ORIGINAL_KEY: 'original_value' };
|
|
79
|
+
process.env = { ...originalEnv };
|
|
80
|
+
|
|
81
|
+
mockSecretsmanager.mockResolvedValue(mockSecrets);
|
|
82
|
+
|
|
83
|
+
// Simulate the action logic
|
|
84
|
+
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
85
|
+
env = Object.assign({}, process.env, env);
|
|
86
|
+
|
|
87
|
+
expect(env).toEqual(
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
ORIGINAL_KEY: 'original_value',
|
|
90
|
+
SECRET_KEY: 'secret_value'
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should spawn a program when provided', async () => {
|
|
96
|
+
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
97
|
+
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
98
|
+
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
const mockChildProcess = {
|
|
101
|
+
stdio: 'inherit'
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
} as any;
|
|
104
|
+
mockSpawn.mockReturnValue(mockChildProcess);
|
|
105
|
+
|
|
106
|
+
const program = ['node', 'script.js', 'arg1', 'arg2'];
|
|
107
|
+
const options = { secret: 'my-secret' };
|
|
108
|
+
|
|
109
|
+
// Simulate the action logic
|
|
110
|
+
let env = await mockSecretsmanager(options);
|
|
111
|
+
env = Object.assign({}, process.env, env);
|
|
112
|
+
|
|
113
|
+
if (program && program.length > 0) {
|
|
114
|
+
mockSpawn(program[0], program.slice(1), {
|
|
115
|
+
stdio: 'inherit',
|
|
116
|
+
shell: true,
|
|
117
|
+
env
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
122
|
+
'node',
|
|
123
|
+
['script.js', 'arg1', 'arg2'],
|
|
124
|
+
{
|
|
125
|
+
stdio: 'inherit',
|
|
126
|
+
shell: true,
|
|
127
|
+
env: expect.objectContaining({
|
|
128
|
+
SECRET_KEY: 'secret_value'
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should not spawn a program when no program is provided', async () => {
|
|
135
|
+
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
136
|
+
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
137
|
+
|
|
138
|
+
// Simulate the action logic
|
|
139
|
+
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
140
|
+
env = Object.assign({}, process.env, env);
|
|
141
|
+
|
|
142
|
+
const program: string[] = [];
|
|
143
|
+
if (program && program.length > 0) {
|
|
144
|
+
mockSpawn(program[0], program.slice(1), {
|
|
145
|
+
stdio: 'inherit',
|
|
146
|
+
shell: true,
|
|
147
|
+
env
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
expect(mockSpawn).not.toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should handle empty program array', async () => {
|
|
155
|
+
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
156
|
+
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
157
|
+
|
|
158
|
+
// Simulate the action logic
|
|
159
|
+
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
160
|
+
env = Object.assign({}, process.env, env);
|
|
161
|
+
|
|
162
|
+
const program: string[] = [];
|
|
163
|
+
if (program && program.length > 0) {
|
|
164
|
+
mockSpawn(program[0], program.slice(1), {
|
|
165
|
+
stdio: 'inherit',
|
|
166
|
+
shell: true,
|
|
167
|
+
env
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
expect(mockSpawn).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should handle single program argument', async () => {
|
|
175
|
+
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
176
|
+
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
177
|
+
|
|
178
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
179
|
+
const mockChildProcess = {
|
|
180
|
+
stdio: 'inherit'
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
182
|
+
} as any;
|
|
183
|
+
mockSpawn.mockReturnValue(mockChildProcess);
|
|
184
|
+
|
|
185
|
+
const program = ['echo'];
|
|
186
|
+
const options = { secret: 'my-secret' };
|
|
187
|
+
|
|
188
|
+
// Simulate the action logic
|
|
189
|
+
let env = await mockSecretsmanager(options);
|
|
190
|
+
env = Object.assign({}, process.env, env);
|
|
191
|
+
|
|
192
|
+
if (program && program.length > 0) {
|
|
193
|
+
mockSpawn(program[0], program.slice(1), {
|
|
194
|
+
stdio: 'inherit',
|
|
195
|
+
shell: true,
|
|
196
|
+
env
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
expect(mockSpawn).toHaveBeenCalledWith('echo', [], {
|
|
201
|
+
stdio: 'inherit',
|
|
202
|
+
shell: true,
|
|
203
|
+
env: expect.objectContaining({
|
|
204
|
+
SECRET_KEY: 'secret_value'
|
|
205
|
+
})
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should preserve existing environment variables', async () => {
|
|
210
|
+
const mockSecrets = { SECRET_KEY: 'secret_value' };
|
|
211
|
+
const originalEnv = {
|
|
212
|
+
PATH: '/usr/bin',
|
|
213
|
+
HOME: '/home/user',
|
|
214
|
+
NODE_ENV: 'test'
|
|
215
|
+
};
|
|
216
|
+
process.env = { ...originalEnv };
|
|
217
|
+
|
|
218
|
+
mockSecretsmanager.mockResolvedValue(mockSecrets);
|
|
219
|
+
|
|
220
|
+
// Simulate the action logic
|
|
221
|
+
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
222
|
+
env = Object.assign({}, process.env, env);
|
|
223
|
+
|
|
224
|
+
const program = ['echo'];
|
|
225
|
+
if (program && program.length > 0) {
|
|
226
|
+
mockSpawn(program[0], program.slice(1), {
|
|
227
|
+
stdio: 'inherit',
|
|
228
|
+
shell: true,
|
|
229
|
+
env
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
expect(mockSpawn).toHaveBeenCalledWith('echo', [], {
|
|
234
|
+
stdio: 'inherit',
|
|
235
|
+
shell: true,
|
|
236
|
+
env: expect.objectContaining({
|
|
237
|
+
PATH: '/usr/bin',
|
|
238
|
+
HOME: '/home/user',
|
|
239
|
+
NODE_ENV: 'test',
|
|
240
|
+
SECRET_KEY: 'secret_value'
|
|
241
|
+
})
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should handle secretsmanager returning empty object', async () => {
|
|
246
|
+
mockSecretsmanager.mockResolvedValue({});
|
|
247
|
+
|
|
248
|
+
// Simulate the action logic
|
|
249
|
+
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
250
|
+
env = Object.assign({}, process.env, env);
|
|
251
|
+
|
|
252
|
+
const program = ['echo'];
|
|
253
|
+
if (program && program.length > 0) {
|
|
254
|
+
mockSpawn(program[0], program.slice(1), {
|
|
255
|
+
stdio: 'inherit',
|
|
256
|
+
shell: true,
|
|
257
|
+
env
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
expect(mockSpawn).toHaveBeenCalledWith('echo', [], {
|
|
262
|
+
stdio: 'inherit',
|
|
263
|
+
shell: true,
|
|
264
|
+
env: expect.objectContaining({})
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should handle secretsmanager throwing an error', async () => {
|
|
269
|
+
const error = new Error('AWS connection failed');
|
|
270
|
+
mockSecretsmanager.mockRejectedValue(error);
|
|
271
|
+
|
|
272
|
+
// Should throw - the error should propagate
|
|
273
|
+
await expect(mockSecretsmanager({ secret: 'my-secret' })).rejects.toThrow(
|
|
274
|
+
'AWS connection failed'
|
|
275
|
+
);
|
|
276
|
+
});
|
|
19
277
|
});
|
|
20
|
-
});
|
|
21
278
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
279
|
+
describe('Debug logging', () => {
|
|
280
|
+
it('should create debug instance with correct namespace', () => {
|
|
281
|
+
mockDebug('env-secrets');
|
|
282
|
+
expect(mockDebug).toHaveBeenCalledWith('env-secrets');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should log environment variables when debug is enabled', async () => {
|
|
286
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
287
|
+
const mockDebugInstance = jest.fn() as any;
|
|
288
|
+
mockDebug.mockReturnValue(mockDebugInstance);
|
|
289
|
+
|
|
290
|
+
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
291
|
+
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
292
|
+
|
|
293
|
+
// Simulate the action logic
|
|
294
|
+
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
295
|
+
env = Object.assign({}, process.env, env);
|
|
296
|
+
|
|
297
|
+
// Simulate debug logging
|
|
298
|
+
mockDebugInstance(env);
|
|
299
|
+
|
|
300
|
+
expect(mockDebugInstance).toHaveBeenCalledWith(
|
|
301
|
+
expect.objectContaining({
|
|
302
|
+
SECRET_KEY: 'secret_value'
|
|
303
|
+
})
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should log program execution when program is provided', async () => {
|
|
308
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
309
|
+
const mockDebugInstance = jest.fn() as any;
|
|
310
|
+
mockDebug.mockReturnValue(mockDebugInstance);
|
|
311
|
+
|
|
312
|
+
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
313
|
+
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
314
|
+
|
|
315
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
316
|
+
const mockChildProcess = {
|
|
317
|
+
stdio: 'inherit'
|
|
318
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
319
|
+
} as any;
|
|
320
|
+
mockSpawn.mockReturnValue(mockChildProcess);
|
|
321
|
+
|
|
322
|
+
const program = ['node', 'script.js', 'arg1'];
|
|
323
|
+
|
|
324
|
+
// Simulate the action logic
|
|
325
|
+
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
326
|
+
env = Object.assign({}, process.env, env);
|
|
327
|
+
|
|
328
|
+
if (program && program.length > 0) {
|
|
329
|
+
// Simulate debug logging
|
|
330
|
+
mockDebugInstance(`${program[0]} ${program.slice(1).join(' ')}`);
|
|
331
|
+
|
|
332
|
+
mockSpawn(program[0], program.slice(1), {
|
|
333
|
+
stdio: 'inherit',
|
|
334
|
+
shell: true,
|
|
335
|
+
env
|
|
33
336
|
});
|
|
34
337
|
}
|
|
35
|
-
|
|
338
|
+
|
|
339
|
+
expect(mockDebugInstance).toHaveBeenCalledWith('node script.js arg1');
|
|
340
|
+
});
|
|
36
341
|
});
|
|
37
|
-
|
|
342
|
+
|
|
343
|
+
describe('File output functionality', () => {
|
|
344
|
+
beforeEach(() => {
|
|
345
|
+
// Reset console methods
|
|
346
|
+
jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
347
|
+
jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
348
|
+
jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
349
|
+
throw new Error('process.exit called');
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
afterEach(() => {
|
|
354
|
+
jest.restoreAllMocks();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should write secrets to file when output option is provided', async () => {
|
|
358
|
+
const mockSecrets = { SECRET_KEY: 'secret_value', API_KEY: 'api_value' };
|
|
359
|
+
const mockEnvContent =
|
|
360
|
+
'export SECRET_KEY=secret_value\nexport API_KEY=api_value\n';
|
|
361
|
+
|
|
362
|
+
mockSecretsmanager.mockResolvedValue(mockSecrets);
|
|
363
|
+
mockObjectToExport.mockReturnValue(mockEnvContent);
|
|
364
|
+
mockExistsSync.mockReturnValue(false);
|
|
365
|
+
|
|
366
|
+
const options: { secret: string; output: string } = {
|
|
367
|
+
secret: 'my-secret',
|
|
368
|
+
output: '/tmp/secrets.env'
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Simulate the action logic
|
|
372
|
+
const secrets = await mockSecretsmanager(options);
|
|
373
|
+
|
|
374
|
+
if (options.output) {
|
|
375
|
+
if (mockExistsSync(options.output)) {
|
|
376
|
+
// eslint-disable-next-line no-console
|
|
377
|
+
console.error(
|
|
378
|
+
`Error: File ${options.output} already exists and will not be overwritten`
|
|
379
|
+
);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const envContent = mockObjectToExport(secrets);
|
|
384
|
+
mockWriteFileSync(options.output, envContent, { mode: 0o400 });
|
|
385
|
+
// eslint-disable-next-line no-console
|
|
386
|
+
console.log(`Secrets written to ${options.output}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
expect(mockSecretsmanager).toHaveBeenCalledWith(options);
|
|
390
|
+
expect(mockObjectToExport).toHaveBeenCalledWith(mockSecrets);
|
|
391
|
+
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
|
392
|
+
'/tmp/secrets.env',
|
|
393
|
+
mockEnvContent,
|
|
394
|
+
{ mode: 0o400 }
|
|
395
|
+
);
|
|
396
|
+
// eslint-disable-next-line no-console
|
|
397
|
+
expect(console.log).toHaveBeenCalledWith(
|
|
398
|
+
'Secrets written to /tmp/secrets.env'
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should not overwrite existing file', async () => {
|
|
403
|
+
const mockSecrets = { SECRET_KEY: 'secret_value' };
|
|
404
|
+
|
|
405
|
+
mockSecretsmanager.mockResolvedValue(mockSecrets);
|
|
406
|
+
mockExistsSync.mockReturnValue(true);
|
|
407
|
+
|
|
408
|
+
const options: { secret: string; output: string } = {
|
|
409
|
+
secret: 'my-secret',
|
|
410
|
+
output: '/tmp/existing.env'
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// Simulate the action logic
|
|
414
|
+
await mockSecretsmanager(options);
|
|
415
|
+
|
|
416
|
+
// Test the file existence check and error handling
|
|
417
|
+
if (options.output) {
|
|
418
|
+
if (mockExistsSync(options.output)) {
|
|
419
|
+
// eslint-disable-next-line no-console
|
|
420
|
+
console.error(
|
|
421
|
+
`Error: File ${options.output} already exists and will not be overwritten`
|
|
422
|
+
);
|
|
423
|
+
// In the actual implementation, this would call process.exit(1)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
expect(mockExistsSync).toHaveBeenCalledWith('/tmp/existing.env');
|
|
428
|
+
// eslint-disable-next-line no-console
|
|
429
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
430
|
+
'Error: File /tmp/existing.env already exists and will not be overwritten'
|
|
431
|
+
);
|
|
432
|
+
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should use original behavior when no output option is provided', async () => {
|
|
436
|
+
const mockSecrets = { SECRET_KEY: 'secret_value' };
|
|
437
|
+
mockSecretsmanager.mockResolvedValue(mockSecrets);
|
|
438
|
+
|
|
439
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
440
|
+
const mockChildProcess = {
|
|
441
|
+
stdio: 'inherit'
|
|
442
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
443
|
+
} as any;
|
|
444
|
+
mockSpawn.mockReturnValue(mockChildProcess);
|
|
445
|
+
|
|
446
|
+
const options: { secret: string; output?: string } = {
|
|
447
|
+
secret: 'my-secret'
|
|
448
|
+
// No output option
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const program = ['echo', 'hello'];
|
|
452
|
+
|
|
453
|
+
// Simulate the action logic
|
|
454
|
+
const secrets = await mockSecretsmanager(options);
|
|
455
|
+
|
|
456
|
+
if (options.output) {
|
|
457
|
+
// This branch should not be taken
|
|
458
|
+
if (mockExistsSync(options.output)) {
|
|
459
|
+
// eslint-disable-next-line no-console
|
|
460
|
+
console.error(
|
|
461
|
+
`Error: File ${options.output} already exists and will not be overwritten`
|
|
462
|
+
);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const envContent = mockObjectToExport(secrets);
|
|
467
|
+
mockWriteFileSync(options.output, envContent, { mode: 0o400 });
|
|
468
|
+
// eslint-disable-next-line no-console
|
|
469
|
+
console.log(`Secrets written to ${options.output}`);
|
|
470
|
+
} else {
|
|
471
|
+
// Original behavior: merge secrets into environment and run program
|
|
472
|
+
const env = Object.assign({}, process.env, secrets);
|
|
473
|
+
|
|
474
|
+
if (program && program.length > 0) {
|
|
475
|
+
mockSpawn(program[0], program.slice(1), {
|
|
476
|
+
stdio: 'inherit',
|
|
477
|
+
shell: true,
|
|
478
|
+
env
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
expect(mockSecretsmanager).toHaveBeenCalledWith(options);
|
|
484
|
+
expect(mockSpawn).toHaveBeenCalledWith('echo', ['hello'], {
|
|
485
|
+
stdio: 'inherit',
|
|
486
|
+
shell: true,
|
|
487
|
+
env: expect.objectContaining({
|
|
488
|
+
SECRET_KEY: 'secret_value'
|
|
489
|
+
})
|
|
490
|
+
});
|
|
491
|
+
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('should handle empty secrets object in file output', async () => {
|
|
495
|
+
const mockSecrets = {};
|
|
496
|
+
const mockEnvContent = '';
|
|
497
|
+
|
|
498
|
+
mockSecretsmanager.mockResolvedValue(mockSecrets);
|
|
499
|
+
mockObjectToExport.mockReturnValue(mockEnvContent);
|
|
500
|
+
mockExistsSync.mockReturnValue(false);
|
|
501
|
+
|
|
502
|
+
const options: { secret: string; output: string } = {
|
|
503
|
+
secret: 'my-secret',
|
|
504
|
+
output: '/tmp/empty.env'
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Simulate the action logic
|
|
508
|
+
const secrets = await mockSecretsmanager(options);
|
|
509
|
+
|
|
510
|
+
if (options.output) {
|
|
511
|
+
if (mockExistsSync(options.output)) {
|
|
512
|
+
// eslint-disable-next-line no-console
|
|
513
|
+
console.error(
|
|
514
|
+
`Error: File ${options.output} already exists and will not be overwritten`
|
|
515
|
+
);
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const envContent = mockObjectToExport(secrets);
|
|
520
|
+
mockWriteFileSync(options.output, envContent, { mode: 0o400 });
|
|
521
|
+
// eslint-disable-next-line no-console
|
|
522
|
+
console.log(`Secrets written to ${options.output}`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
expect(mockObjectToExport).toHaveBeenCalledWith({});
|
|
526
|
+
expect(mockWriteFileSync).toHaveBeenCalledWith('/tmp/empty.env', '', {
|
|
527
|
+
mode: 0o400
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should not run program when output option is provided', async () => {
|
|
532
|
+
const mockSecrets = { SECRET_KEY: 'secret_value' };
|
|
533
|
+
const mockEnvContent = 'export SECRET_KEY=secret_value\n';
|
|
534
|
+
|
|
535
|
+
mockSecretsmanager.mockResolvedValue(mockSecrets);
|
|
536
|
+
mockObjectToExport.mockReturnValue(mockEnvContent);
|
|
537
|
+
mockExistsSync.mockReturnValue(false);
|
|
538
|
+
|
|
539
|
+
const options: { secret: string; output: string } = {
|
|
540
|
+
secret: 'my-secret',
|
|
541
|
+
output: '/tmp/secrets.env'
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const program = ['echo', 'hello'];
|
|
545
|
+
|
|
546
|
+
// Simulate the action logic
|
|
547
|
+
const secrets = await mockSecretsmanager(options);
|
|
548
|
+
|
|
549
|
+
if (options.output) {
|
|
550
|
+
if (mockExistsSync(options.output)) {
|
|
551
|
+
// eslint-disable-next-line no-console
|
|
552
|
+
console.error(
|
|
553
|
+
`Error: File ${options.output} already exists and will not be overwritten`
|
|
554
|
+
);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const envContent = mockObjectToExport(secrets);
|
|
559
|
+
mockWriteFileSync(options.output, envContent, { mode: 0o400 });
|
|
560
|
+
// eslint-disable-next-line no-console
|
|
561
|
+
console.log(`Secrets written to ${options.output}`);
|
|
562
|
+
} else {
|
|
563
|
+
// This branch should not be taken
|
|
564
|
+
const env = Object.assign({}, process.env, secrets);
|
|
565
|
+
|
|
566
|
+
if (program && program.length > 0) {
|
|
567
|
+
mockSpawn(program[0], program.slice(1), {
|
|
568
|
+
stdio: 'inherit',
|
|
569
|
+
shell: true,
|
|
570
|
+
env
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
expect(mockWriteFileSync).toHaveBeenCalled();
|
|
576
|
+
expect(mockSpawn).not.toHaveBeenCalled();
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
});
|