diff --git a/.projenrc.ts b/.projenrc.ts index aeced15..10c2d03 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -63,6 +63,7 @@ const project = new typescript.TypeScriptProject({ jestConfig: { verbose: true, maxWorkers: '50%', + randomize: true, }, configFilePath: 'jest.config.json', }, diff --git a/jest.config.json b/jest.config.json index 47c9b85..de497c8 100644 --- a/jest.config.json +++ b/jest.config.json @@ -2,6 +2,7 @@ "coverageProvider": "v8", "verbose": true, "maxWorkers": "50%", + "randomize": true, "testMatch": [ "/@(lib|test)/**/*(*.)@(spec|test).ts?(x)", "/@(lib|test)/**/__tests__/**/*.ts?(x)", diff --git a/lib/private/docker-credentials.ts b/lib/private/docker-credentials.ts index ad61288..41b3341 100644 --- a/lib/private/docker-credentials.ts +++ b/lib/private/docker-credentials.ts @@ -47,6 +47,13 @@ export function cdkCredentialsConfig(): DockerCredentialsConfig | undefined { return _cdkCredentials; } +/** + * Just for testing + */ +export function _clearCdkCredentialsConfigCache() { + _cdkCredentials = undefined; +} + /** Fetches login credentials from the configured source (e.g., SecretsManager, ECR) */ export async function fetchDockerLoginCredentials( aws: IAws, diff --git a/test/docker-images.test.ts b/test/docker-images.test.ts index 1a2181c..7cbd868 100644 --- a/test/docker-images.test.ts +++ b/test/docker-images.test.ts @@ -8,8 +8,9 @@ import { DescribeImagesCommand, DescribeRepositoriesCommand, GetAuthorizationTokenCommand, + GetAuthorizationTokenResponse, } from '@aws-sdk/client-ecr'; -import { MockAws, mockEcr } from './mock-aws'; +import { MockAws, mockEcr, resetDefaultAwsMockBehavior } from './mock-aws'; import { mockSpawn } from './mock-child_process'; import mockfs from './mock-fs'; import { AssetManifest, AssetPublishing, IAws } from '../lib'; @@ -23,6 +24,7 @@ err.name = 'ImageNotFoundException'; beforeEach(() => { jest.resetAllMocks(); + resetDefaultAwsMockBehavior(); delete process.env.CDK_DOCKER; // By default, assume no externally-configured credentials. @@ -78,6 +80,39 @@ beforeEach(() => { }, }, }), + '/multi2/cdk.out/assets.json': JSON.stringify({ + version: Manifest.version(), + dockerImages: { + theAsset1: { + source: { + directory: 'dockerdir', + }, + destinations: { + theDestination: { + region: 'us-north-50', + account: '12345', + assumeRoleArn: 'arn:aws:role', + repositoryName: 'repo', + imageTag: 'theAsset1', + }, + }, + }, + theAsset2: { + source: { + directory: 'dockerdir', + }, + destinations: { + theDestination: { + region: 'us-north-50', + account: '12346', + assumeRoleArn: 'arn:aws:role', + repositoryName: 'repo2', + imageTag: 'theAsset2', + }, + }, + }, + }, + }), '/external/cdk.out/assets.json': JSON.stringify({ version: Manifest.version(), dockerImages: { @@ -256,40 +291,42 @@ afterEach(() => { }); test('logging in twice for two repository domains (containing account id & region)', async () => { - const pub = new AssetPublishing(AssetManifest.fromPath(mockfs.path('/multi/cdk.out')), { + const pub = new AssetPublishing(AssetManifest.fromPath(mockfs.path('/multi2/cdk.out')), { aws, - throwOnError: false, + throwOnError: true, }); - mockEcr - .on(DescribeRepositoriesCommand) - .resolvesOnce({ - repositories: [{ repositoryUri: '12345.amazonaws.com/aws-cdk/assets' }], - }) - .resolvesOnce({ - repositories: [{ repositoryUri: '12346.amazonaws.com/aws-cdk/assets' }], - }) - .resolves({ - repositories: [ - { - repositoryName: 'repo', - repositoryUri: '12345.amazonaws.com/repo', - }, - ], - }); + mockEcr.on(DescribeRepositoriesCommand).callsFake((input) => { + const url = { + repo: '12345.amazonaws.com/repo', + repo2: '12346.amazonaws.com/repo2', + }[input.repositoryNames[0]]; + if (!url) { + throw new Error(`Unexpected repo: ${JSON.stringify(input)}`); + } + return { + repositories: [{ repositoryUri: url }], + }; + }); - mockEcr - .on(GetAuthorizationTokenCommand) - .resolvesOnce({ + const responses: GetAuthorizationTokenResponse[] = [ + { authorizationData: [ { authorizationToken: 'dXNlcjpwYXNz', proxyEndpoint: 'https://12345.proxy.com/' }, ], - }) - .resolvesOnce({ + }, + { authorizationData: [ { authorizationToken: 'dXNlcjpwYXNz', proxyEndpoint: 'https://12346.proxy.com/' }, ], - }); + }, + ]; + + let i = 0; + // For some reason, `mockResolvedValueOnce()` doesn't work here, but this does. + mockEcr.on(GetAuthorizationTokenCommand).callsFake(() => { + return responses[i++]; + }); const expectAllSpawns = mockSpawn( { @@ -305,17 +342,12 @@ test('logging in twice for two repository domains (containing account id & regio { commandLine: ['docker', 'inspect', 'cdkasset-theasset1'], exitCode: 1 }, { commandLine: ['docker', 'build', '--tag', 'cdkasset-theasset1', '.'], - cwd: '/multi/cdk.out/dockerdir', + cwd: 'multi2/cdk.out/dockerdir', }, { - commandLine: [ - 'docker', - 'tag', - 'cdkasset-theasset1', - '12345.amazonaws.com/aws-cdk/assets:theAsset1', - ], + commandLine: ['docker', 'tag', 'cdkasset-theasset1', '12345.amazonaws.com/repo:theAsset1'], }, - { commandLine: ['docker', 'push', '12345.amazonaws.com/aws-cdk/assets:theAsset1'] }, + { commandLine: ['docker', 'push', '12345.amazonaws.com/repo:theAsset1'] }, { commandLine: [ 'docker', @@ -329,17 +361,12 @@ test('logging in twice for two repository domains (containing account id & regio { commandLine: ['docker', 'inspect', 'cdkasset-theasset2'], exitCode: 1 }, { commandLine: ['docker', 'build', '--tag', 'cdkasset-theasset2', '.'], - cwd: '/multi/cdk.out/dockerdir', + cwd: 'multi2/cdk.out/dockerdir', }, { - commandLine: [ - 'docker', - 'tag', - 'cdkasset-theasset2', - '12346.amazonaws.com/aws-cdk/assets:theAsset2', - ], + commandLine: ['docker', 'tag', 'cdkasset-theasset2', '12346.amazonaws.com/repo2:theAsset2'], }, - { commandLine: ['docker', 'push', '12346.amazonaws.com/aws-cdk/assets:theAsset2'] } + { commandLine: ['docker', 'push', '12346.amazonaws.com/repo2:theAsset2'] } ); await pub.publish(); diff --git a/test/mock-aws.ts b/test/mock-aws.ts index 84d955d..30f5160 100644 --- a/test/mock-aws.ts +++ b/test/mock-aws.ts @@ -6,25 +6,26 @@ import { mockClient } from 'aws-sdk-client-mock'; import { Account, ClientOptions, DefaultAwsClient } from '../lib/aws'; export const mockEcr = mockClient(ECRClient); -mockEcr.on(DescribeRepositoriesCommand).resolves({ - repositories: [ - { - repositoryName: 'repo', - repositoryUri: '12345.amazonaws.com/repo', - }, - ], -}); -mockEcr.on(DescribeImagesCommand).resolves({}); - export const mockS3 = mockClient(S3Client); -mockS3.on(UploadPartCommand).resolves({ ETag: '1' }); -mockS3.on(PutObjectCommand).resolves({}); - export const mockSecretsManager = mockClient(SecretsManagerClient); export const mockSTS = mockClient(STSClient); -mockSTS - .on(GetCallerIdentityCommand) - .resolves({ Account: '123456789012', Arn: 'aws:swa:123456789012:some-other-stuff' }); + +export function resetDefaultAwsMockBehavior() { + mockEcr.on(DescribeRepositoriesCommand).resolves({ + repositories: [ + { + repositoryName: 'repo', + repositoryUri: '12345.amazonaws.com/repo', + }, + ], + }); + mockEcr.on(DescribeImagesCommand).resolves({}); + mockS3.on(UploadPartCommand).resolves({ ETag: '1' }); + mockS3.on(PutObjectCommand).resolves({}); + mockSTS + .on(GetCallerIdentityCommand) + .resolves({ Account: '123456789012', Arn: 'aws:swa:123456789012:some-other-stuff' }); +} export class MockAws extends DefaultAwsClient { discoverPartition(): Promise { diff --git a/test/placeholders.test.ts b/test/placeholders.test.ts index c519515..1d5f3cd 100644 --- a/test/placeholders.test.ts +++ b/test/placeholders.test.ts @@ -3,12 +3,13 @@ import 'aws-sdk-client-mock-jest'; import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import { DescribeImagesCommand } from '@aws-sdk/client-ecr'; import { ListObjectsV2Command } from '@aws-sdk/client-s3'; -import { MockAws, mockEcr, mockS3 } from './mock-aws'; +import { MockAws, mockEcr, mockS3, resetDefaultAwsMockBehavior } from './mock-aws'; import mockfs from './mock-fs'; import { AssetManifest, AssetPublishing, IAws } from '../lib'; let aws: IAws; beforeEach(() => { + resetDefaultAwsMockBehavior(); mockfs({ '/simple/cdk.out/assets.json': JSON.stringify({ version: Manifest.version(), diff --git a/test/private/docker-credentials.test.ts b/test/private/docker-credentials.test.ts index 3bc8c21..92aba60 100644 --- a/test/private/docker-credentials.test.ts +++ b/test/private/docker-credentials.test.ts @@ -1,9 +1,11 @@ +import * as crypto from 'crypto'; import * as os from 'os'; import * as path from 'path'; import { GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr'; import { GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; import { IAws } from '../../lib/aws'; import { + _clearCdkCredentialsConfigCache, cdkCredentialsConfig, cdkCredentialsConfigFile, DockerCredentialsConfig, @@ -18,6 +20,8 @@ let aws: IAws; beforeEach(() => { jest.resetModules(); jest.resetAllMocks(); + mockSecretWithSecretString({ username: 'secretUser', secret: 'secretPass' }); + _clearCdkCredentialsConfigCache(); aws = new MockAws(); @@ -45,8 +49,9 @@ describe('cdkCredentialsConfigFile', () => { }); describe('cdkCredentialsConfig', () => { - const credsFile = '/tmp/foo/bar/does/not/exist/config.json'; + let credsFile: string; beforeEach(() => { + credsFile = `/tmp/foo/bar/does/not/exist/config${crypto.randomUUID()}.json`; process.env.CDK_DOCKER_CREDS_FILE = mockfs.path(credsFile); }); @@ -118,8 +123,6 @@ describe('fetchDockerLoginCredentials', () => { }); test('does not throw on correctly configured raw domain', async () => { - mockSecretWithSecretString({ username: 'secretUser', secret: 'secretPass' }); - await expect( fetchDockerLoginCredentials(aws, config, 'https://secret.example.com/v1/') ).resolves.toBeTruthy(); @@ -139,7 +142,6 @@ describe('fetchDockerLoginCredentials', () => { }); test('supports assuming a role', async () => { - mockSecretWithSecretString({ username: 'secretUser', secret: 'secretPass' }); const secretsManagerClient = jest.spyOn(aws, 'secretsManagerClient'); const creds = await fetchDockerLoginCredentials(aws, config, 'secretwithrole.example.com');