From d20ad9cf57b7353032fefd666c0364f365f69919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A9=20Donzallaz?= Date: Sun, 3 Apr 2022 12:23:52 +0200 Subject: [PATCH] feat(test): create ssm utilities (#14) --- .changeset/tough-cherries-lay.md | 6 + package-lock.json | 175 ++++++++++++---- packages/test/package.json | 1 + packages/test/src/index.ts | 3 + packages/test/src/system-store.ts | 109 ++++++++++ packages/test/tests/system-store.test.ts | 254 +++++++++++++++++++++++ 6 files changed, 512 insertions(+), 36 deletions(-) create mode 100644 .changeset/tough-cherries-lay.md create mode 100644 packages/test/src/system-store.ts create mode 100644 packages/test/tests/system-store.test.ts diff --git a/.changeset/tough-cherries-lay.md b/.changeset/tough-cherries-lay.md new file mode 100644 index 0000000..72adddf --- /dev/null +++ b/.changeset/tough-cherries-lay.md @@ -0,0 +1,6 @@ +--- +'@onia/test': patch +--- + +Create a `SystemStore` class to populate environment variables from SSM parameters. + diff --git a/package-lock.json b/package-lock.json index eb0aa56..96e73fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -332,6 +332,56 @@ "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", "dev": true }, + "node_modules/@aws-sdk/client-ssm": { + "version": "3.58.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.58.0.tgz", + "integrity": "sha512-vyHtfn5pZfte018sy6+E8Y638ibe7E4vUZrWP2ojtjNJVxaZGUwQtc07B1s/qmj7v8CxVEkd/ae9NP07IeF5Wg==", + "dependencies": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/client-sts": "3.58.0", + "@aws-sdk/config-resolver": "3.58.0", + "@aws-sdk/credential-provider-node": "3.58.0", + "@aws-sdk/fetch-http-handler": "3.58.0", + "@aws-sdk/hash-node": "3.55.0", + "@aws-sdk/invalid-dependency": "3.55.0", + "@aws-sdk/middleware-content-length": "3.58.0", + "@aws-sdk/middleware-host-header": "3.58.0", + "@aws-sdk/middleware-logger": "3.55.0", + "@aws-sdk/middleware-retry": "3.58.0", + "@aws-sdk/middleware-serde": "3.55.0", + "@aws-sdk/middleware-signing": "3.58.0", + "@aws-sdk/middleware-stack": "3.55.0", + "@aws-sdk/middleware-user-agent": "3.58.0", + "@aws-sdk/node-config-provider": "3.58.0", + "@aws-sdk/node-http-handler": "3.58.0", + "@aws-sdk/protocol-http": "3.58.0", + "@aws-sdk/smithy-client": "3.55.0", + "@aws-sdk/types": "3.55.0", + "@aws-sdk/url-parser": "3.55.0", + "@aws-sdk/util-base64-browser": "3.58.0", + "@aws-sdk/util-base64-node": "3.55.0", + "@aws-sdk/util-body-length-browser": "3.55.0", + "@aws-sdk/util-body-length-node": "3.55.0", + "@aws-sdk/util-defaults-mode-browser": "3.55.0", + "@aws-sdk/util-defaults-mode-node": "3.58.0", + "@aws-sdk/util-user-agent-browser": "3.58.0", + "@aws-sdk/util-user-agent-node": "3.58.0", + "@aws-sdk/util-utf8-browser": "3.55.0", + "@aws-sdk/util-utf8-node": "3.55.0", + "@aws-sdk/util-waiter": "3.55.0", + "tslib": "^2.3.1", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, "node_modules/@aws-sdk/client-sso": { "version": "3.58.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.58.0.tgz", @@ -1659,7 +1709,6 @@ "version": "3.55.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.55.0.tgz", "integrity": "sha512-Do34MKPFSC/+zVN6vY+FZ+0WN61hzga4nPoAC590AOjs8rW6/H6sDN6Gz1KAZbPnuQUZfvsIJjMxN7lblXHJkQ==", - "dev": true, "dependencies": { "@aws-sdk/abort-controller": "3.55.0", "@aws-sdk/types": "3.55.0", @@ -1672,8 +1721,7 @@ "node_modules/@aws-sdk/util-waiter/node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/@aws-sdk/xml-builder": { "version": "3.55.0", @@ -3677,9 +3725,9 @@ "dev": true }, "node_modules/emittery": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.1.tgz", - "integrity": "sha512-OBSS9uVXbpgqEGq2V5VnpfCu9vSnfiR9eYVJmxFYToNIcWRHkM4BAFbJe/PWjf/pQdEL7OPxd2jOW/bJiyX7gg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", "dev": true, "engines": { "node": ">=12" @@ -3879,9 +3927,9 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.0.tgz", - "integrity": "sha512-MNHS3u5pebvROX4MjGP9coda589ZGfL1SqdxUV4kSrcclfDRWvNE2D+eljbnWVMvWDVRgT89nhscMHPKYGcObQ==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz", + "integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==", "dependencies": { "debug": "^4.3.4", "glob": "^7.2.0", @@ -5092,9 +5140,9 @@ } }, "node_modules/is-number-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -5163,9 +5211,12 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dependencies": { + "call-bind": "^1.0.2" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6352,9 +6403,9 @@ } }, "node_modules/prettier": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.1.tgz", - "integrity": "sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", + "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==", "bin": { "prettier": "bin-prettier.js" }, @@ -8070,6 +8121,7 @@ "license": "MIT", "dependencies": { "@aws-sdk/client-cognito-identity-provider": "^3.0.0", + "@aws-sdk/client-ssm": "^3.0.0", "@hapi/hoek": "^9.0.0" }, "devDependencies": { @@ -8387,6 +8439,55 @@ } } }, + "@aws-sdk/client-ssm": { + "version": "3.58.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.58.0.tgz", + "integrity": "sha512-vyHtfn5pZfte018sy6+E8Y638ibe7E4vUZrWP2ojtjNJVxaZGUwQtc07B1s/qmj7v8CxVEkd/ae9NP07IeF5Wg==", + "requires": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/client-sts": "3.58.0", + "@aws-sdk/config-resolver": "3.58.0", + "@aws-sdk/credential-provider-node": "3.58.0", + "@aws-sdk/fetch-http-handler": "3.58.0", + "@aws-sdk/hash-node": "3.55.0", + "@aws-sdk/invalid-dependency": "3.55.0", + "@aws-sdk/middleware-content-length": "3.58.0", + "@aws-sdk/middleware-host-header": "3.58.0", + "@aws-sdk/middleware-logger": "3.55.0", + "@aws-sdk/middleware-retry": "3.58.0", + "@aws-sdk/middleware-serde": "3.55.0", + "@aws-sdk/middleware-signing": "3.58.0", + "@aws-sdk/middleware-stack": "3.55.0", + "@aws-sdk/middleware-user-agent": "3.58.0", + "@aws-sdk/node-config-provider": "3.58.0", + "@aws-sdk/node-http-handler": "3.58.0", + "@aws-sdk/protocol-http": "3.58.0", + "@aws-sdk/smithy-client": "3.55.0", + "@aws-sdk/types": "3.55.0", + "@aws-sdk/url-parser": "3.55.0", + "@aws-sdk/util-base64-browser": "3.58.0", + "@aws-sdk/util-base64-node": "3.55.0", + "@aws-sdk/util-body-length-browser": "3.55.0", + "@aws-sdk/util-body-length-node": "3.55.0", + "@aws-sdk/util-defaults-mode-browser": "3.55.0", + "@aws-sdk/util-defaults-mode-node": "3.58.0", + "@aws-sdk/util-user-agent-browser": "3.58.0", + "@aws-sdk/util-user-agent-node": "3.58.0", + "@aws-sdk/util-utf8-browser": "3.55.0", + "@aws-sdk/util-utf8-node": "3.55.0", + "@aws-sdk/util-waiter": "3.55.0", + "tslib": "^2.3.1", + "uuid": "^8.3.2" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + } + } + }, "@aws-sdk/client-sso": { "version": "3.58.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.58.0.tgz", @@ -9665,7 +9766,6 @@ "version": "3.55.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.55.0.tgz", "integrity": "sha512-Do34MKPFSC/+zVN6vY+FZ+0WN61hzga4nPoAC590AOjs8rW6/H6sDN6Gz1KAZbPnuQUZfvsIJjMxN7lblXHJkQ==", - "dev": true, "requires": { "@aws-sdk/abort-controller": "3.55.0", "@aws-sdk/types": "3.55.0", @@ -9675,8 +9775,7 @@ "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" } } }, @@ -10159,6 +10258,7 @@ "version": "file:packages/test", "requires": { "@aws-sdk/client-cognito-identity-provider": "^3.0.0", + "@aws-sdk/client-ssm": "^3.0.0", "@hapi/hoek": "^9.0.0", "@onia/mock": "^0.1.0" } @@ -11257,9 +11357,9 @@ "dev": true }, "emittery": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.1.tgz", - "integrity": "sha512-OBSS9uVXbpgqEGq2V5VnpfCu9vSnfiR9eYVJmxFYToNIcWRHkM4BAFbJe/PWjf/pQdEL7OPxd2jOW/bJiyX7gg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", "dev": true }, "emoji-regex": { @@ -11512,9 +11612,9 @@ } }, "eslint-import-resolver-typescript": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.0.tgz", - "integrity": "sha512-MNHS3u5pebvROX4MjGP9coda589ZGfL1SqdxUV4kSrcclfDRWvNE2D+eljbnWVMvWDVRgT89nhscMHPKYGcObQ==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz", + "integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==", "requires": { "debug": "^4.3.4", "glob": "^7.2.0", @@ -12280,9 +12380,9 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-number-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", "requires": { "has-tostringtag": "^1.0.0" } @@ -12327,9 +12427,12 @@ } }, "is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "requires": { + "call-bind": "^1.0.2" + } }, "is-string": { "version": "1.0.7", @@ -13170,9 +13273,9 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" }, "prettier": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.1.tgz", - "integrity": "sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", + "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==" }, "prettier-linter-helpers": { "version": "1.0.0", diff --git a/packages/test/package.json b/packages/test/package.json index fa9c634..796ec4e 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -12,6 +12,7 @@ ], "dependencies": { "@aws-sdk/client-cognito-identity-provider": "^3.0.0", + "@aws-sdk/client-ssm": "^3.0.0", "@hapi/hoek": "^9.0.0" }, "devDependencies": { diff --git a/packages/test/src/index.ts b/packages/test/src/index.ts index cfc97eb..1031891 100644 --- a/packages/test/src/index.ts +++ b/packages/test/src/index.ts @@ -5,3 +5,6 @@ export * from './cognito-user'; // Gateway export * from './gateway-wrapper'; + +// System +export * from './system-store'; diff --git a/packages/test/src/system-store.ts b/packages/test/src/system-store.ts new file mode 100644 index 0000000..e8f39ca --- /dev/null +++ b/packages/test/src/system-store.ts @@ -0,0 +1,109 @@ +import { + DescribeParametersCommand, + GetParametersCommand, + ParameterMetadata, + ParameterStringFilter, + Parameter, + SSMClient, +} from '@aws-sdk/client-ssm'; + +const client = new SSMClient({}); + +export class SystemStore { + /** + * The parameters chunk size. + */ + private static chunk = 10; + + /** + * Create a new system store. + */ + constructor(private $path = '', private $decryption = true) {} + + /** + * Get all parameters names. + */ + private async metadata(token?: string): Promise { + const filters: ParameterStringFilter[] = []; + + if (this.$path.length > 0) { + filters.push({ + Key: 'Name', + Option: 'BeginsWith', + Values: [this.$path], + }); + } + + const result = await client.send( + new DescribeParametersCommand({ + NextToken: token, + ParameterFilters: filters, + }) + ); + + const metadata = result.Parameters ?? []; + + if (result.NextToken) { + metadata.push(...(await this.metadata(result.NextToken))); + } + + return metadata; + } + + /** + * Get all parameters values. + */ + private async parameters(names: string[]): Promise { + const parameters: Parameter[] = []; + + for (let index = 0; index < names.length; index += SystemStore.chunk) { + const result = await client.send( + new GetParametersCommand({ + Names: names.slice(index, index + SystemStore.chunk), + WithDecryption: this.$decryption, + }) + ); + + if (result.Parameters) { + parameters.push(...result.Parameters); + } + } + + return parameters; + } + + /** + * Populate process.env from parameters. + */ + async config(prefix = ''): Promise> { + const metadata = await this.metadata(); + + const parameters = await this.parameters( + metadata.map((m) => String(m.Name)) + ); + + const output: Record = {}; + + for (const parameter of parameters) { + if (!parameter.Name || !parameter.Value) { + continue; + } + + let name = parameter.Name; + + name = name.slice(this.$path.length); + name = prefix + '/' + name; + name = name.split(/[/-]/g).filter(Boolean).join('_').toUpperCase(); + + output[name] = parameter.Value; + } + + for (const name of Object.keys(output)) { + if (!process.env.hasOwnProperty(name)) { + process.env[name] = output[name]; + } + } + + return output; + } +} diff --git a/packages/test/tests/system-store.test.ts b/packages/test/tests/system-store.test.ts new file mode 100644 index 0000000..7b49c53 --- /dev/null +++ b/packages/test/tests/system-store.test.ts @@ -0,0 +1,254 @@ +import anyTest, { TestFn } from 'ava'; + +import { + DescribeParametersCommand, + GetParametersCommand, + SSMClient, +} from '@aws-sdk/client-ssm'; + +import { ClientMock, ClientType } from '@onia/mock'; + +import { SystemStore } from '../src'; + +interface TestContext { + mock: ClientType; +} + +const test = anyTest as TestFn; + +test.beforeEach(function (t) { + t.context.mock = new ClientMock(SSMClient); +}); + +test.afterEach.always(function (t) { + t.context.mock.restore(); +}); + +test('gets all parameters', async function (t) { + const { mock } = t.context; + + mock.on(DescribeParametersCommand).resolves({ + Parameters: [{ Name: '/a' }, { Name: '/b' }], + }); + + mock.on(GetParametersCommand).resolves({ + Parameters: [ + { Name: '/a', Value: '1' }, + { Name: '/b', Value: '2' }, + ], + }); + + const result = await new SystemStore().config(); + + t.deepEqual(result, { + A: '1', + B: '2', + }); + + t.is(mock.count(), 2); + + t.like(mock.call(0), { ParameterFilters: [] }); + t.like(mock.call(1), { Names: ['/a', '/b'] }); +}); + +test('gets all parameters with path', async function (t) { + const { mock } = t.context; + + mock.on(DescribeParametersCommand).resolves({ + Parameters: [{ Name: '/dev/a' }, { Name: '/dev/b' }], + }); + + mock.on(GetParametersCommand).resolves({ + Parameters: [ + { Name: '/dev/a', Value: '1' }, + { Name: '/dev/b', Value: '2' }, + ], + }); + + const result = await new SystemStore('/dev').config(); + + t.deepEqual(result, { + A: '1', + B: '2', + }); + + t.is(mock.count(), 2); + + t.like(mock.call(0), { + ParameterFilters: [ + { + Key: 'Name', + Option: 'BeginsWith', + Values: ['/dev'], + }, + ], + }); + + t.like(mock.call(1), { Names: ['/dev/a', '/dev/b'] }); +}); + +test('gets all parameters in multiple batches', async function (t) { + const { mock } = t.context; + + mock + .on(DescribeParametersCommand) + .onCall(0) + .resolves({ + Parameters: [{ Name: '/a' }, { Name: '/b' }], + NextToken: 'token-1', + }) + .onCall(1) + .resolves({ + Parameters: [{ Name: '/c' }, { Name: '/d' }], + NextToken: 'token-2', + }) + .onCall(2) + .resolves({ + Parameters: undefined, + }); + + mock.on(GetParametersCommand).resolves({ + Parameters: [ + { Name: '/a', Value: '1' }, + { Name: '/b', Value: '2' }, + { Name: '/c', Value: '3' }, + { Name: '/d', Value: '4' }, + ], + }); + + const result = await new SystemStore().config(); + + t.deepEqual(result, { + A: '1', + B: '2', + C: '3', + D: '4', + }); + + t.is(mock.count(), 4); + + t.like(mock.call(0), { NextToken: undefined }); + t.like(mock.call(1), { NextToken: 'token-1' }); + t.like(mock.call(2), { NextToken: 'token-2' }); + t.like(mock.call(3), { Names: ['/a', '/b', '/c', '/d'] }); +}); + +test('formats configuration keys', async function (t) { + const { mock } = t.context; + + mock.on(DescribeParametersCommand).resolves({ + Parameters: [ + { Name: '/dev/api/url' }, + { Name: '/dev/api/arn' }, + { Name: '/dev/user-table/name' }, + ], + }); + + mock.on(GetParametersCommand).resolves({ + Parameters: [ + { Name: '/dev/api/url', Value: '1' }, + { Name: '/dev/api/arn', Value: '2' }, + { Name: '/dev/user-table/name', Value: '3' }, + ], + }); + + const result = await new SystemStore('/dev').config(); + + t.deepEqual(result, { + API_URL: '1', + API_ARN: '2', + USER_TABLE_NAME: '3', + }); +}); + +test('formats configuration keys with custom prefix', async function (t) { + const { mock } = t.context; + + mock.on(DescribeParametersCommand).resolves({ + Parameters: [ + { Name: '/dev/api/url' }, + { Name: '/dev/api/arn' }, + { Name: '/dev/user-table/name' }, + ], + }); + + mock.on(GetParametersCommand).resolves({ + Parameters: [ + { Name: '/dev/api/url', Value: '1' }, + { Name: '/dev/api/arn', Value: '2' }, + { Name: '/dev/user-table/name', Value: '3' }, + ], + }); + + const result = await new SystemStore('/dev').config('/onia'); + + t.deepEqual(result, { + ONIA_API_URL: '1', + ONIA_API_ARN: '2', + ONIA_USER_TABLE_NAME: '3', + }); +}); + +test('does not override existing environment variables', async function (t) { + const { mock } = t.context; + + mock.on(DescribeParametersCommand).resolves({ + Parameters: [{ Name: '/a' }, { Name: '/b' }], + }); + + mock.on(GetParametersCommand).resolves({ + Parameters: [ + { Name: '/a', Value: '1' }, + { Name: '/b', Value: '2' }, + ], + }); + + delete process.env.A; + delete process.env.B; + + process.env.B = 'X'; + + const result = await new SystemStore().config(); + + t.deepEqual(result, { + A: '1', + B: '2', + }); + + t.like(process.env, { + A: '1', + B: 'X', + }); +}); + +test('ignores parameters with empty names', async function (t) { + const { mock } = t.context; + + mock.on(DescribeParametersCommand).resolves({ + Parameters: [{ Name: '/x' }], + }); + + mock.on(GetParametersCommand).resolves({ + Parameters: [{ Value: 'x' }], + }); + + const result = await new SystemStore().config(); + + t.deepEqual(result, {}); +}); + +test('ignores parameters with empty values', async function (t) { + const { mock } = t.context; + + mock.on(DescribeParametersCommand).resolves({ + Parameters: [{ Name: '/x' }], + }); + + mock.on(GetParametersCommand).resolves({ + Parameters: [{ Name: '/x' }], + }); + + const result = await new SystemStore().config(); + + t.deepEqual(result, {}); +});