From f3f5cac4d1836b9089a8603285a0d319770ea73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 6 Mar 2026 13:54:19 +0100 Subject: [PATCH] fix: update DefaultAwsCredentialsManager tests for new AWS SDK internals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AWS SDK now uses @aws-sdk/nested-clients/sts internally in fromTemporaryCredentials, so the tests need to mock both the regular and nested STS clients. Also adapts to the new $source metadata on credentials and the new @smithy/shared-ini-file-loader for ini profile mocking. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Fredrik Adelöw --- packages/integration-aws-node/package.json | 1 + .../src/DefaultAwsCredentialsManager.test.ts | 138 +++++++++++------- yarn.lock | 1 + 3 files changed, 86 insertions(+), 54 deletions(-) diff --git a/packages/integration-aws-node/package.json b/packages/integration-aws-node/package.json index 6070a3eea0..0f71c487ef 100644 --- a/packages/integration-aws-node/package.json +++ b/packages/integration-aws-node/package.json @@ -48,6 +48,7 @@ "@backstage/cli": "workspace:^", "@backstage/config-loader": "workspace:^", "@backstage/test-utils": "workspace:^", + "@smithy/shared-ini-file-loader": "^4.4.6", "aws-sdk-client-mock": "^4.0.0", "aws-sdk-client-mock-jest": "^4.0.0" }, diff --git a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts index c21ea13e4c..60fa190ee9 100644 --- a/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts +++ b/packages/integration-aws-node/src/DefaultAwsCredentialsManager.test.ts @@ -22,15 +22,20 @@ import { GetCallerIdentityCommand, AssumeRoleCommand, } from '@aws-sdk/client-sts'; +// this is an internal package that ths sdk uses behind the scenes, and we need to mock parts of it +// eslint-disable-next-line @backstage/no-undeclared-imports +import { + STSClient as NestedSTSClient, + AssumeRoleCommand as NestedAssumeRoleCommand, +} from '@aws-sdk/nested-clients/sts'; import { Config, ConfigReader } from '@backstage/config'; -import { promises } from 'node:fs'; import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; const env = process.env; let stsMock: AwsClientStub; +let nestedStsMock: AwsClientStub; let config: Config; -jest.mock('fs', () => ({ promises: { readFile: jest.fn() } })); jest.mock('@aws-sdk/credential-providers', () => { const originalModule = jest.requireActual('@aws-sdk/credential-providers'); return { @@ -45,6 +50,7 @@ describe('DefaultAwsCredentialsManager', () => { jest.resetAllMocks(); stsMock = mockClient(STSClient); + nestedStsMock = mockClient(NestedSTSClient); config = new ConfigReader({ aws: { @@ -103,49 +109,59 @@ describe('DefaultAwsCredentialsManager', () => { Account: '123456789012', }; }); - stsMock - .on(AssumeRoleCommand, { - RoleArn: 'arn:aws:iam::111111111111:role/hello', - RoleSessionName: 'backstage', - ExternalId: 'world', - }) - .resolves({ - Credentials: { - AccessKeyId: 'ACCESS_KEY_ID_1', - SecretAccessKey: 'SECRET_ACCESS_KEY_1', - SessionToken: 'SESSION_TOKEN_1', - Expiration: new Date('2022-01-01'), + const assumeRoleResponses = { + 'arn:aws:iam::111111111111:role/hello': { + input: { + RoleArn: 'arn:aws:iam::111111111111:role/hello', + RoleSessionName: 'backstage', + ExternalId: 'world', }, - }); + output: { + Credentials: { + AccessKeyId: 'ACCESS_KEY_ID_1', + SecretAccessKey: 'SECRET_ACCESS_KEY_1', + SessionToken: 'SESSION_TOKEN_1', + Expiration: new Date('2022-01-01'), + }, + }, + }, + 'arn:aws-other:iam::222222222222:role/hi': { + input: { + RoleArn: 'arn:aws-other:iam::222222222222:role/hi', + RoleSessionName: 'backstage', + }, + output: { + Credentials: { + AccessKeyId: 'ACCESS_KEY_ID_2', + SecretAccessKey: 'SECRET_ACCESS_KEY_2', + SessionToken: 'SESSION_TOKEN_2', + Expiration: new Date('2022-01-02'), + }, + }, + }, + 'arn:aws:iam::999999999999:role/backstage-role': { + input: { + RoleArn: 'arn:aws:iam::999999999999:role/backstage-role', + RoleSessionName: 'backstage', + ExternalId: 'my-id', + }, + output: { + Credentials: { + AccessKeyId: 'ACCESS_KEY_ID_9', + SecretAccessKey: 'SECRET_ACCESS_KEY_9', + SessionToken: 'SESSION_TOKEN_9', + Expiration: new Date('2022-01-09'), + }, + }, + }, + }; - stsMock - .on(AssumeRoleCommand, { - RoleArn: 'arn:aws-other:iam::222222222222:role/hi', - RoleSessionName: 'backstage', - }) - .resolves({ - Credentials: { - AccessKeyId: 'ACCESS_KEY_ID_2', - SecretAccessKey: 'SECRET_ACCESS_KEY_2', - SessionToken: 'SESSION_TOKEN_2', - Expiration: new Date('2022-01-02'), - }, - }); - - stsMock - .on(AssumeRoleCommand, { - RoleArn: 'arn:aws:iam::999999999999:role/backstage-role', - RoleSessionName: 'backstage', - ExternalId: 'my-id', - }) - .resolves({ - Credentials: { - AccessKeyId: 'ACCESS_KEY_ID_9', - SecretAccessKey: 'SECRET_ACCESS_KEY_9', - SessionToken: 'SESSION_TOKEN_9', - Expiration: new Date('2022-01-09'), - }, - }); + // Mock AssumeRoleCommand on both the regular and nested STS clients. + // fromTemporaryCredentials internally uses @aws-sdk/nested-clients/sts. + for (const { input, output } of Object.values(assumeRoleResponses)) { + stsMock.on(AssumeRoleCommand, input).resolves(output); + nestedStsMock.on(NestedAssumeRoleCommand, input).resolves(output); + } const testDate = new Date('2022-01-10'); @@ -159,11 +175,22 @@ describe('DefaultAwsCredentialsManager', () => { jest.requireActual('@aws-sdk/credential-providers').fromNodeProviderChain, ); - const mockProfile = `[my-profile] - aws_access_key_id=ACCESS_KEY_ID_9 - aws_secret_access_key=SECRET_ACCESS_KEY_9 - `; - (promises.readFile as jest.Mock).mockResolvedValue(mockProfile); + // Inject mock credentials file via @smithy/shared-ini-file-loader's fileIntercept + const mockProfile = `[my-profile]\naws_access_key_id=ACCESS_KEY_ID_9\naws_secret_access_key=SECRET_ACCESS_KEY_9\n`; + const sharedIniReadFile = require('@smithy/shared-ini-file-loader/dist-cjs/readFile'); + // Clear cached file promises so re-reads pick up the intercept + for (const key of Object.keys(sharedIniReadFile.filePromises)) { + delete sharedIniReadFile.filePromises[key]; + } + for (const key of Object.keys(sharedIniReadFile.fileIntercept)) { + delete sharedIniReadFile.fileIntercept[key]; + } + // Set intercepts for both the credentials and config file paths + const homeDir = require('node:os').homedir(); + const credPath = `${homeDir}/.aws/credentials`; + const configPath = `${homeDir}/.aws/config`; + sharedIniReadFile.fileIntercept[credPath] = Promise.resolve(mockProfile); + sharedIniReadFile.fileIntercept[configPath] = Promise.resolve(''); }); afterEach(() => { @@ -192,7 +219,10 @@ describe('DefaultAwsCredentialsManager', () => { }); expect(awsCredentialProvider).toBe(awsCredentialProvider2); - expect(stsMock).toHaveReceivedCommandTimes(AssumeRoleCommand, 1); + expect(nestedStsMock).toHaveReceivedCommandTimes( + NestedAssumeRoleCommand, + 1, + ); }); it('retrieves assume-role creds in another partition for the given account ID', async () => { @@ -297,7 +327,7 @@ describe('DefaultAwsCredentialsManager', () => { expect(awsCredentialProvider.accountId).toEqual('555555555555'); const creds = await awsCredentialProvider.sdkCredentialProvider(); - expect(creds).toEqual({ + expect(creds).toMatchObject({ accessKeyId: 'ACCESS_KEY_ID_9', secretAccessKey: 'SECRET_ACCESS_KEY_9', }); @@ -312,7 +342,7 @@ describe('DefaultAwsCredentialsManager', () => { expect(awsCredentialProvider.accountId).toEqual('444444444444'); const creds = await awsCredentialProvider.sdkCredentialProvider(); - expect(creds).toEqual({ + expect(creds).toMatchObject({ accessKeyId: 'ACCESS_KEY_ID_10', secretAccessKey: 'SECRET_ACCESS_KEY_10', sessionToken: 'SESSION_TOKEN_10', @@ -336,7 +366,7 @@ describe('DefaultAwsCredentialsManager', () => { expect(awsCredentialProvider.accountId).toEqual('123456789012'); const creds = await awsCredentialProvider.sdkCredentialProvider(); - expect(creds).toEqual({ + expect(creds).toMatchObject({ accessKeyId: 'ACCESS_KEY_ID_9', secretAccessKey: 'SECRET_ACCESS_KEY_9', }); @@ -354,7 +384,7 @@ describe('DefaultAwsCredentialsManager', () => { expect(awsCredentialProvider.accountId).toEqual('123456789012'); const creds = await awsCredentialProvider.sdkCredentialProvider(); - expect(creds).toEqual({ + expect(creds).toMatchObject({ accessKeyId: 'ACCESS_KEY_ID_10', secretAccessKey: 'SECRET_ACCESS_KEY_10', sessionToken: 'SESSION_TOKEN_10', @@ -372,7 +402,7 @@ describe('DefaultAwsCredentialsManager', () => { expect(awsCredentialProvider.accountId).toEqual('123456789012'); const creds = await awsCredentialProvider.sdkCredentialProvider(); - expect(creds).toEqual({ + expect(creds).toMatchObject({ accessKeyId: 'ACCESS_KEY_ID_10', secretAccessKey: 'SECRET_ACCESS_KEY_10', sessionToken: 'SESSION_TOKEN_10', diff --git a/yarn.lock b/yarn.lock index ffed4a9193..4b6be16ac0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4429,6 +4429,7 @@ __metadata: "@backstage/config-loader": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/test-utils": "workspace:^" + "@smithy/shared-ini-file-loader": "npm:^4.4.6" aws-sdk-client-mock: "npm:^4.0.0" aws-sdk-client-mock-jest: "npm:^4.0.0" languageName: unknown