From a0e8732ef9ef142dd26a708376265583a3c4a18e Mon Sep 17 00:00:00 2001 From: Juan Hapes Date: Mon, 29 Jun 2026 15:10:01 +0200 Subject: [PATCH 1/2] [master] - Read accountsIdsByService from Parameter Store with Secrets Manager fallback --- CHANGELOG.md | 5 + lib/helpers/secret-fetcher.js | 57 +++++++++- lib/invoke-permissions.js | 7 ++ package.json | 1 + tests/helpers/secret-fetcher.js | 193 +++++++++++++++++++++++--------- 5 files changed, 206 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8234aac..8ffa52f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- `Invoker` resolves service account ids from Parameter Store (`accountsIdsByService`) instead of Secrets Manager, with a temporary fallback to the secret when `DEVOPS_ACCOUNT_ID` is absent + +### Added +- `ssm:GetParameter` permission to `invokePermissions` for reading the shared accounts-ids parameter ## [6.3.1] - 2026-06-19 ### Fixed diff --git a/lib/helpers/secret-fetcher.js b/lib/helpers/secret-fetcher.js index 3daa4d8..e585ffd 100644 --- a/lib/helpers/secret-fetcher.js +++ b/lib/helpers/secret-fetcher.js @@ -1,15 +1,20 @@ 'use strict'; +const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm'); const { AwsSecretsManager } = require('@janiscommerce/aws-secrets-manager'); +const logger = require('lllog')(); + const LambdaError = require('../lambda-error'); const isLocalEnv = require('./is-local-env'); +const PARAMETER_NAME = 'accountsIdsByService'; + module.exports = class SecretFetcher { /** - * Get the secret name of Janis Service + * Get the secret name of Janis Service (kept for backward-compat with any direct callers) * @returns {string} */ static get secretName() { @@ -21,8 +26,17 @@ module.exports = class SecretFetcher { } /** - * Request the secret value to AwsSecretManager - * @throws {LambdaError} If the secretValue is missing + * Build the Parameter Store ARN for the shared Devops parameter + * @returns {string} + */ + static get parameterArn() { + return `arn:aws:ssm:${process.env.AWS_REGION}:${process.env.DEVOPS_ACCOUNT_ID}:parameter/${PARAMETER_NAME}`; + } + + /** + * Fetch accounts-ids-by-service from Parameter Store. + * Falls back to Secrets Manager (with a warning) only when DEVOPS_ACCOUNT_ID is absent. + * @throws {LambdaError} If the parameter/secret value is missing or the SSM call fails */ static async fetch() { @@ -34,14 +48,47 @@ module.exports = class SecretFetcher { return; } + if(!process.env.DEVOPS_ACCOUNT_ID) { + logger.warn('DEVOPS_ACCOUNT_ID env var is not set, falling back to Secrets Manager', { + hint: 'Update the service plugin to get the env var injected' + }); + await this.fetchFromSecret(); + return; + } + + await this.fetchFromParameterStore(); + } + + /** + * @private + */ + static async fetchFromParameterStore() { + + const client = new SSMClient(); + try { + const { Parameter } = await client.send(new GetParameterCommand({ Name: this.parameterArn })); + this.secretValue = (Parameter && Parameter.Value && JSON.parse(Parameter.Value)) || false; + } catch(err) { + logger.error('Failed to fetch accountsIdsByService from Parameter Store', { error: err.message }); + this.secretValue = false; + } - const secretHandler = AwsSecretsManager.secret(this.secretName); + if(this.secretValue === false) + throw new LambdaError('Secret is missing', LambdaError.codes.JANIS_SECRET_MISSING); + } + + /** + * @private + */ + static async fetchFromSecret() { + try { + const secretHandler = AwsSecretsManager.secret(this.secretName); this.secretValue = await secretHandler.getValue(); this.secretValue = this.secretValue || false; - } catch(err) { + logger.error('Failed to fetch AccountsIdsByService from Secrets Manager', { error: err.message }); this.secretValue = false; } diff --git a/lib/invoke-permissions.js b/lib/invoke-permissions.js index ecdd3ac..09d9bf7 100644 --- a/lib/invoke-permissions.js +++ b/lib/invoke-permissions.js @@ -16,5 +16,12 @@ module.exports = [ action: 'Sts:AssumeRole', resource: 'arn:aws:iam::*:role/LambdaRemoteInvoke' } + ], + [ + 'iamStatement', + { + action: 'ssm:GetParameter', + resource: 'arn:aws:ssm:*:*:parameter/accountsIdsByService' + } ] ]; diff --git a/package.json b/package.json index 9e8f942..e893c2a 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "dependencies": { "@aws-sdk/client-lambda": "^3.632.0", "@aws-sdk/client-sfn": "^3.632.0", + "@aws-sdk/client-ssm": "^3.1075.0", "@aws-sdk/client-sts": "^3.632.0", "@janiscommerce/api-session": "^3.3.1", "@janiscommerce/aws-secrets-manager": "^1.1.1", diff --git a/tests/helpers/secret-fetcher.js b/tests/helpers/secret-fetcher.js index 54bc125..a5cdb67 100644 --- a/tests/helpers/secret-fetcher.js +++ b/tests/helpers/secret-fetcher.js @@ -2,113 +2,202 @@ const sinon = require('sinon'); const assert = require('assert'); - +const { mockClient } = require('aws-sdk-client-mock'); +const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm'); const { AwsSecretsManager } = require('@janiscommerce/aws-secrets-manager'); +const lllog = require('lllog'); const SecretFetcher = require('../../lib/helpers/secret-fetcher'); const LambdaError = require('../../lib/lambda-error'); +const loggerProto = Object.getPrototypeOf(lllog()); + describe('Libraries', () => { describe('SecretFetcher', () => { const fakeAccountIdsByService = { - pricing: '123456789', - wms: '987654321' + pricing: '123456789012', + wms: '987654321098' }; const fakeSecretHandler = () => ({ getValue: sinon.stub() }); + let ssmClientMock; + + beforeEach(() => { + ssmClientMock = mockClient(SSMClient); + }); + afterEach(() => { sinon.restore(); + ssmClientMock.reset(); delete SecretFetcher.secretValue; delete process.env.JANIS_ENV; + delete process.env.DEVOPS_ACCOUNT_ID; + delete process.env.AWS_REGION; }); - it('Should get the secret AccountIdsByService', async () => { + context('When DEVOPS_ACCOUNT_ID is set (Parameter Store path)', () => { - const secretHandler = fakeSecretHandler(); + beforeEach(() => { + process.env.DEVOPS_ACCOUNT_ID = '111122223333'; + process.env.AWS_REGION = 'us-east-1'; + }); - sinon.stub(AwsSecretsManager, 'secret') - .returns(secretHandler); + it('Should get accounts-ids from Parameter Store and cache the parsed value', async () => { - secretHandler.getValue.resolves(fakeAccountIdsByService); + ssmClientMock.on(GetParameterCommand).resolves({ + Parameter: { Value: JSON.stringify(fakeAccountIdsByService) } + }); - await SecretFetcher.fetch(); + await SecretFetcher.fetch(); - assert.deepStrictEqual(SecretFetcher.secretValue, fakeAccountIdsByService); + assert.deepStrictEqual(SecretFetcher.secretValue, fakeAccountIdsByService); - sinon.assert.calledOnceWithExactly(AwsSecretsManager.secret, 'AccountsIdsByService'); - sinon.assert.calledOnce(secretHandler.getValue); - }); + const calls = ssmClientMock.commandCalls(GetParameterCommand); + assert.strictEqual(calls.length, 1); + assert.deepStrictEqual(calls[0].args[0].input, { + Name: 'arn:aws:ssm:us-east-1:111122223333:parameter/accountsIdsByService' + }); + }); + + it('Should use internal cache and call Parameter Store only once across multiple fetch() calls', async () => { + + ssmClientMock.on(GetParameterCommand).resolves({ + Parameter: { Value: JSON.stringify(fakeAccountIdsByService) } + }); - it('Should use internal cache and make request only once', async () => { + await SecretFetcher.fetch(); + await SecretFetcher.fetch(); + await SecretFetcher.fetch(); - const secretHandler = fakeSecretHandler(); + assert.deepStrictEqual(SecretFetcher.secretValue, fakeAccountIdsByService); + assert.strictEqual(ssmClientMock.commandCalls(GetParameterCommand).length, 1); + }); + + it('Should throw JANIS_SECRET_MISSING when Parameter Store returns empty Parameter', async () => { + + ssmClientMock.on(GetParameterCommand).resolves({ Parameter: null }); + + await assert.rejects(SecretFetcher.fetch(), { + name: 'LambdaError', + code: LambdaError.codes.JANIS_SECRET_MISSING + }); + }); + + it('Should throw JANIS_SECRET_MISSING when Parameter Store returns Parameter without Value', async () => { + + ssmClientMock.on(GetParameterCommand).resolves({ Parameter: {} }); + + await assert.rejects(SecretFetcher.fetch(), { + name: 'LambdaError', + code: LambdaError.codes.JANIS_SECRET_MISSING + }); + }); - sinon.stub(AwsSecretsManager, 'secret') - .returns(secretHandler); + it('Should throw JANIS_SECRET_MISSING when GetParameterCommand fails — no fallback to Secrets Manager', async () => { - secretHandler.getValue.resolves(fakeAccountIdsByService); + ssmClientMock.on(GetParameterCommand).rejects(new Error('ParameterNotFound')); - await SecretFetcher.fetch(); - await SecretFetcher.fetch(); - await SecretFetcher.fetch(); - await SecretFetcher.fetch(); + sinon.stub(loggerProto, 'error'); + sinon.spy(AwsSecretsManager, 'secret'); - assert.deepStrictEqual(SecretFetcher.secretValue, fakeAccountIdsByService); + await assert.rejects(SecretFetcher.fetch(), { + name: 'LambdaError', + code: LambdaError.codes.JANIS_SECRET_MISSING + }); - sinon.assert.calledOnceWithExactly(AwsSecretsManager.secret, 'AccountsIdsByService'); - sinon.assert.calledOnce(secretHandler.getValue); + sinon.assert.notCalled(AwsSecretsManager.secret); + }); }); - it('Should reject when can\'t get the secret value', async () => { + context('When DEVOPS_ACCOUNT_ID is missing (Secrets Manager fallback)', () => { + + it('Should emit a logger.warn and fall back to Secrets Manager', async () => { + + const secretHandler = fakeSecretHandler(); - const secretHandler = fakeSecretHandler(); + const warnStub = sinon.stub(loggerProto, 'warn'); - sinon.stub(AwsSecretsManager, 'secret') - .returns(secretHandler); + sinon.stub(AwsSecretsManager, 'secret').returns(secretHandler); + secretHandler.getValue.resolves(fakeAccountIdsByService); - secretHandler.getValue.resolves(); + await SecretFetcher.fetch(); - await assert.rejects(SecretFetcher.fetch(), { - name: 'LambdaError', - code: LambdaError.codes.JANIS_SECRET_MISSING + assert.deepStrictEqual(SecretFetcher.secretValue, fakeAccountIdsByService); + + sinon.assert.calledOnce(warnStub); + sinon.assert.calledOnceWithExactly(AwsSecretsManager.secret, 'AccountsIdsByService'); + sinon.assert.calledOnce(secretHandler.getValue); + assert.strictEqual(ssmClientMock.commandCalls(GetParameterCommand).length, 0); }); - sinon.assert.calledOnceWithExactly(AwsSecretsManager.secret, 'AccountsIdsByService'); - sinon.assert.calledOnce(secretHandler.getValue); - }); + it('Should treat an empty DEVOPS_ACCOUNT_ID as missing and fall back to Secrets Manager', async () => { + + process.env.DEVOPS_ACCOUNT_ID = ''; + + const secretHandler = fakeSecretHandler(); + + const warnStub = sinon.stub(loggerProto, 'warn'); + + sinon.stub(AwsSecretsManager, 'secret').returns(secretHandler); + secretHandler.getValue.resolves(fakeAccountIdsByService); - it('Should reject when fails at getting the secret value', async () => { + await SecretFetcher.fetch(); - const secretHandler = fakeSecretHandler(); + assert.deepStrictEqual(SecretFetcher.secretValue, fakeAccountIdsByService); - sinon.stub(AwsSecretsManager, 'secret') - .returns(secretHandler); + sinon.assert.calledOnce(warnStub); + sinon.assert.calledOnceWithExactly(AwsSecretsManager.secret, 'AccountsIdsByService'); + assert.strictEqual(ssmClientMock.commandCalls(GetParameterCommand).length, 0); + }); + + it('Should throw JANIS_SECRET_MISSING when Secrets Manager returns empty value', async () => { + + const secretHandler = fakeSecretHandler(); - secretHandler.getValue.rejects(); + sinon.stub(loggerProto, 'warn'); + sinon.stub(AwsSecretsManager, 'secret').returns(secretHandler); + secretHandler.getValue.resolves(); - await assert.rejects(SecretFetcher.fetch(), { - name: 'LambdaError', - code: LambdaError.codes.JANIS_SECRET_MISSING + await assert.rejects(SecretFetcher.fetch(), { + name: 'LambdaError', + code: LambdaError.codes.JANIS_SECRET_MISSING + }); }); - sinon.assert.calledOnceWithExactly(AwsSecretsManager.secret, 'AccountsIdsByService'); - sinon.assert.calledOnce(secretHandler.getValue); + it('Should throw JANIS_SECRET_MISSING when Secrets Manager call fails', async () => { + + const secretHandler = fakeSecretHandler(); + + sinon.stub(loggerProto, 'warn'); + sinon.stub(loggerProto, 'error'); + sinon.stub(AwsSecretsManager, 'secret').returns(secretHandler); + secretHandler.getValue.rejects(); + + await assert.rejects(SecretFetcher.fetch(), { + name: 'LambdaError', + code: LambdaError.codes.JANIS_SECRET_MISSING + }); + }); }); - it('Should return the local secret value (empty object) when the ENV is local', async () => { + context('When running in local environment', () => { - process.env.JANIS_ENV = 'local'; + it('Should return empty object without calling Parameter Store or Secrets Manager', async () => { - sinon.spy(AwsSecretsManager, 'secret'); + process.env.JANIS_ENV = 'local'; - await SecretFetcher.fetch(); + sinon.spy(AwsSecretsManager, 'secret'); - assert.deepStrictEqual(SecretFetcher.secretValue, {}); + await SecretFetcher.fetch(); - sinon.assert.notCalled(AwsSecretsManager.secret); + assert.deepStrictEqual(SecretFetcher.secretValue, {}); + + sinon.assert.notCalled(AwsSecretsManager.secret); + assert.strictEqual(ssmClientMock.commandCalls(GetParameterCommand).length, 0); + }); }); }); }); From a6567b1429ce93122cf62c2cdc7d05f7baefa23d Mon Sep 17 00:00:00 2001 From: Juan Hapes Date: Mon, 29 Jun 2026 15:27:00 +0200 Subject: [PATCH 2/2] [master] - Rename SecretFetcher helper to AccountsIdsProvider (source-agnostic) --- ...et-fetcher.js => accounts-ids-provider.js} | 35 ++--- lib/invoker.js | 6 +- ...et-fetcher.js => accounts-ids-provider.js} | 40 ++--- tests/invoker.js | 142 +++++++++--------- 4 files changed, 106 insertions(+), 117 deletions(-) rename lib/helpers/{secret-fetcher.js => accounts-ids-provider.js} (73%) rename tests/helpers/{secret-fetcher.js => accounts-ids-provider.js} (82%) diff --git a/lib/helpers/secret-fetcher.js b/lib/helpers/accounts-ids-provider.js similarity index 73% rename from lib/helpers/secret-fetcher.js rename to lib/helpers/accounts-ids-provider.js index e585ffd..3121ede 100644 --- a/lib/helpers/secret-fetcher.js +++ b/lib/helpers/accounts-ids-provider.js @@ -11,22 +11,11 @@ const isLocalEnv = require('./is-local-env'); const PARAMETER_NAME = 'accountsIdsByService'; -module.exports = class SecretFetcher { - - /** - * Get the secret name of Janis Service (kept for backward-compat with any direct callers) - * @returns {string} - */ - static get secretName() { - return 'AccountsIdsByService'; - } - - static get localSecretValue() { - return {}; - } +module.exports = class AccountsIdsProvider { /** * Build the Parameter Store ARN for the shared Devops parameter + * @private * @returns {string} */ static get parameterArn() { @@ -40,11 +29,11 @@ module.exports = class SecretFetcher { */ static async fetch() { - if(this.secretValue) + if(this.accountsIds) return; if(isLocalEnv()) { - this.secretValue = this.localSecretValue; + this.accountsIds = {}; return; } @@ -68,13 +57,13 @@ module.exports = class SecretFetcher { try { const { Parameter } = await client.send(new GetParameterCommand({ Name: this.parameterArn })); - this.secretValue = (Parameter && Parameter.Value && JSON.parse(Parameter.Value)) || false; + this.accountsIds = (Parameter && Parameter.Value && JSON.parse(Parameter.Value)) || false; } catch(err) { logger.error('Failed to fetch accountsIdsByService from Parameter Store', { error: err.message }); - this.secretValue = false; + this.accountsIds = false; } - if(this.secretValue === false) + if(this.accountsIds === false) throw new LambdaError('Secret is missing', LambdaError.codes.JANIS_SECRET_MISSING); } @@ -84,15 +73,15 @@ module.exports = class SecretFetcher { static async fetchFromSecret() { try { - const secretHandler = AwsSecretsManager.secret(this.secretName); - this.secretValue = await secretHandler.getValue(); - this.secretValue = this.secretValue || false; + const secretHandler = AwsSecretsManager.secret('AccountsIdsByService'); + this.accountsIds = await secretHandler.getValue(); + this.accountsIds = this.accountsIds || false; } catch(err) { logger.error('Failed to fetch AccountsIdsByService from Secrets Manager', { error: err.message }); - this.secretValue = false; + this.accountsIds = false; } - if(this.secretValue === false) + if(this.accountsIds === false) throw new LambdaError('Secret is missing', LambdaError.codes.JANIS_SECRET_MISSING); } }; diff --git a/lib/invoker.js b/lib/invoker.js index 6d9ac20..6c3517f 100644 --- a/lib/invoker.js +++ b/lib/invoker.js @@ -4,7 +4,7 @@ const { ApiSession } = require('@janiscommerce/api-session'); const LambdaError = require('./lambda-error'); -const SecretFetcher = require('./helpers/secret-fetcher'); +const AccountsIdsProvider = require('./helpers/accounts-ids-provider'); const LambdaInstance = require('./helpers/lambda-instance'); const getLambdaFunctionName = require('./helpers/get-lambda-function-name'); @@ -291,9 +291,9 @@ module.exports = class Invoker { if(isLocalEnv()) return; - await SecretFetcher.fetch(); + await AccountsIdsProvider.fetch(); - const { [serviceCode]: serviceAccountId } = SecretFetcher.secretValue; + const { [serviceCode]: serviceAccountId } = AccountsIdsProvider.accountsIds; if(!serviceAccountId) throw new LambdaError(`Service account ID not found for service code ${serviceCode}`, LambdaError.codes.NO_SERVICE_ACCOUNT_ID); diff --git a/tests/helpers/secret-fetcher.js b/tests/helpers/accounts-ids-provider.js similarity index 82% rename from tests/helpers/secret-fetcher.js rename to tests/helpers/accounts-ids-provider.js index a5cdb67..733e89f 100644 --- a/tests/helpers/secret-fetcher.js +++ b/tests/helpers/accounts-ids-provider.js @@ -7,14 +7,14 @@ const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm'); const { AwsSecretsManager } = require('@janiscommerce/aws-secrets-manager'); const lllog = require('lllog'); -const SecretFetcher = require('../../lib/helpers/secret-fetcher'); +const AccountsIdsProvider = require('../../lib/helpers/accounts-ids-provider'); const LambdaError = require('../../lib/lambda-error'); const loggerProto = Object.getPrototypeOf(lllog()); describe('Libraries', () => { - describe('SecretFetcher', () => { + describe('AccountsIdsProvider', () => { const fakeAccountIdsByService = { pricing: '123456789012', @@ -32,7 +32,7 @@ describe('Libraries', () => { afterEach(() => { sinon.restore(); ssmClientMock.reset(); - delete SecretFetcher.secretValue; + delete AccountsIdsProvider.accountsIds; delete process.env.JANIS_ENV; delete process.env.DEVOPS_ACCOUNT_ID; delete process.env.AWS_REGION; @@ -51,9 +51,9 @@ describe('Libraries', () => { Parameter: { Value: JSON.stringify(fakeAccountIdsByService) } }); - await SecretFetcher.fetch(); + await AccountsIdsProvider.fetch(); - assert.deepStrictEqual(SecretFetcher.secretValue, fakeAccountIdsByService); + assert.deepStrictEqual(AccountsIdsProvider.accountsIds, fakeAccountIdsByService); const calls = ssmClientMock.commandCalls(GetParameterCommand); assert.strictEqual(calls.length, 1); @@ -68,11 +68,11 @@ describe('Libraries', () => { Parameter: { Value: JSON.stringify(fakeAccountIdsByService) } }); - await SecretFetcher.fetch(); - await SecretFetcher.fetch(); - await SecretFetcher.fetch(); + await AccountsIdsProvider.fetch(); + await AccountsIdsProvider.fetch(); + await AccountsIdsProvider.fetch(); - assert.deepStrictEqual(SecretFetcher.secretValue, fakeAccountIdsByService); + assert.deepStrictEqual(AccountsIdsProvider.accountsIds, fakeAccountIdsByService); assert.strictEqual(ssmClientMock.commandCalls(GetParameterCommand).length, 1); }); @@ -80,7 +80,7 @@ describe('Libraries', () => { ssmClientMock.on(GetParameterCommand).resolves({ Parameter: null }); - await assert.rejects(SecretFetcher.fetch(), { + await assert.rejects(AccountsIdsProvider.fetch(), { name: 'LambdaError', code: LambdaError.codes.JANIS_SECRET_MISSING }); @@ -90,7 +90,7 @@ describe('Libraries', () => { ssmClientMock.on(GetParameterCommand).resolves({ Parameter: {} }); - await assert.rejects(SecretFetcher.fetch(), { + await assert.rejects(AccountsIdsProvider.fetch(), { name: 'LambdaError', code: LambdaError.codes.JANIS_SECRET_MISSING }); @@ -103,7 +103,7 @@ describe('Libraries', () => { sinon.stub(loggerProto, 'error'); sinon.spy(AwsSecretsManager, 'secret'); - await assert.rejects(SecretFetcher.fetch(), { + await assert.rejects(AccountsIdsProvider.fetch(), { name: 'LambdaError', code: LambdaError.codes.JANIS_SECRET_MISSING }); @@ -123,9 +123,9 @@ describe('Libraries', () => { sinon.stub(AwsSecretsManager, 'secret').returns(secretHandler); secretHandler.getValue.resolves(fakeAccountIdsByService); - await SecretFetcher.fetch(); + await AccountsIdsProvider.fetch(); - assert.deepStrictEqual(SecretFetcher.secretValue, fakeAccountIdsByService); + assert.deepStrictEqual(AccountsIdsProvider.accountsIds, fakeAccountIdsByService); sinon.assert.calledOnce(warnStub); sinon.assert.calledOnceWithExactly(AwsSecretsManager.secret, 'AccountsIdsByService'); @@ -144,9 +144,9 @@ describe('Libraries', () => { sinon.stub(AwsSecretsManager, 'secret').returns(secretHandler); secretHandler.getValue.resolves(fakeAccountIdsByService); - await SecretFetcher.fetch(); + await AccountsIdsProvider.fetch(); - assert.deepStrictEqual(SecretFetcher.secretValue, fakeAccountIdsByService); + assert.deepStrictEqual(AccountsIdsProvider.accountsIds, fakeAccountIdsByService); sinon.assert.calledOnce(warnStub); sinon.assert.calledOnceWithExactly(AwsSecretsManager.secret, 'AccountsIdsByService'); @@ -161,7 +161,7 @@ describe('Libraries', () => { sinon.stub(AwsSecretsManager, 'secret').returns(secretHandler); secretHandler.getValue.resolves(); - await assert.rejects(SecretFetcher.fetch(), { + await assert.rejects(AccountsIdsProvider.fetch(), { name: 'LambdaError', code: LambdaError.codes.JANIS_SECRET_MISSING }); @@ -176,7 +176,7 @@ describe('Libraries', () => { sinon.stub(AwsSecretsManager, 'secret').returns(secretHandler); secretHandler.getValue.rejects(); - await assert.rejects(SecretFetcher.fetch(), { + await assert.rejects(AccountsIdsProvider.fetch(), { name: 'LambdaError', code: LambdaError.codes.JANIS_SECRET_MISSING }); @@ -191,9 +191,9 @@ describe('Libraries', () => { sinon.spy(AwsSecretsManager, 'secret'); - await SecretFetcher.fetch(); + await AccountsIdsProvider.fetch(); - assert.deepStrictEqual(SecretFetcher.secretValue, {}); + assert.deepStrictEqual(AccountsIdsProvider.accountsIds, {}); sinon.assert.notCalled(AwsSecretsManager.secret); assert.strictEqual(ssmClientMock.commandCalls(GetParameterCommand).length, 0); diff --git a/tests/invoker.js b/tests/invoker.js index 5007aea..7d76bf7 100644 --- a/tests/invoker.js +++ b/tests/invoker.js @@ -9,7 +9,7 @@ const RouterFetcher = require('@janiscommerce/router-fetcher'); const { Invoker, LambdaError } = require('../lib/index'); const { LambdaWrapper } = require('../lib/helpers/aws-wrappers'); const LambdaInstance = require('../lib/helpers/lambda-instance'); -const SecretFetcher = require('../lib/helpers/secret-fetcher'); +const AccountsIdsProvider = require('../lib/helpers/accounts-ids-provider'); describe('Invoker', () => { @@ -69,7 +69,7 @@ describe('Invoker', () => { afterEach(() => { process.env = oldEnv; - delete SecretFetcher.secretValue; + delete AccountsIdsProvider.accountsIds; }); describe('Call', () => { @@ -757,17 +757,17 @@ describe('Invoker', () => { sinon.spy(LambdaWrapper.prototype, 'invoke'); - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = {}; + AccountsIdsProvider.accountsIds = {}; await assert.rejects(Invoker.serviceCall('some-service', 'some-function'), { name: 'LambdaError', code: LambdaError.codes.NO_SERVICE_ACCOUNT_ID }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.notCalled(LambdaWrapper.prototype.invoke); }); @@ -778,10 +778,10 @@ describe('Invoker', () => { const ConfigError = new Error('Missing region in config'); ConfigError.name = 'ConfigError'; - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -790,16 +790,16 @@ describe('Invoker', () => { await assert.rejects(Invoker.serviceCall('some-service', functionName), { name: 'ConfigError', message: 'Missing region in config' }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnce(LambdaWrapper.prototype.invoke); }); it('Should fail if invoke rejects (rare or local cases)', async () => { - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -808,16 +808,16 @@ describe('Invoker', () => { await assert.rejects(Invoker.serviceCall('some-service', functionName), { name: 'Error', message: 'AWS Failed' }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnce(LambdaWrapper.prototype.invoke); }); it('Should return the lambda response formatted', async () => { - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -831,7 +831,7 @@ describe('Invoker', () => { payload: {} }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${fakeServiceAccountId}:function:${lambdaExternalFunctionName}`, @@ -847,11 +847,11 @@ describe('Invoker', () => { sinon.stub(LambdaWrapper.prototype, 'invoke').resolves(invokeAsyncResponse); - sinon.spy(SecretFetcher, 'fetch'); + sinon.spy(AccountsIdsProvider, 'fetch'); const lambdaResponse = await Invoker.serviceCall('some-service', functionName); - sinon.assert.notCalled(SecretFetcher.fetch); + sinon.assert.notCalled(AccountsIdsProvider.fetch); assert.deepStrictEqual(lambdaResponse, { statusCode: invokeAsyncResponse.StatusCode, @@ -872,7 +872,7 @@ describe('Invoker', () => { sinon.stub(RouterFetcher.prototype, 'getSchema').resolves({}); sinon.spy(LambdaWrapper.prototype, 'invoke'); - sinon.spy(SecretFetcher, 'fetch'); + sinon.spy(AccountsIdsProvider, 'fetch'); await assert.rejects(Invoker.serviceCall('some-service', functionName), { name: 'LambdaError', @@ -880,16 +880,16 @@ describe('Invoker', () => { }); sinon.assert.calledOnceWithExactly(RouterFetcher.prototype.getSchema, 'some-service'); - sinon.assert.notCalled(SecretFetcher.fetch); + sinon.assert.notCalled(AccountsIdsProvider.fetch); sinon.assert.notCalled(LambdaWrapper.prototype.invoke); }); it('Should reject if the lambda response status code is 400 or higher', async () => { - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -904,7 +904,7 @@ describe('Invoker', () => { code: LambdaError.codes.INVOCATION_FAILED }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${fakeServiceAccountId}:function:${lambdaExternalFunctionName}`, @@ -924,10 +924,10 @@ describe('Invoker', () => { Payload: '{"message": "Success"}' }; - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -941,7 +941,7 @@ describe('Invoker', () => { payload: JSON.parse(lambdaResponse.Payload) }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${fakeServiceAccountId}:function:${lambdaExternalFunctionName}`, @@ -959,10 +959,10 @@ describe('Invoker', () => { Payload: 'OK' }; - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -976,7 +976,7 @@ describe('Invoker', () => { payload: lambdaResponse.Payload }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${fakeServiceAccountId}:function:${lambdaExternalFunctionName}`, @@ -992,10 +992,10 @@ describe('Invoker', () => { StatusCode: 202 }; - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -1009,7 +1009,7 @@ describe('Invoker', () => { payload: {} }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${fakeServiceAccountId}:function:${lambdaExternalFunctionName}`, @@ -1023,10 +1023,10 @@ describe('Invoker', () => { it('Should not reject if the lambda response status code is 400 or higher', async () => { - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -1045,7 +1045,7 @@ describe('Invoker', () => { } }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${fakeServiceAccountId}:function:${lambdaExternalFunctionName}`, @@ -1167,17 +1167,17 @@ describe('Invoker', () => { sinon.spy(LambdaWrapper.prototype, 'invoke'); - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = {}; + AccountsIdsProvider.accountsIds = {}; await assert.rejects(Invoker.serviceClientCall('some-service', 'some-function', client), { name: 'LambdaError', code: LambdaError.codes.NO_SERVICE_ACCOUNT_ID }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.notCalled(LambdaWrapper.prototype.invoke); }); @@ -1188,10 +1188,10 @@ describe('Invoker', () => { const ConfigError = new Error('Missing region in config'); ConfigError.name = 'ConfigError'; - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -1203,16 +1203,16 @@ describe('Invoker', () => { message: 'Missing region in config' }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnce(LambdaWrapper.prototype.invoke); }); it('Should fail if invoke rejects (rare or local cases)', async () => { - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -1221,16 +1221,16 @@ describe('Invoker', () => { await assert.rejects(Invoker.serviceClientCall('some-service', functionName, client), { name: 'Error', message: 'AWS Failed' }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnce(LambdaWrapper.prototype.invoke); }); it('Should return the lambda response formatted', async () => { - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -1244,7 +1244,7 @@ describe('Invoker', () => { payload: {} }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${fakeServiceAccountId}:function:${lambdaExternalFunctionName}`, @@ -1261,11 +1261,11 @@ describe('Invoker', () => { sinon.stub(LambdaWrapper.prototype, 'invoke').resolves(invokeAsyncResponse); - sinon.spy(SecretFetcher, 'fetch'); + sinon.spy(AccountsIdsProvider, 'fetch'); const lambdaResponse = await Invoker.serviceClientCall('some-service', functionName, client); - sinon.assert.notCalled(SecretFetcher.fetch); + sinon.assert.notCalled(AccountsIdsProvider.fetch); assert.deepStrictEqual(lambdaResponse, { statusCode: invokeAsyncResponse.StatusCode, @@ -1286,7 +1286,7 @@ describe('Invoker', () => { sinon.stub(RouterFetcher.prototype, 'getSchema').resolves({ servers: [routerFetcherSchema.servers[0]] }); sinon.spy(LambdaWrapper.prototype, 'invoke'); - sinon.spy(SecretFetcher, 'fetch'); + sinon.spy(AccountsIdsProvider, 'fetch'); await assert.rejects(Invoker.serviceClientCall('some-service', functionName, client), { name: 'LambdaError', @@ -1295,16 +1295,16 @@ describe('Invoker', () => { sinon.assert.calledOnceWithExactly(RouterFetcher.prototype.getSchema, 'some-service'); - sinon.assert.notCalled(SecretFetcher.fetch); + sinon.assert.notCalled(AccountsIdsProvider.fetch); sinon.assert.notCalled(LambdaWrapper.prototype.invoke); }); it('Should reject if the lambda response status code is 400 or higher', async () => { - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -1319,7 +1319,7 @@ describe('Invoker', () => { code: LambdaError.codes.INVOCATION_FAILED }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${fakeServiceAccountId}:function:${lambdaExternalFunctionName}`, @@ -1340,10 +1340,10 @@ describe('Invoker', () => { Payload: '{"message": "Success"}' }; - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -1357,7 +1357,7 @@ describe('Invoker', () => { payload: JSON.parse(lambdaResponse.Payload) }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${fakeServiceAccountId}:function:${lambdaExternalFunctionName}`, @@ -1375,10 +1375,10 @@ describe('Invoker', () => { Payload: 'OK' }; - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -1392,7 +1392,7 @@ describe('Invoker', () => { payload: lambdaResponse.Payload }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${fakeServiceAccountId}:function:${lambdaExternalFunctionName}`, @@ -1409,10 +1409,10 @@ describe('Invoker', () => { StatusCode: 202 }; - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -1426,7 +1426,7 @@ describe('Invoker', () => { payload: {} }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${fakeServiceAccountId}:function:${lambdaExternalFunctionName}`, @@ -1441,10 +1441,10 @@ describe('Invoker', () => { it('Should not reject if the lambda response status code is 400 or higher', async () => { - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); - SecretFetcher.secretValue = fakeSecretValue; + AccountsIdsProvider.accountsIds = fakeSecretValue; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -1465,7 +1465,7 @@ describe('Invoker', () => { } }); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${fakeServiceAccountId}:function:${lambdaExternalFunctionName}`, @@ -1579,7 +1579,7 @@ describe('Invoker', () => { it('Should reject when no received namespace', async () => { - sinon.spy(SecretFetcher, 'fetch'); + sinon.spy(AccountsIdsProvider, 'fetch'); sinon.spy(LambdaWrapper.prototype, 'invoke'); @@ -1588,14 +1588,14 @@ describe('Invoker', () => { { name: 'LambdaError', code: LambdaError.codes.NO_ENDPOINT_PARAMS } ); - sinon.assert.notCalled(SecretFetcher.fetch); + sinon.assert.notCalled(AccountsIdsProvider.fetch); sinon.assert.notCalled(LambdaWrapper.prototype.invoke); }); it('Should reject when no method was received', async () => { - sinon.spy(SecretFetcher, 'fetch'); + sinon.spy(AccountsIdsProvider, 'fetch'); sinon.spy(LambdaWrapper.prototype, 'invoke'); @@ -1604,19 +1604,19 @@ describe('Invoker', () => { { name: 'LambdaError', code: LambdaError.codes.NO_ENDPOINT_PARAMS } ); - sinon.assert.notCalled(SecretFetcher.fetch); + sinon.assert.notCalled(AccountsIdsProvider.fetch); sinon.assert.notCalled(LambdaWrapper.prototype.invoke); }); it('Should invoke the api lambda of the service', async () => { - sinon.stub(SecretFetcher, 'fetch') + sinon.stub(AccountsIdsProvider, 'fetch') .resolves(); const accountId = 123456789; - SecretFetcher.secretValue = { catalog: accountId }; + AccountsIdsProvider.accountsIds = { catalog: accountId }; sinon.stub(LambdaInstance, 'getInstanceWithRole') .resolves(new LambdaWrapper()); @@ -1630,7 +1630,7 @@ describe('Invoker', () => { await Invoker.apiCall('catalog', 'Update-Product', 'product', 'update', event); - sinon.assert.calledOnce(SecretFetcher.fetch); + sinon.assert.calledOnce(AccountsIdsProvider.fetch); sinon.assert.calledOnceWithExactly(LambdaWrapper.prototype.invoke, { FunctionName: `${accountId}:function:API-Catalog-Update-Product-test`,