diff --git a/src/clients.ts b/src/clients.ts index 0fe1da01..84ef41c1 100644 --- a/src/clients.ts +++ b/src/clients.ts @@ -44,8 +44,11 @@ function log(text: string = '') { } function getRepositoryName(): string { - const output = spawnSync('gh', ['repo', 'view', '--json', 'nameWithOwner'], { stdio: ['ignore', 'pipe', 'inherit'] }).stdout.toString('utf-8'); - + const spawn = spawnSync('gh', ['repo', 'view', '--json', 'nameWithOwner'], { stdio: ['ignore', 'pipe', 'pipe'] }); + if (spawn.status !== 0) { + throw new Error(`Failed to get repository name: ${spawn.stderr?.toString()}`); + } + const output = spawn.stdout.toString('utf-8'); try { const repo = JSON.parse(output); return repo.nameWithOwner; @@ -56,18 +59,27 @@ function getRepositoryName(): string { function storeSecret(repository: string, name: string, value: string): void { const args = ['secret', 'set', '--repo', repository, name]; - spawnSync('gh', args, { input: value, stdio: ['pipe', 'inherit', 'inherit'] }); + const spawn = spawnSync('gh', args, { input: value, stdio: ['pipe', 'inherit', 'pipe'] }); + if (spawn.status !== 0) { + throw new Error(`Failed to store secret '${name}' in repository '${repository}': ${spawn.stderr?.toString()}`); + } } function listSecrets(repository: string): string[] { const args = ['secret', 'list', '--repo', repository]; - const stdout = spawnSync('gh', args, { stdio: ['ignore', 'pipe', 'inherit'] }).stdout.toString('utf-8').trim(); - return stdout.split('\n').map(line => line.split('\t')[0]); + const spawn = spawnSync('gh', args, { stdio: ['ignore', 'pipe', 'pipe'] }); + if (spawn.status !== 0) { + throw new Error(`Failed to list secrets in repository '${repository}': ${spawn.stderr?.toString()}`); + } + return spawn.stdout.toString('utf-8').trim().split('\n').map(line => line.split('\t')[0]); } function removeSecret(repository: string, key: string): void { const args = ['secret', 'remove', '--repo', repository, key]; - spawnSync('gh', args, { stdio: ['ignore', 'inherit', 'inherit'] }); + const spawn = spawnSync('gh', args, { stdio: ['ignore', 'inherit', 'pipe'] }); + if (spawn.status !== 0) { + throw new Error(`Failed to remove secret '${key}' from repository '${repository}': ${spawn.stderr?.toString()}`); + } } function confirmPrompt(): Promise { @@ -96,7 +108,13 @@ async function getSecret(secretId: string, options: SecretOptions = {}): Promise customUserAgent: `${PKG.name}/${PKG.version}`, }); - const result = await client.getSecretValue({ SecretId: secretId }).promise(); + let result; + try { + result = await client.getSecretValue({ SecretId: secretId }).promise(); + } catch (error) { + throw new Error(`Failed to retrieve secret '${secretId}' from SecretsManager: ${error}`); + } + let json; try { json = JSON.parse(result.SecretString!); @@ -113,4 +131,4 @@ async function getSecret(secretId: string, options: SecretOptions = {}): Promise export interface Secret { readonly json: Record; readonly arn: string; -} \ No newline at end of file +} diff --git a/src/update-secrets.ts b/src/update-secrets.ts index fc153d83..545b7eb4 100644 --- a/src/update-secrets.ts +++ b/src/update-secrets.ts @@ -124,7 +124,7 @@ export async function updateSecrets(options: UpdateSecretsOptions) { c.log(`FROM : ${secret.arn}`); c.log(`REPO : ${repository}`); - c.log(`UDPATE: ${keys.join(',')}`); + c.log(`UPDATE: ${keys.join(',')}`); if (pruneCandidates.length > 0) { if (prune) { diff --git a/test/clients.test.ts b/test/clients.test.ts new file mode 100644 index 00000000..b4fa168d --- /dev/null +++ b/test/clients.test.ts @@ -0,0 +1,66 @@ +import { spawnSync } from 'child_process'; +import { SecretsManager } from 'aws-sdk'; +import { DEFAULTS as clients } from '../src/clients'; + +jest.mock('child_process', () => ({ + spawnSync: jest.fn(), +})); +jest.mock('aws-sdk', () => ({ + SecretsManager: jest.fn(), +})); + + +describe('Error handling', () => { + beforeEach(() => { + // @ts-expect-error We've mocked this submodule at top of file. + spawnSync.mockImplementation(function (_a: any, _b: any, options: any) { + const actual = jest.requireActual('child_process'); + return actual.spawnSync('node', ['--eval', "throw new Error('Nope');"], options); + }); + + // @ts-expect-error We've mocked this submodule at top of file. + SecretsManager.mockImplementation(function () { + return { + getSecretValue: function () { + return { + promise: async () => Promise.reject(new Error('Nope')), + }; + }, + }; + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('throws error, when .getRepositoryName() sub-process throws an error', () => { + expect( + () => clients.getRepositoryName(), + ).toThrowError('Failed to get repository name'); + }); + + test('throws error, when .storeSecret() sub-process throws an error', () => { + expect( + () => clients.storeSecret('repo', 'name', 'value'), + ).toThrowError("Failed to store secret 'name' in repository 'repo'"); + }); + + test('throws error, when .listSecrets() sub-process throws an error', () => { + expect( + () => clients.listSecrets('repo'), + ).toThrowError("Failed to list secrets in repository 'repo'"); + }); + + test('throws error, when .removeSecret() sub-process throws an error', () => { + expect( + () => clients.removeSecret('repo', 'key'), + ).toThrowError("Failed to remove secret 'key' from repository 'repo'"); + }); + + test('throws error, when SecretsManager.getSecretValue() throws an error', async () => { + return expect(async () => { + return clients.getSecret('secretId'); + }).rejects.toThrow("Failed to retrieve secret 'secretId' from SecretsManager: Error: Nope"); + }); +});