From e2aaa835d5d819a79e76ecde93019b0cd76c8872 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Tue, 4 Jul 2023 12:52:14 +0200 Subject: [PATCH 1/5] add user agent to appconfig parameters --- .../src/appconfig/AppConfigProvider.ts | 3 +++ .../tests/unit/AppConfigProvider.test.ts | 25 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/parameters/src/appconfig/AppConfigProvider.ts b/packages/parameters/src/appconfig/AppConfigProvider.ts index 637cc85723..46b80cdaaf 100644 --- a/packages/parameters/src/appconfig/AppConfigProvider.ts +++ b/packages/parameters/src/appconfig/AppConfigProvider.ts @@ -10,6 +10,7 @@ import type { AppConfigGetOptions, AppConfigGetOutput, } from '../types/AppConfigProvider'; +import { addUserAgentMiddleware } from '@aws-lambda-powertools/commons'; /** * ## Intro @@ -210,6 +211,8 @@ class AppConfigProvider extends BaseProvider { this.client = new AppConfigDataClient(options.clientConfig || {}); } + addUserAgentMiddleware(this.client, 'parameters'); + this.application = options?.application || this.envVarsService.getServiceName(); if (!this.application || this.application.trim().length === 0) { diff --git a/packages/parameters/tests/unit/AppConfigProvider.test.ts b/packages/parameters/tests/unit/AppConfigProvider.test.ts index a7cfa50559..a333544eb8 100644 --- a/packages/parameters/tests/unit/AppConfigProvider.test.ts +++ b/packages/parameters/tests/unit/AppConfigProvider.test.ts @@ -8,11 +8,12 @@ import { ExpirableValue } from '../../src/base/ExpirableValue'; import { AppConfigProviderOptions } from '../../src/types/AppConfigProvider'; import { AppConfigDataClient, - StartConfigurationSessionCommand, GetLatestConfigurationCommand, + StartConfigurationSessionCommand, } from '@aws-sdk/client-appconfigdata'; import { Uint8ArrayBlobAdapter } from '@aws-sdk/util-stream'; import { mockClient } from 'aws-sdk-client-mock'; +import * as UserAgentMiddleware from '@aws-lambda-powertools/commons/lib/userAgentMiddleware'; import 'aws-sdk-client-mock-jest'; describe('Class: AppConfigProvider', () => { @@ -34,6 +35,11 @@ describe('Class: AppConfigProvider', () => { environment: 'MyAppProdEnv', }; + const userAgentSpy = jest.spyOn( + UserAgentMiddleware, + 'addUserAgentMiddleware' + ); + // Act const provider = new AppConfigProvider(options); @@ -43,6 +49,8 @@ describe('Class: AppConfigProvider', () => { serviceId: 'AppConfigData', }) ); + + expect(userAgentSpy).toHaveBeenCalled(); }); test('when the user provides a client config in the options, the class instantiates a new client with client config options', async () => { @@ -55,6 +63,11 @@ describe('Class: AppConfigProvider', () => { }, }; + const userAgentSpy = jest.spyOn( + UserAgentMiddleware, + 'addUserAgentMiddleware' + ); + // Act const provider = new AppConfigProvider(options); @@ -64,6 +77,8 @@ describe('Class: AppConfigProvider', () => { serviceId: 'with-client-config', }) ); + + expect(userAgentSpy).toHaveBeenCalled(); }); test('when the user provides an SDK client in the options, the class instantiates with it', async () => { @@ -78,6 +93,11 @@ describe('Class: AppConfigProvider', () => { awsSdkV3Client: awsSdkV3Client, }; + const userAgentSpy = jest.spyOn( + UserAgentMiddleware, + 'addUserAgentMiddleware' + ); + // Act const provider = new AppConfigProvider(options); @@ -87,6 +107,8 @@ describe('Class: AppConfigProvider', () => { serviceId: 'with-custom-sdk-client', }) ); + + expect(userAgentSpy).toHaveBeenCalledWith(awsSdkV3Client, 'parameters'); }); test('when the user provides NOT an SDK client in the options, it throws an error', async () => { @@ -187,6 +209,7 @@ describe('Class: AppConfigProvider', () => { public _addToStore(key: string, value: string): void { this.configurationTokenStore.set(key, value); } + public _storeHas(key: string): boolean { return this.configurationTokenStore.has(key); } From 58172433ef29e2efb97811cc2733dc4d3ef1d8e4 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Tue, 4 Jul 2023 12:55:45 +0200 Subject: [PATCH 2/5] add user agent to dynamodb provider --- .../src/dynamodb/DynamoDBProvider.ts | 3 +++ .../tests/unit/DynamoDBProvider.test.ts | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/parameters/src/dynamodb/DynamoDBProvider.ts b/packages/parameters/src/dynamodb/DynamoDBProvider.ts index 3aa9f51424..e95afb3142 100644 --- a/packages/parameters/src/dynamodb/DynamoDBProvider.ts +++ b/packages/parameters/src/dynamodb/DynamoDBProvider.ts @@ -18,6 +18,7 @@ import type { } from '@aws-sdk/client-dynamodb'; import type { PaginationConfiguration } from '@aws-sdk/types'; import type { JSONValue } from '@aws-lambda-powertools/commons'; +import { addUserAgentMiddleware } from '@aws-lambda-powertools/commons'; /** * ## Intro @@ -266,6 +267,8 @@ class DynamoDBProvider extends BaseProvider { this.client = new DynamoDBClient(clientConfig); } + addUserAgentMiddleware(this.client, 'parameters'); + this.tableName = config.tableName; if (config.keyAttr) this.keyAttr = config.keyAttr; if (config.sortAttr) this.sortAttr = config.sortAttr; diff --git a/packages/parameters/tests/unit/DynamoDBProvider.test.ts b/packages/parameters/tests/unit/DynamoDBProvider.test.ts index 43f011ed28..41c7c044fd 100644 --- a/packages/parameters/tests/unit/DynamoDBProvider.test.ts +++ b/packages/parameters/tests/unit/DynamoDBProvider.test.ts @@ -15,6 +15,7 @@ import type { } from '@aws-sdk/client-dynamodb'; import type { DynamoDBProviderOptions } from '../../src/types/DynamoDBProvider'; import { marshall } from '@aws-sdk/util-dynamodb'; +import * as UserAgentMiddleware from '@aws-lambda-powertools/commons/lib/userAgentMiddleware'; import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; @@ -30,6 +31,11 @@ describe('Class: DynamoDBProvider', () => { tableName: 'test-table', }; + const userAgentSpy = jest.spyOn( + UserAgentMiddleware, + 'addUserAgentMiddleware' + ); + // Act const provider = new DynamoDBProvider(options); @@ -39,6 +45,8 @@ describe('Class: DynamoDBProvider', () => { serviceId: 'DynamoDB', }) ); + + expect(userAgentSpy).toHaveBeenCalled(); }); test('when the user provides a client config in the options, the class instantiates a new client with client config options', async () => { @@ -50,6 +58,11 @@ describe('Class: DynamoDBProvider', () => { }, }; + const userAgentSpy = jest.spyOn( + UserAgentMiddleware, + 'addUserAgentMiddleware' + ); + // Act const provider = new DynamoDBProvider(options); @@ -59,6 +72,8 @@ describe('Class: DynamoDBProvider', () => { serviceId: 'with-client-config', }) ); + + expect(userAgentSpy).toHaveBeenCalled(); }); test('when the user provides an SDK client in the options, the class instantiates with it', async () => { @@ -67,6 +82,11 @@ describe('Class: DynamoDBProvider', () => { serviceId: 'with-custom-sdk-client', }); + const userAgentSpy = jest.spyOn( + UserAgentMiddleware, + 'addUserAgentMiddleware' + ); + const options: DynamoDBProviderOptions = { tableName: 'test-table', awsSdkV3Client: awsSdkV3Client, @@ -81,6 +101,7 @@ describe('Class: DynamoDBProvider', () => { serviceId: 'with-custom-sdk-client', }) ); + expect(userAgentSpy).toHaveBeenCalledWith(awsSdkV3Client, 'parameters'); }); test('when the user provides NOT an SDK client in the options, it throws an error', async () => { From b7a3c010861bfe13737db5b7dbe222fdf4703214 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Tue, 4 Jul 2023 13:02:45 +0200 Subject: [PATCH 3/5] add user agent to secrets provider --- .../parameters/src/secrets/SecretsProvider.ts | 3 +++ .../tests/unit/SecretsProvider.test.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/parameters/src/secrets/SecretsProvider.ts b/packages/parameters/src/secrets/SecretsProvider.ts index defc552664..6f82e1615e 100644 --- a/packages/parameters/src/secrets/SecretsProvider.ts +++ b/packages/parameters/src/secrets/SecretsProvider.ts @@ -9,6 +9,7 @@ import type { SecretsGetOptions, SecretsGetOutput, } from '../types/SecretsProvider'; +import { addUserAgentMiddleware } from '@aws-lambda-powertools/commons'; /** * ## Intro @@ -171,6 +172,8 @@ class SecretsProvider extends BaseProvider { const clientConfig = config?.clientConfig || {}; this.client = new SecretsManagerClient(clientConfig); } + + addUserAgentMiddleware(this.client, 'parameters'); } /** diff --git a/packages/parameters/tests/unit/SecretsProvider.test.ts b/packages/parameters/tests/unit/SecretsProvider.test.ts index 7eac0c8cf5..f3238afcfc 100644 --- a/packages/parameters/tests/unit/SecretsProvider.test.ts +++ b/packages/parameters/tests/unit/SecretsProvider.test.ts @@ -12,6 +12,7 @@ import type { GetSecretValueCommandInput } from '@aws-sdk/client-secrets-manager import type { SecretsProviderOptions } from '../../src/types/SecretsProvider'; import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; +import * as UserAgentMiddleware from '@aws-lambda-powertools/commons/lib/userAgentMiddleware'; const encoder = new TextEncoder(); @@ -27,6 +28,11 @@ describe('Class: SecretsProvider', () => { // Prepare const options: SecretsProviderOptions = {}; + const userAgentSpy = jest.spyOn( + UserAgentMiddleware, + 'addUserAgentMiddleware' + ); + // Act const provider = new SecretsProvider(options); @@ -36,6 +42,7 @@ describe('Class: SecretsProvider', () => { serviceId: 'Secrets Manager', }) ); + expect(userAgentSpy).toHaveBeenCalled(); }); test('when the user provides a client config in the options, the class instantiates a new client with client config options', async () => { @@ -46,6 +53,11 @@ describe('Class: SecretsProvider', () => { }, }; + const userAgentSpy = jest.spyOn( + UserAgentMiddleware, + 'addUserAgentMiddleware' + ); + // Act const provider = new SecretsProvider(options); @@ -55,6 +67,7 @@ describe('Class: SecretsProvider', () => { serviceId: 'with-client-config', }) ); + expect(userAgentSpy).toHaveBeenCalled(); }); test('when the user provides an SDK client in the options, the class instantiates with it', async () => { @@ -67,6 +80,11 @@ describe('Class: SecretsProvider', () => { awsSdkV3Client: awsSdkV3Client, }; + const userAgentSpy = jest.spyOn( + UserAgentMiddleware, + 'addUserAgentMiddleware' + ); + // Act const provider = new SecretsProvider(options); @@ -76,6 +94,7 @@ describe('Class: SecretsProvider', () => { serviceId: 'with-custom-sdk-client', }) ); + expect(userAgentSpy).toHaveBeenCalledWith(awsSdkV3Client, 'parameters'); }); test('when the user provides NOT an SDK client in the options, it throws an error', async () => { From d10969d2d2bc3bfded19c64bdc811de0339a6e11 Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Tue, 4 Jul 2023 13:13:47 +0200 Subject: [PATCH 4/5] add user agent to ssm provider --- packages/parameters/src/ssm/SSMProvider.ts | 3 +++ .../parameters/tests/unit/SSMProvider.test.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/parameters/src/ssm/SSMProvider.ts b/packages/parameters/src/ssm/SSMProvider.ts index 4ffbb1b6ee..1a2f1c5ed5 100644 --- a/packages/parameters/src/ssm/SSMProvider.ts +++ b/packages/parameters/src/ssm/SSMProvider.ts @@ -27,6 +27,7 @@ import type { SSMGetParametersByNameFromCacheOutputType, } from '../types/SSMProvider'; import type { PaginationConfiguration } from '@aws-sdk/types'; +import { addUserAgentMiddleware } from '@aws-lambda-powertools/commons'; /** * ## Intro @@ -290,6 +291,8 @@ class SSMProvider extends BaseProvider { const clientConfig = config?.clientConfig || {}; this.client = new SSMClient(clientConfig); } + + addUserAgentMiddleware(this.client, 'parameters'); } /** diff --git a/packages/parameters/tests/unit/SSMProvider.test.ts b/packages/parameters/tests/unit/SSMProvider.test.ts index 8cec3e4de2..138f291156 100644 --- a/packages/parameters/tests/unit/SSMProvider.test.ts +++ b/packages/parameters/tests/unit/SSMProvider.test.ts @@ -22,6 +22,7 @@ import type { } from '../../src/types/SSMProvider'; import { ExpirableValue } from '../../src/base/ExpirableValue'; import { toBase64 } from '@aws-sdk/util-base64'; +import * as UserAgentMiddleware from '@aws-lambda-powertools/commons/lib/userAgentMiddleware'; const encoder = new TextEncoder(); @@ -37,6 +38,10 @@ describe('Class: SSMProvider', () => { test('when the class instantiates without SDK client and client config it has default options', async () => { // Prepare const options: SSMProviderOptions = {}; + const userAgentSpy = jest.spyOn( + UserAgentMiddleware, + 'addUserAgentMiddleware' + ); // Act const provider = new SSMProvider(options); @@ -47,6 +52,7 @@ describe('Class: SSMProvider', () => { serviceId: 'SSM', }) ); + expect(userAgentSpy).toHaveBeenCalled(); }); test('when the user provides a client config in the options, the class instantiates a new client with client config options', async () => { @@ -56,6 +62,10 @@ describe('Class: SSMProvider', () => { serviceId: 'with-client-config', }, }; + const userAgentSpy = jest.spyOn( + UserAgentMiddleware, + 'addUserAgentMiddleware' + ); // Act const provider = new SSMProvider(options); @@ -66,6 +76,7 @@ describe('Class: SSMProvider', () => { serviceId: 'with-client-config', }) ); + expect(userAgentSpy).toHaveBeenCalled(); }); test('when the user provides an SDK client in the options, the class instantiates with it', async () => { @@ -77,6 +88,10 @@ describe('Class: SSMProvider', () => { const options: SSMProviderOptions = { awsSdkV3Client: awsSdkV3Client, }; + const userAgentSpy = jest.spyOn( + UserAgentMiddleware, + 'addUserAgentMiddleware' + ); // Act const provider = new SSMProvider(options); @@ -87,6 +102,7 @@ describe('Class: SSMProvider', () => { serviceId: 'with-custom-sdk-client', }) ); + expect(userAgentSpy).toHaveBeenCalledWith(awsSdkV3Client, 'parameters'); }); test('when the user provides NOT an SDK client in the options, it throws an error', async () => { From c5ac696caf49b8028dc9f2092957f4173b75ebac Mon Sep 17 00:00:00 2001 From: Alexander Melnyk Date: Wed, 5 Jul 2023 15:50:03 +0200 Subject: [PATCH 5/5] add skip step for multiple call operations --- packages/commons/src/userAgentMiddleware.ts | 7 + .../tests/unit/userAgentMiddleware.test.ts | 121 +++++++++++++----- 2 files changed, 94 insertions(+), 34 deletions(-) diff --git a/packages/commons/src/userAgentMiddleware.ts b/packages/commons/src/userAgentMiddleware.ts index 2f66661ef8..24b278f7b4 100644 --- a/packages/commons/src/userAgentMiddleware.ts +++ b/packages/commons/src/userAgentMiddleware.ts @@ -33,6 +33,13 @@ const customUserAgentMiddleware = (feature: string) => { // @ts-ignore const addUserAgentMiddleware = (client, feature: string): void => { try { + if ( + client.middlewareStack + .identify() + .includes('addPowertoolsToUserAgent: POWERTOOLS,USER_AGENT') + ) { + return; + } client.middlewareStack.addRelativeTo( customUserAgentMiddleware(feature), middlewareOptions diff --git a/packages/commons/tests/unit/userAgentMiddleware.test.ts b/packages/commons/tests/unit/userAgentMiddleware.test.ts index da71ecd8a5..3a7f4538ed 100644 --- a/packages/commons/tests/unit/userAgentMiddleware.test.ts +++ b/packages/commons/tests/unit/userAgentMiddleware.test.ts @@ -3,12 +3,26 @@ import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { ScanCommand } from '@aws-sdk/lib-dynamodb'; import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm'; -import { version as PT_VERSION } from '../../package.json'; +import { PT_VERSION } from '../../src/version'; import { AppConfigDataClient } from '@aws-sdk/client-appconfigdata'; import { GetSecretValueCommand, SecretsManagerClient, } from '@aws-sdk/client-secrets-manager'; +import { RelativeMiddlewareOptions } from '@aws-sdk/types/dist-types/middleware'; + +type SupportedSdkClients = + | LambdaClient + | DynamoDBClient + | SSMClient + | SecretsManagerClient + | AppConfigDataClient; + +type SupportedSdkCommands = + | InvokeCommand + | ScanCommand + | GetParameterCommand + | GetSecretValueCommand; const options = { region: 'us-east-1', @@ -20,6 +34,57 @@ const options = { }, }; +const assertMiddleware = (feature: string) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return (next) => (args) => { + const userAgent = args?.request?.headers['user-agent']; + expect(userAgent).toContain(`PT/${feature}/${PT_VERSION} PTEnv/NA`); + // make sure it's at the end of the user agent + expect( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + userAgent + ?.split(' ') + .slice(userAgent?.split(' ').length - 2) // take the last to entries of the user-agent header + .join(' ') + ).toEqual(`PT/${feature}/${PT_VERSION} PTEnv/NA`); + + return next(args); + }; +}; + +const assertMiddlewareOptions: RelativeMiddlewareOptions = { + relation: 'after', + toMiddleware: 'addPowertoolsToUserAgent', + name: 'testUserAgentHeader', + tags: ['TEST'], +}; + +const runCommand = async ( + client: SupportedSdkClients, + command: SupportedSdkCommands +): Promise => { + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return await client.send(command); + } catch (e) { + // throw only jest errors and swallow the SDK client errors like credentials or connection issues + if ( + e instanceof Error && + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + e.matcherResult !== undefined && + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + e.matcherResult.pass === false + ) { + throw e; + } + } +}; + describe('Given a client of instance: ', () => { it.each([ { @@ -57,44 +122,15 @@ describe('Given a client of instance: ', () => { ); client.middlewareStack.addRelativeTo( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - (next) => (args) => { - const userAgent = args?.request?.headers['user-agent']; - expect(userAgent).toContain(`PT/my-feature/${PT_VERSION} PTEnv/NA`); - // make sure it's at the end of the user agent - expect( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - userAgent - ?.split(' ') - .slice(userAgent?.split(' ').length - 2) // take the last to entries of the user-agent header - .join(' ') - ).toEqual(`PT/my-feature/${PT_VERSION} PTEnv/NA`); - - return next(args); - }, - { - relation: 'after', - toMiddleware: 'addPowertoolsToUserAgent', - name: 'testUserAgentHeader', - tags: ['TEST'], - } + assertMiddleware('my-feature'), + assertMiddlewareOptions ); - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - await client.send(command); - } catch (e) { - if (e instanceof Error && e.name === 'JestAssertionError') { - throw e; - } - } + await runCommand(client, command); } ); - it('should not throw erro, when client fails to add middleware', () => { + it('should not throw error, when client fails to add middleware', () => { // create mock client that throws error when adding middleware const client = { middlewareStack: { @@ -106,4 +142,21 @@ describe('Given a client of instance: ', () => { expect(() => addUserAgentMiddleware(client, 'my-feature')).not.toThrow(); }); + + it('should no-op if we add the middleware twice', async () => { + const client = new LambdaClient(options); + const command = new InvokeCommand({ FunctionName: 'test', Payload: '' }); + addUserAgentMiddleware(client, 'my-feature'); + addUserAgentMiddleware(client, 'your-feature'); + + client.middlewareStack.addRelativeTo( + assertMiddleware('my-feature'), + assertMiddlewareOptions + ); + await runCommand(client, command); + + expect(client.middlewareStack.identify()).toContain( + 'addPowertoolsToUserAgent: POWERTOOLS,USER_AGENT' + ); + }); });