env-secrets 0.3.3 → 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.
- package/AGENTS.md +9 -3
- package/README.md +13 -8
- package/__e2e__/README.md +2 -5
- package/__e2e__/index.test.ts +152 -1
- package/__e2e__/utils/test-utils.ts +61 -1
- package/__tests__/cli/helpers.test.ts +129 -0
- package/__tests__/vaults/aws-config.test.ts +85 -0
- package/__tests__/vaults/secretsmanager-admin.test.ts +312 -0
- package/dist/cli/helpers.js +110 -0
- package/dist/index.js +221 -2
- package/dist/vaults/aws-config.js +29 -0
- package/dist/vaults/secretsmanager-admin.js +240 -0
- package/dist/vaults/secretsmanager.js +8 -16
- package/docs/AWS.md +77 -2
- package/jest.e2e.config.js +1 -0
- package/package.json +2 -2
- package/src/cli/helpers.ts +144 -0
- package/src/index.ts +287 -2
- package/src/vaults/aws-config.ts +51 -0
- package/src/vaults/secretsmanager-admin.ts +352 -0
- package/src/vaults/secretsmanager.ts +8 -21
- package/website/docs/cli-reference.mdx +67 -0
- package/website/docs/providers/aws-secrets-manager.mdx +32 -0
package/AGENTS.md
CHANGED
|
@@ -51,7 +51,7 @@ Always run quaity checks after creating or modifing files
|
|
|
51
51
|
### Testing Strategy
|
|
52
52
|
|
|
53
53
|
Always run unit tests after creating or modifying files.
|
|
54
|
-
Always run end to end tests before pushing code to a remote git repository.
|
|
54
|
+
Always start Docker Compose LocalStack and run end to end tests before pushing code to a remote git repository.
|
|
55
55
|
|
|
56
56
|
- **Unit Tests**: Jest framework, located in `__tests__/`
|
|
57
57
|
- **E2E Tests**: Located in `__e2e__/`
|
|
@@ -60,6 +60,7 @@ Always run end to end tests before pushing code to a remote git repository.
|
|
|
60
60
|
- `yarn test` - runs all tests
|
|
61
61
|
- `yarn test:unit` - runs unit tests only
|
|
62
62
|
- `yarn test:e2e` - builds and runs e2e tests
|
|
63
|
+
- `docker compose up -d localstack` - start LocalStack for e2e tests
|
|
63
64
|
|
|
64
65
|
## Project Structure
|
|
65
66
|
|
|
@@ -120,8 +121,9 @@ yarn test:unit:coverage # Run tests with coverage
|
|
|
120
121
|
|
|
121
122
|
1. Run `yarn prettier:fix && yarn lint` to ensure code quality
|
|
122
123
|
2. Run `yarn test` to ensure all tests pass
|
|
123
|
-
3.
|
|
124
|
-
4. Update
|
|
124
|
+
3. Run `docker compose up -d localstack` and then `yarn test:e2e` before pushing
|
|
125
|
+
4. Update tests for new features or bug fixes
|
|
126
|
+
5. Update documentation if needed
|
|
125
127
|
|
|
126
128
|
### Pull Request Process
|
|
127
129
|
|
|
@@ -130,6 +132,7 @@ yarn test:unit:coverage # Run tests with coverage
|
|
|
130
132
|
3. Add tests for new functionality
|
|
131
133
|
4. Ensure all CI checks pass
|
|
132
134
|
5. Submit a pull request with a clear description
|
|
135
|
+
6. Always request a GitHub Copilot review on every new pull request
|
|
133
136
|
|
|
134
137
|
## Development Environment
|
|
135
138
|
|
|
@@ -138,6 +141,8 @@ yarn test:unit:coverage # Run tests with coverage
|
|
|
138
141
|
- Node.js 20.0.0 or higher (see .nvmrc)
|
|
139
142
|
- Yarn package manager
|
|
140
143
|
- AWS CLI (for testing AWS integration)
|
|
144
|
+
- Homebrew (macOS/Linux) with `awscli-local` installed:
|
|
145
|
+
- `brew install awscli-local`
|
|
141
146
|
|
|
142
147
|
### Setup
|
|
143
148
|
|
|
@@ -145,5 +150,6 @@ yarn test:unit:coverage # Run tests with coverage
|
|
|
145
150
|
git clone https://github.com/markcallen/env-secrets.git
|
|
146
151
|
cd env-secrets
|
|
147
152
|
yarn install
|
|
153
|
+
brew install awscli-local
|
|
148
154
|
yarn build
|
|
149
155
|
```
|
package/README.md
CHANGED
|
@@ -103,6 +103,14 @@ env-secrets aws -s my-app-secrets -r us-east-1 -- node app.js
|
|
|
103
103
|
- `-o, --output <file>` (optional): Output secrets to a file instead of injecting into environment variables. File will be created with 0400 permissions and will not overwrite existing files
|
|
104
104
|
- `-- <program-to-run>`: The program to run with the injected environment variables (only used when `-o` is not specified)
|
|
105
105
|
|
|
106
|
+
For `aws secret` management subcommands (`create`, `update`, `list`, `get`, `delete`), use:
|
|
107
|
+
|
|
108
|
+
- `-r, --region <region>` to target a specific region
|
|
109
|
+
- `-p, --profile <profile>` to select credentials profile
|
|
110
|
+
- `--output <format>` for `json` or `table`
|
|
111
|
+
|
|
112
|
+
These options are honored consistently on `aws secret` subcommands.
|
|
113
|
+
|
|
106
114
|
#### Examples
|
|
107
115
|
|
|
108
116
|
1. **Create a secret using AWS CLI:**
|
|
@@ -459,11 +467,8 @@ The end-to-end tests use LocalStack to emulate AWS Secrets Manager and test the
|
|
|
459
467
|
1. **Install awslocal** (required for e2e tests):
|
|
460
468
|
|
|
461
469
|
```bash
|
|
462
|
-
#
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
# Or using npm
|
|
466
|
-
npm install -g awscli-local
|
|
470
|
+
# macOS/Linux (recommended)
|
|
471
|
+
brew install awscli-local
|
|
467
472
|
```
|
|
468
473
|
|
|
469
474
|
2. **Start LocalStack**:
|
|
@@ -508,15 +513,15 @@ The end-to-end test suite includes:
|
|
|
508
513
|
- **Program Execution**: Tests for executing programs with injected environment variables
|
|
509
514
|
- **Error Handling**: Tests for various error scenarios and edge cases
|
|
510
515
|
- **AWS Profile Support**: Tests for both default and custom AWS profiles
|
|
511
|
-
- **Region Support**: Tests for different AWS regions
|
|
516
|
+
- **Region Support**: Tests for different AWS regions, including multi-region `aws secret list` isolation checks
|
|
512
517
|
|
|
513
518
|
#### Troubleshooting E2E Tests
|
|
514
519
|
|
|
515
520
|
**awslocal not found**:
|
|
516
521
|
|
|
517
522
|
```bash
|
|
518
|
-
# Install awslocal
|
|
519
|
-
|
|
523
|
+
# Install awslocal (macOS/Linux)
|
|
524
|
+
brew install awscli-local
|
|
520
525
|
|
|
521
526
|
# Verify installation
|
|
522
527
|
awslocal --version
|
package/__e2e__/README.md
CHANGED
|
@@ -33,11 +33,8 @@ localstack start
|
|
|
33
33
|
The tests require `awslocal` to be installed, which is a wrapper around AWS CLI that automatically points to LocalStack:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
# Install awslocal
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# Or using npm
|
|
40
|
-
npm install -g awscli-local
|
|
36
|
+
# Install awslocal (macOS/Linux recommended)
|
|
37
|
+
brew install awscli-local
|
|
41
38
|
|
|
42
39
|
# Verify installation
|
|
43
40
|
awslocal --version
|
package/__e2e__/index.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
LocalStackHelper,
|
|
3
3
|
cli,
|
|
4
4
|
cliWithEnv,
|
|
5
|
+
cliWithEnvAndStdin,
|
|
5
6
|
createTempFile,
|
|
6
7
|
cleanupTempFile,
|
|
7
8
|
createTestProfile,
|
|
@@ -332,7 +333,157 @@ describe('End-to-End Tests', () => {
|
|
|
332
333
|
const result = await cliWithEnv(['aws'], getLocalStackEnv());
|
|
333
334
|
|
|
334
335
|
expect(result.code).toBe(1);
|
|
335
|
-
expect(result.stderr).toContain('required option');
|
|
336
|
+
expect(result.stderr).toContain('Missing required option --secret');
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('AWS Secret Management Commands', () => {
|
|
341
|
+
test('should create, list, get, and delete a secret', async () => {
|
|
342
|
+
const secretName = `e2e-managed-secret-${Date.now()}`;
|
|
343
|
+
|
|
344
|
+
const createResult = await cliWithEnv(
|
|
345
|
+
['aws', 'secret', 'create', '-n', secretName, '-v', 'initial-value'],
|
|
346
|
+
getLocalStackEnv()
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
expect(createResult.code).toBe(0);
|
|
350
|
+
expect(createResult.stdout).toContain(secretName);
|
|
351
|
+
|
|
352
|
+
const listResult = await cliWithEnv(
|
|
353
|
+
['aws', 'secret', 'list', '--prefix', 'e2e-managed-secret-'],
|
|
354
|
+
getLocalStackEnv()
|
|
355
|
+
);
|
|
356
|
+
expect(listResult.code).toBe(0);
|
|
357
|
+
expect(listResult.stdout).toContain(secretName);
|
|
358
|
+
|
|
359
|
+
const getResult = await cliWithEnv(
|
|
360
|
+
['aws', 'secret', 'get', '-n', secretName],
|
|
361
|
+
getLocalStackEnv()
|
|
362
|
+
);
|
|
363
|
+
expect(getResult.code).toBe(0);
|
|
364
|
+
expect(getResult.stdout).toContain(secretName);
|
|
365
|
+
expect(getResult.stdout).not.toContain('initial-value');
|
|
366
|
+
|
|
367
|
+
const deleteResult = await cliWithEnv(
|
|
368
|
+
[
|
|
369
|
+
'aws',
|
|
370
|
+
'secret',
|
|
371
|
+
'delete',
|
|
372
|
+
'-n',
|
|
373
|
+
secretName,
|
|
374
|
+
'--force-delete-without-recovery',
|
|
375
|
+
'--yes'
|
|
376
|
+
],
|
|
377
|
+
getLocalStackEnv()
|
|
378
|
+
);
|
|
379
|
+
expect(deleteResult.code).toBe(0);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('should update secret value from stdin', async () => {
|
|
383
|
+
const secret = await createTestSecret({
|
|
384
|
+
name: `managed-secret-stdin-${Date.now()}`,
|
|
385
|
+
value: 'initial-value',
|
|
386
|
+
description: 'Secret for stdin update test'
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const updateResult = await cliWithEnvAndStdin(
|
|
390
|
+
[
|
|
391
|
+
'aws',
|
|
392
|
+
'secret',
|
|
393
|
+
'update',
|
|
394
|
+
'-n',
|
|
395
|
+
secret.prefixedName,
|
|
396
|
+
'--value-stdin'
|
|
397
|
+
],
|
|
398
|
+
getLocalStackEnv(),
|
|
399
|
+
'stdin-updated-value'
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
expect(updateResult.code).toBe(0);
|
|
403
|
+
expect(updateResult.stderr).toBe('');
|
|
404
|
+
|
|
405
|
+
const deleteResult = await cliWithEnv(
|
|
406
|
+
[
|
|
407
|
+
'aws',
|
|
408
|
+
'secret',
|
|
409
|
+
'delete',
|
|
410
|
+
'-n',
|
|
411
|
+
secret.prefixedName,
|
|
412
|
+
'--force-delete-without-recovery',
|
|
413
|
+
'--yes'
|
|
414
|
+
],
|
|
415
|
+
getLocalStackEnv()
|
|
416
|
+
);
|
|
417
|
+
expect(deleteResult.code).toBe(0);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('should require confirmation for delete', async () => {
|
|
421
|
+
const secret = await createTestSecret({
|
|
422
|
+
name: `managed-secret-confirm-${Date.now()}`,
|
|
423
|
+
value: 'value',
|
|
424
|
+
description: 'Secret for delete confirmation test'
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const result = await cliWithEnv(
|
|
428
|
+
['aws', 'secret', 'delete', '-n', secret.prefixedName],
|
|
429
|
+
getLocalStackEnv()
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
expect(result.code).toBe(1);
|
|
433
|
+
expect(result.stderr).toContain('requires --yes confirmation');
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test('should honor region flag for secret list across multiple regions', async () => {
|
|
437
|
+
const secret = await createTestSecret(
|
|
438
|
+
{
|
|
439
|
+
name: `managed-secret-multi-region-${Date.now()}`,
|
|
440
|
+
value: '{"region":"us-west-2"}',
|
|
441
|
+
description: 'Secret used for region isolation test'
|
|
442
|
+
},
|
|
443
|
+
'us-west-2'
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const westResult = await cliWithEnv(
|
|
447
|
+
[
|
|
448
|
+
'aws',
|
|
449
|
+
'secret',
|
|
450
|
+
'list',
|
|
451
|
+
'--prefix',
|
|
452
|
+
secret.prefixedName,
|
|
453
|
+
'-r',
|
|
454
|
+
'us-west-2',
|
|
455
|
+
'--output',
|
|
456
|
+
'json'
|
|
457
|
+
],
|
|
458
|
+
getLocalStackEnv()
|
|
459
|
+
);
|
|
460
|
+
expect(westResult.code).toBe(0);
|
|
461
|
+
const westRows = JSON.parse(westResult.stdout) as Array<{
|
|
462
|
+
name: string;
|
|
463
|
+
}>;
|
|
464
|
+
expect(westRows.some((row) => row.name === secret.prefixedName)).toBe(
|
|
465
|
+
true
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
const eastResult = await cliWithEnv(
|
|
469
|
+
[
|
|
470
|
+
'aws',
|
|
471
|
+
'secret',
|
|
472
|
+
'list',
|
|
473
|
+
'--prefix',
|
|
474
|
+
secret.prefixedName,
|
|
475
|
+
'-r',
|
|
476
|
+
'us-east-1',
|
|
477
|
+
'--output',
|
|
478
|
+
'json'
|
|
479
|
+
],
|
|
480
|
+
getLocalStackEnv()
|
|
481
|
+
);
|
|
482
|
+
expect(eastResult.code).toBe(0);
|
|
483
|
+
const eastRows = JSON.parse(eastResult.stdout) as Array<{
|
|
484
|
+
name: string;
|
|
485
|
+
}>;
|
|
486
|
+
expect(eastRows).toEqual([]);
|
|
336
487
|
});
|
|
337
488
|
});
|
|
338
489
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { exec } from 'child_process';
|
|
1
|
+
import { exec, spawn } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as fs from 'fs';
|
|
@@ -506,6 +506,66 @@ export async function cliWithEnv(
|
|
|
506
506
|
});
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
+
export async function cliWithEnvAndStdin(
|
|
510
|
+
args: string[],
|
|
511
|
+
env: Record<string, string>,
|
|
512
|
+
stdin: string,
|
|
513
|
+
cwd = '.'
|
|
514
|
+
): Promise<CliResult> {
|
|
515
|
+
return await new Promise((resolve) => {
|
|
516
|
+
const cleanEnv = { ...process.env };
|
|
517
|
+
delete cleanEnv.AWS_PROFILE;
|
|
518
|
+
delete cleanEnv.AWS_DEFAULT_PROFILE;
|
|
519
|
+
delete cleanEnv.AWS_SESSION_TOKEN;
|
|
520
|
+
delete cleanEnv.AWS_SECURITY_TOKEN;
|
|
521
|
+
delete cleanEnv.AWS_ROLE_ARN;
|
|
522
|
+
delete cleanEnv.AWS_ROLE_SESSION_NAME;
|
|
523
|
+
delete cleanEnv.AWS_WEB_IDENTITY_TOKEN_FILE;
|
|
524
|
+
delete cleanEnv.AWS_WEB_IDENTITY_TOKEN;
|
|
525
|
+
|
|
526
|
+
const defaultEnv = {
|
|
527
|
+
AWS_ENDPOINT_URL: process.env.LOCALSTACK_URL || 'http://localhost:4566',
|
|
528
|
+
AWS_ACCESS_KEY_ID: 'test',
|
|
529
|
+
AWS_SECRET_ACCESS_KEY: 'test',
|
|
530
|
+
AWS_DEFAULT_REGION: 'us-east-1',
|
|
531
|
+
AWS_REGION: 'us-east-1',
|
|
532
|
+
NODE_ENV: 'test'
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const envVars = { ...cleanEnv, ...defaultEnv, ...env };
|
|
536
|
+
const child = spawn('node', [path.resolve('./dist/index'), ...args], {
|
|
537
|
+
cwd,
|
|
538
|
+
env: envVars,
|
|
539
|
+
stdio: 'pipe'
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
let stdout = '';
|
|
543
|
+
let stderr = '';
|
|
544
|
+
let error: Error | null = null;
|
|
545
|
+
|
|
546
|
+
child.stdout.on('data', (chunk) => {
|
|
547
|
+
stdout += chunk.toString();
|
|
548
|
+
});
|
|
549
|
+
child.stderr.on('data', (chunk) => {
|
|
550
|
+
stderr += chunk.toString();
|
|
551
|
+
});
|
|
552
|
+
child.on('error', (err) => {
|
|
553
|
+
error = err;
|
|
554
|
+
});
|
|
555
|
+
child.on('close', (code) => {
|
|
556
|
+
resolve({
|
|
557
|
+
code: code ?? (error ? 1 : 0),
|
|
558
|
+
error,
|
|
559
|
+
stdout,
|
|
560
|
+
stderr
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
child.stdin.write(stdin);
|
|
565
|
+
child.stdin.end();
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
509
569
|
export function createTempFile(content: string) {
|
|
510
570
|
const tempDir = os.tmpdir();
|
|
511
571
|
const tempFile = path.join(tempDir, `env-secrets-test-${Date.now()}.env`);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { PassThrough } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
asOutputFormat,
|
|
5
|
+
parseRecoveryDays,
|
|
6
|
+
printData,
|
|
7
|
+
readStdin,
|
|
8
|
+
renderTable,
|
|
9
|
+
resolveAwsScope,
|
|
10
|
+
resolveSecretValue
|
|
11
|
+
} from '../../src/cli/helpers';
|
|
12
|
+
|
|
13
|
+
describe('cli/helpers', () => {
|
|
14
|
+
it('validates output format', () => {
|
|
15
|
+
expect(asOutputFormat('json')).toBe('json');
|
|
16
|
+
expect(asOutputFormat('table')).toBe('table');
|
|
17
|
+
expect(() => asOutputFormat('xml')).toThrow('Invalid output format');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders empty table message', () => {
|
|
21
|
+
const output = renderTable([{ key: 'name', label: 'Name' }], []);
|
|
22
|
+
expect(output).toBe('No results.');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('renders aligned table output', () => {
|
|
26
|
+
const output = renderTable(
|
|
27
|
+
[
|
|
28
|
+
{ key: 'name', label: 'Name' },
|
|
29
|
+
{ key: 'value', label: 'Value' }
|
|
30
|
+
],
|
|
31
|
+
[{ name: 'one', value: '1' }]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(output).toContain('Name');
|
|
35
|
+
expect(output).toContain('Value');
|
|
36
|
+
expect(output).toContain('one');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('prints JSON output', () => {
|
|
40
|
+
const consoleSpy = jest
|
|
41
|
+
.spyOn(console, 'log')
|
|
42
|
+
.mockImplementation(() => undefined);
|
|
43
|
+
|
|
44
|
+
printData('json', [{ key: 'name', label: 'Name' }], [{ name: 'value' }]);
|
|
45
|
+
|
|
46
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
47
|
+
JSON.stringify([{ name: 'value' }], null, 2)
|
|
48
|
+
);
|
|
49
|
+
consoleSpy.mockRestore();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('parses valid recovery days and rejects invalid values', () => {
|
|
53
|
+
expect(parseRecoveryDays('7')).toBe(7);
|
|
54
|
+
expect(parseRecoveryDays('30')).toBe(30);
|
|
55
|
+
expect(() => parseRecoveryDays('6')).toThrow();
|
|
56
|
+
expect(() => parseRecoveryDays('31')).toThrow();
|
|
57
|
+
expect(() => parseRecoveryDays('abc')).toThrow();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('reads stdin content and strips one trailing newline', async () => {
|
|
61
|
+
const stream = new PassThrough();
|
|
62
|
+
const promise = readStdin(stream as unknown as NodeJS.ReadStream);
|
|
63
|
+
|
|
64
|
+
stream.write('secret-value\n');
|
|
65
|
+
stream.end();
|
|
66
|
+
|
|
67
|
+
await expect(promise).resolves.toBe('secret-value');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('resolves secret value from explicit --value', async () => {
|
|
71
|
+
await expect(resolveSecretValue('inline', false)).resolves.toBe('inline');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('rejects when both --value and --value-stdin are used', async () => {
|
|
75
|
+
await expect(resolveSecretValue('inline', true)).rejects.toThrow(
|
|
76
|
+
'Use either --value or --value-stdin, not both.'
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('rejects stdin mode when no stdin is provided', async () => {
|
|
81
|
+
const originalStdin = process.stdin;
|
|
82
|
+
Object.defineProperty(process, 'stdin', {
|
|
83
|
+
value: { ...originalStdin, isTTY: true },
|
|
84
|
+
configurable: true
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await expect(resolveSecretValue(undefined, true)).rejects.toThrow(
|
|
88
|
+
'No stdin detected. Pipe a value when using --value-stdin.'
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
Object.defineProperty(process, 'stdin', {
|
|
92
|
+
value: originalStdin,
|
|
93
|
+
configurable: true
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('prefers explicit aws scope options over global options', () => {
|
|
98
|
+
const command = {
|
|
99
|
+
optsWithGlobals: () => ({
|
|
100
|
+
profile: 'global-profile',
|
|
101
|
+
region: 'us-west-2'
|
|
102
|
+
})
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
resolveAwsScope(
|
|
107
|
+
{ profile: 'local-profile', region: 'us-east-1' },
|
|
108
|
+
command
|
|
109
|
+
)
|
|
110
|
+
).toEqual({
|
|
111
|
+
profile: 'local-profile',
|
|
112
|
+
region: 'us-east-1'
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('falls back to global aws scope options when local options are absent', () => {
|
|
117
|
+
const command = {
|
|
118
|
+
optsWithGlobals: () => ({
|
|
119
|
+
profile: 'global-profile',
|
|
120
|
+
region: 'us-west-2'
|
|
121
|
+
})
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
expect(resolveAwsScope({}, command)).toEqual({
|
|
125
|
+
profile: 'global-profile',
|
|
126
|
+
region: 'us-west-2'
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { fromIni } from '@aws-sdk/credential-providers';
|
|
2
|
+
|
|
3
|
+
jest.mock('@aws-sdk/credential-providers');
|
|
4
|
+
|
|
5
|
+
import { buildAwsClientConfig } from '../../src/vaults/aws-config';
|
|
6
|
+
|
|
7
|
+
const mockFromIni = fromIni as jest.MockedFunction<typeof fromIni>;
|
|
8
|
+
|
|
9
|
+
describe('aws-config', () => {
|
|
10
|
+
let originalEnv: NodeJS.ProcessEnv;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
originalEnv = { ...process.env };
|
|
15
|
+
process.env = { ...originalEnv };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
process.env = originalEnv;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('uses explicit profile when provided', () => {
|
|
23
|
+
const mockCredentials = jest.fn();
|
|
24
|
+
mockFromIni.mockReturnValue(mockCredentials as ReturnType<typeof fromIni>);
|
|
25
|
+
|
|
26
|
+
const config = buildAwsClientConfig({
|
|
27
|
+
profile: 'my-profile',
|
|
28
|
+
region: 'us-east-1'
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(mockFromIni).toHaveBeenCalledWith({ profile: 'my-profile' });
|
|
32
|
+
expect(config).toEqual({
|
|
33
|
+
region: 'us-east-1',
|
|
34
|
+
credentials: mockCredentials
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('uses environment credentials when access key and secret are present', () => {
|
|
39
|
+
process.env.AWS_ACCESS_KEY_ID = 'test';
|
|
40
|
+
process.env.AWS_SECRET_ACCESS_KEY = 'test';
|
|
41
|
+
|
|
42
|
+
const config = buildAwsClientConfig({ region: 'us-east-1' });
|
|
43
|
+
|
|
44
|
+
expect(mockFromIni).not.toHaveBeenCalled();
|
|
45
|
+
expect(config).toEqual({
|
|
46
|
+
region: 'us-east-1',
|
|
47
|
+
credentials: undefined
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('falls back to default profile when no explicit credentials are provided', () => {
|
|
52
|
+
const mockCredentials = jest.fn();
|
|
53
|
+
mockFromIni.mockReturnValue(mockCredentials as ReturnType<typeof fromIni>);
|
|
54
|
+
|
|
55
|
+
const config = buildAwsClientConfig({ region: 'us-east-1' });
|
|
56
|
+
|
|
57
|
+
expect(mockFromIni).toHaveBeenCalledWith({ profile: 'default' });
|
|
58
|
+
expect(config).toEqual({
|
|
59
|
+
region: 'us-east-1',
|
|
60
|
+
credentials: mockCredentials
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('prefers AWS_ENDPOINT_URL for custom endpoint', () => {
|
|
65
|
+
const mockCredentials = jest.fn();
|
|
66
|
+
mockFromIni.mockReturnValue(mockCredentials as ReturnType<typeof fromIni>);
|
|
67
|
+
process.env.AWS_ENDPOINT_URL = 'http://localhost:4566';
|
|
68
|
+
process.env.AWS_SECRETS_MANAGER_ENDPOINT = 'http://localhost:4577';
|
|
69
|
+
|
|
70
|
+
const config = buildAwsClientConfig({ region: 'us-east-1' });
|
|
71
|
+
|
|
72
|
+
expect(config.endpoint).toBe('http://localhost:4566');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('uses AWS_SECRETS_MANAGER_ENDPOINT when AWS_ENDPOINT_URL is unset', () => {
|
|
76
|
+
const mockCredentials = jest.fn();
|
|
77
|
+
mockFromIni.mockReturnValue(mockCredentials as ReturnType<typeof fromIni>);
|
|
78
|
+
delete process.env.AWS_ENDPOINT_URL;
|
|
79
|
+
process.env.AWS_SECRETS_MANAGER_ENDPOINT = 'http://localhost:4577';
|
|
80
|
+
|
|
81
|
+
const config = buildAwsClientConfig({ region: 'us-east-1' });
|
|
82
|
+
|
|
83
|
+
expect(config.endpoint).toBe('http://localhost:4577');
|
|
84
|
+
});
|
|
85
|
+
});
|