From f9825a3cd60f4cf9036df35662d335fdca49d2cf Mon Sep 17 00:00:00 2001 From: Ghosh Date: Fri, 29 Oct 2021 20:24:03 -0700 Subject: [PATCH 1/8] test: unit tests for custom resources --- packages/amplify-category-custom/package.json | 1 + .../resources/cdk-stack.ts | 6 ++ .../src/__tests__/commands/add.test.ts | 39 +++++++++ .../src/__tests__/commands/build.test.ts | 32 +++++++ .../src/__tests__/commands/remove.test.ts | 38 +++++++++ .../src/__tests__/commands/update.test.ts | 57 +++++++++++++ .../utils/build-custom-resources.test.ts | 83 +++++++++++++++++++ .../utils/dependency-management-utils.test.ts | 0 .../walkthroughts/cdk-walkthrough.test.ts | 51 ++++++++++++ .../cloudformation-walkthrough.test.ts | 48 +++++++++++ .../src/utils/build-custom-resources.ts | 12 +-- .../src/utils/constants.ts | 2 +- .../src/utils/dependency-management-utils.ts | 3 +- .../src/utils/generate-cfn-from-cdk.ts | 14 ++++ .../src/walkthroughs/cdk-walkthrough.ts | 1 - .../cloudformation-walkthrough.ts | 5 +- .../ddb-stack-transform.test.ts | 6 +- .../cdk-stack-builder/ddb-stack-transform.ts | 2 +- 18 files changed, 381 insertions(+), 19 deletions(-) create mode 100644 packages/amplify-category-custom/src/__tests__/commands/add.test.ts create mode 100644 packages/amplify-category-custom/src/__tests__/commands/build.test.ts create mode 100644 packages/amplify-category-custom/src/__tests__/commands/remove.test.ts create mode 100644 packages/amplify-category-custom/src/__tests__/commands/update.test.ts create mode 100644 packages/amplify-category-custom/src/__tests__/utils/build-custom-resources.test.ts create mode 100644 packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts create mode 100644 packages/amplify-category-custom/src/__tests__/walkthroughts/cdk-walkthrough.test.ts create mode 100644 packages/amplify-category-custom/src/__tests__/walkthroughts/cloudformation-walkthrough.test.ts create mode 100644 packages/amplify-category-custom/src/utils/generate-cfn-from-cdk.ts diff --git a/packages/amplify-category-custom/package.json b/packages/amplify-category-custom/package.json index 2026d475a1c..1b8ab8406d9 100644 --- a/packages/amplify-category-custom/package.json +++ b/packages/amplify-category-custom/package.json @@ -13,6 +13,7 @@ "types": "lib/index.d.ts", "scripts": { "build": "tsc", + "test": "jest", "clean": "rimraf lib tsconfig.tsbuildinfo", "watch": "tsc -w" }, diff --git a/packages/amplify-category-custom/resources/cdk-stack.ts b/packages/amplify-category-custom/resources/cdk-stack.ts index c260d4c71be..44828168cd9 100644 --- a/packages/amplify-category-custom/resources/cdk-stack.ts +++ b/packages/amplify-category-custom/resources/cdk-stack.ts @@ -9,6 +9,12 @@ export class cdkStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); + /* Do not remove - Amplify CLI automatically injects the current deployment environment in this input paramater */ + new cdk.CfnParameter(this, 'env', { + type: 'String', + description: 'Current Amplify CLI env name', + }); + /* AWS CDK Code goes here - Learn more: https://docs.aws.amazon.com/cdk/latest/guide/home.html */ /* Example 1: Set up an SQS queue with an SNS topic diff --git a/packages/amplify-category-custom/src/__tests__/commands/add.test.ts b/packages/amplify-category-custom/src/__tests__/commands/add.test.ts new file mode 100644 index 00000000000..0e1c1314204 --- /dev/null +++ b/packages/amplify-category-custom/src/__tests__/commands/add.test.ts @@ -0,0 +1,39 @@ +import { $TSContext } from 'amplify-cli-core'; +import { run } from '../../commands/custom/add'; +import { customDeploymentOptionsQuestion } from '../../utils/common-questions'; +import { CDK_DEPLOYMENT_NAME, CFN_DEPLOYMENT_NAME } from '../../utils/constants'; +import { addCDKWalkthrough } from '../../walkthroughs/cdk-walkthrough'; +import { addCloudFormationWalkthrough } from '../../walkthroughs/cloudformation-walkthrough'; + +jest.mock('../../utils/common-questions'); +jest.mock('../../walkthroughs/cloudformation-walkthrough'); +jest.mock('../../walkthroughs/cdk-walkthrough'); + +const addCloudFormationWalkthrough_mock = addCloudFormationWalkthrough as jest.MockedFunction; +const addCDKWalkthrough_mock = addCDKWalkthrough as jest.MockedFunction; +const customDeploymentOptionsQuestion_mock = customDeploymentOptionsQuestion as jest.MockedFunction; + +describe('add custom flow', () => { + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: {}, + } as unknown as $TSContext; + }); + + it('add custom workflow is invoked for CDK', async () => { + customDeploymentOptionsQuestion_mock.mockResolvedValueOnce(CDK_DEPLOYMENT_NAME); + + await run(mockContext); + expect(addCDKWalkthrough_mock).toHaveBeenCalledTimes(1); + }); + + it('add custom workflow is invoked for CFN', async () => { + customDeploymentOptionsQuestion_mock.mockResolvedValueOnce(CFN_DEPLOYMENT_NAME); + + await run(mockContext); + expect(addCloudFormationWalkthrough_mock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/amplify-category-custom/src/__tests__/commands/build.test.ts b/packages/amplify-category-custom/src/__tests__/commands/build.test.ts new file mode 100644 index 00000000000..172d98e40a1 --- /dev/null +++ b/packages/amplify-category-custom/src/__tests__/commands/build.test.ts @@ -0,0 +1,32 @@ +import { $TSContext } from 'amplify-cli-core'; +import { run } from '../../commands/custom/build'; +import { buildCustomResources } from '../../utils/build-custom-resources'; + +jest.mock('../../utils/build-custom-resources'); +const buildCustomResources_mock = buildCustomResources as jest.MockedFunction; + +describe('build custom resources flow', () => { + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: {}, + parameters: {}, + } as unknown as $TSContext; + }); + + it('build all custom resources', async () => { + await run(mockContext); + + expect(buildCustomResources_mock).toHaveBeenCalledWith(mockContext, undefined); + }); + + it('build one custom resource', async () => { + const mockResourceName = 'mockresourcename'; + mockContext.parameters.first = mockResourceName; + + await run(mockContext); + expect(buildCustomResources_mock).toHaveBeenCalledWith(mockContext, mockResourceName); + }); +}); diff --git a/packages/amplify-category-custom/src/__tests__/commands/remove.test.ts b/packages/amplify-category-custom/src/__tests__/commands/remove.test.ts new file mode 100644 index 00000000000..1f7f1e4a154 --- /dev/null +++ b/packages/amplify-category-custom/src/__tests__/commands/remove.test.ts @@ -0,0 +1,38 @@ +import { $TSContext } from 'amplify-cli-core'; +import { run } from '../../commands/custom/remove'; + +jest.mock('amplify-cli-core'); + +describe('remove custom resource command tests', () => { + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: {}, + parameters: {}, + } as unknown as $TSContext; + }); + + it('remove resource workflow is invoked for custom resources with no params', async () => { + mockContext.amplify.removeResource = jest.fn().mockImplementation(async () => { + return; + }); + + await run(mockContext); + + expect(mockContext.amplify.removeResource).toHaveBeenCalledWith(mockContext, 'custom', undefined); + }); + + it('remove resource workflow is invoked for custom resource with params as resourceName', async () => { + const mockResourceName = 'mockResourceName'; + mockContext.parameters.first = mockResourceName; + mockContext.amplify.removeResource = jest.fn().mockImplementation(async () => { + return; + }); + + await run(mockContext); + + expect(mockContext.amplify.removeResource).toHaveBeenCalledWith(mockContext, 'custom', mockResourceName); + }); +}); diff --git a/packages/amplify-category-custom/src/__tests__/commands/update.test.ts b/packages/amplify-category-custom/src/__tests__/commands/update.test.ts new file mode 100644 index 00000000000..7c006441896 --- /dev/null +++ b/packages/amplify-category-custom/src/__tests__/commands/update.test.ts @@ -0,0 +1,57 @@ +import { $TSContext, pathManager, stateManager } from 'amplify-cli-core'; +import { prompter } from 'amplify-prompts'; +import { run } from '../../commands/custom/update'; +import { CDK_SERVICE_NAME, CFN_SERVICE_NAME } from '../../utils/constants'; +import { updateCloudFormationWalkthrough } from '../../walkthroughs/cloudformation-walkthrough'; + +jest.mock('../../walkthroughs/cloudformation-walkthrough'); +jest.mock('amplify-cli-core'); +jest.mock('amplify-prompts'); + +let mockAmplifyMeta = { + custom: { + mockcdkresourcename: { + service: CDK_SERVICE_NAME, + providerPlugin: 'awscloudformation', + }, + mockcfnresourcename: { + service: CFN_SERVICE_NAME, + providerPlugin: 'awscloudformation', + }, + }, +}; + +stateManager.getMeta = jest.fn().mockReturnValue(mockAmplifyMeta); +pathManager.getBackendDirPath = jest.fn().mockReturnValue('mockTargetDir'); + +const updateCloudFormationWalkthrough_mock = updateCloudFormationWalkthrough as jest.MockedFunction; + +describe('update custom flow', () => { + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: { + openEditor: jest.fn(), + }, + } as unknown as $TSContext; + }); + + it('update custom workflow is invoked for a CFN resource', async () => { + prompter.pick = jest.fn().mockReturnValueOnce('mockcfnresourcename'); + + await run(mockContext); + expect(updateCloudFormationWalkthrough_mock).toHaveBeenCalledWith(mockContext, 'mockcfnresourcename'); + }); + + it('update custom workflow is invoked for a CDK resource', async () => { + prompter.pick = jest.fn().mockReturnValueOnce('mockcdkresourcename'); + + prompter.yesOrNo = jest.fn().mockReturnValueOnce(true); + + await run(mockContext); + + expect(mockContext.amplify.openEditor).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/amplify-category-custom/src/__tests__/utils/build-custom-resources.test.ts b/packages/amplify-category-custom/src/__tests__/utils/build-custom-resources.test.ts new file mode 100644 index 00000000000..0e8aacd3ad4 --- /dev/null +++ b/packages/amplify-category-custom/src/__tests__/utils/build-custom-resources.test.ts @@ -0,0 +1,83 @@ +import { $TSContext, JSONUtilities, pathManager, getPackageManager } from 'amplify-cli-core'; +import * as fs from 'fs-extra'; +import execa from 'execa'; +import ora from 'ora'; +import { getAllResources } from '../../utils/dependency-management-utils'; +import { generateCloudFormationFromCDK } from '../../utils/generate-cfn-from-cdk'; +import { buildCustomResources } from '../../utils/build-custom-resources'; + +jest.mock('amplify-cli-core'); +jest.mock('amplify-prompts'); +jest.mock('../../utils/dependency-management-utils'); +jest.mock('../../utils/generate-cfn-from-cdk'); +jest.mock('execa'); +jest.mock('ora'); + +jest.mock('fs-extra', () => ({ + readFileSync: jest.fn().mockReturnValue('mockCode'), + existsSync: jest.fn().mockReturnValue(true), + ensureDirSync: jest.fn().mockReturnValue(true), + writeFileSync: jest.fn().mockReturnValue(true), +})); + +jest.mock('ora', () => { + return () => ({ + start: jest.fn(), + fail: jest.fn(), + succeed: jest.fn(), + stop: jest.fn(), + }); +}); + +jest.mock('../../utils/dependency-management-utils', () => ({ + getAllResources: jest.fn().mockResolvedValue({ mockedvalue: 'mockedkey' }), +})); + +jest.mock('../../utils/generate-cfn-from-cdk', () => ({ + generateCloudFormationFromCDK: jest.fn(), +})); + +jest.mock('amplify-cli-core', () => ({ + getPackageManager: jest.fn().mockResolvedValue('npm'), + pathManager: { + getBackendDirPath: jest.fn().mockReturnValue('mockTargetDir'), + }, + JSONUtilities: { + writeJson: jest.fn(), + readJson: jest.fn(), + }, +})); + +describe('build custom resources scenarios', () => { + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: { + openEditor: jest.fn(), + updateamplifyMetaAfterResourceAdd: jest.fn(), + copyBatch: jest.fn(), + getResourceStatus: jest.fn().mockResolvedValue({ + allResources: [ + { + resourceName: 'mockresource1', + service: 'customCDK', + }, + { + resourceName: 'mockresource2', + service: 'customCDK', + }, + ], + }), + }, + } as unknown as $TSContext; + }); + + it('build all resources', async () => { + await buildCustomResources(mockContext); + + // 2 for npm install and 2 for tsc build (1 per resource) + expect(execa.sync).toBeCalledTimes(4); + }); +}); diff --git a/packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts b/packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/amplify-category-custom/src/__tests__/walkthroughts/cdk-walkthrough.test.ts b/packages/amplify-category-custom/src/__tests__/walkthroughts/cdk-walkthrough.test.ts new file mode 100644 index 00000000000..3ff00191b0c --- /dev/null +++ b/packages/amplify-category-custom/src/__tests__/walkthroughts/cdk-walkthrough.test.ts @@ -0,0 +1,51 @@ +import { $TSContext, JSONUtilities, pathManager } from 'amplify-cli-core'; +import { prompter } from 'amplify-prompts'; +import * as fs from 'fs-extra'; +import { buildCustomResources } from '../../utils/build-custom-resources'; +import { customResourceNameQuestion } from '../../utils/common-questions'; +import { addCDKWalkthrough } from '../../walkthroughs/cdk-walkthrough'; + +jest.mock('../../utils/common-questions'); +jest.mock('../../utils/build-custom-resources'); + +jest.mock('amplify-cli-core'); +jest.mock('amplify-prompts'); + +jest.mock('fs-extra', () => ({ + readFileSync: jest.fn().mockReturnValue('mockCode'), + existsSync: jest.fn().mockReturnValue(false), + ensureDirSync: jest.fn().mockReturnValue(true), + writeFileSync: jest.fn().mockReturnValue(true), +})); + +pathManager.getBackendDirPath = jest.fn().mockReturnValue('mockTargetDir'); +(JSONUtilities.writeJson = jest.fn()), (JSONUtilities.readJson = jest.fn()); + +const buildCustomResources_mock = buildCustomResources as jest.MockedFunction; +let customResourceNameQuestion_mock = customResourceNameQuestion as jest.MockedFunction; +customResourceNameQuestion_mock.mockResolvedValue('customresoourcename'); + +describe('addCDKWalkthrough scenarios', () => { + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: { + openEditor: jest.fn(), + updateamplifyMetaAfterResourceAdd: jest.fn(), + }, + } as unknown as $TSContext; + }); + + it('successfully goes through cdk update walkthrough', async () => { + prompter.yesOrNo = jest.fn().mockReturnValueOnce(true); + + await addCDKWalkthrough(mockContext); + + expect(buildCustomResources_mock).toHaveBeenCalledWith(mockContext, 'customresoourcename'); + expect(mockContext.amplify.openEditor).toHaveBeenCalledTimes(1); + expect(mockContext.amplify.updateamplifyMetaAfterResourceAdd).toHaveBeenCalledTimes(1); + expect(fs.writeFileSync).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/amplify-category-custom/src/__tests__/walkthroughts/cloudformation-walkthrough.test.ts b/packages/amplify-category-custom/src/__tests__/walkthroughts/cloudformation-walkthrough.test.ts new file mode 100644 index 00000000000..d340a5195a3 --- /dev/null +++ b/packages/amplify-category-custom/src/__tests__/walkthroughts/cloudformation-walkthrough.test.ts @@ -0,0 +1,48 @@ +import { $TSContext, pathManager } from 'amplify-cli-core'; +import { prompter } from 'amplify-prompts'; +import { customResourceNameQuestion } from '../../utils/common-questions'; +import { addCloudFormationWalkthrough } from '../../walkthroughs/cloudformation-walkthrough'; + +jest.mock('../../utils/common-questions'); +jest.mock('../../utils/build-custom-resources'); +jest.mock('../../utils/dependency-management-utils'); + +jest.mock('amplify-cli-core'); +jest.mock('amplify-prompts'); + +jest.mock('fs-extra', () => ({ + readFileSync: jest.fn().mockReturnValue('mockCode'), + existsSync: jest.fn().mockReturnValue(false), + ensureDirSync: jest.fn().mockReturnValue(true), + writeFileSync: jest.fn().mockReturnValue(true), +})); + +pathManager.getBackendDirPath = jest.fn().mockReturnValue('mockTargetDir'); + +let customResourceNameQuestion_mock = customResourceNameQuestion as jest.MockedFunction; +customResourceNameQuestion_mock.mockResolvedValue('customresoourcename'); + +describe('addCFNWalkthrough scenarios', () => { + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: { + openEditor: jest.fn(), + updateamplifyMetaAfterResourceAdd: jest.fn(), + copyBatch: jest.fn(), + }, + } as unknown as $TSContext; + }); + + it('successfully goes through cdk update walkthrough', async () => { + prompter.yesOrNo = jest.fn().mockReturnValueOnce(true); + + await addCloudFormationWalkthrough(mockContext); + + expect(mockContext.amplify.openEditor).toHaveBeenCalledTimes(1); + expect(mockContext.amplify.updateamplifyMetaAfterResourceAdd).toHaveBeenCalledTimes(1); + expect(mockContext.amplify.copyBatch).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/amplify-category-custom/src/utils/build-custom-resources.ts b/packages/amplify-category-custom/src/utils/build-custom-resources.ts index 112177efac9..04befbdae90 100644 --- a/packages/amplify-category-custom/src/utils/build-custom-resources.ts +++ b/packages/amplify-category-custom/src/utils/build-custom-resources.ts @@ -1,4 +1,3 @@ -import * as cdk from '@aws-cdk/core'; import { $TSContext, getPackageManager, JSONUtilities, pathManager, ResourceTuple } from 'amplify-cli-core'; import { printer } from 'amplify-prompts'; import execa from 'execa'; @@ -7,6 +6,7 @@ import ora from 'ora'; import * as path from 'path'; import { categoryName } from './constants'; import { getAllResources } from './dependency-management-utils'; +import { generateCloudFormationFromCDK } from './generate-cfn-from-cdk'; const resourcesDirRoot = path.normalize(path.join(__dirname, '../../resources')); const amplifyDependentResourcesFilename = 'amplify-dependent-resources-ref.ejs'; @@ -102,13 +102,3 @@ async function buildResource(context: $TSContext, resource: ResourceMeta) { await generateDependentResourcesType(context, targetDir); } - -async function generateCloudFormationFromCDK(resourceName: string) { - const targetDir = path.join(pathManager.getBackendDirPath(), categoryName, resourceName); - const { cdkStack } = require(path.resolve(path.join(targetDir, 'build', 'cdk-stack.js'))); - - const customStack: cdk.Stack = new cdkStack(undefined, undefined, undefined, { category: categoryName, resourceName }); - - // @ts-ignore - JSONUtilities.writeJson(path.join(targetDir, 'build', 'cloudformation-template.json'), customStack._toCloudFormation()); -} diff --git a/packages/amplify-category-custom/src/utils/constants.ts b/packages/amplify-category-custom/src/utils/constants.ts index 9865897e356..ae51bf3ce6d 100644 --- a/packages/amplify-category-custom/src/utils/constants.ts +++ b/packages/amplify-category-custom/src/utils/constants.ts @@ -5,5 +5,5 @@ export const categoryName = 'custom'; export const CDK_SERVICE_NAME = 'customCDK'; export const CFN_SERVICE_NAME = 'customCloudformation'; export const DEPLOYMENT_PROVIDER_NAME = 'awscloudformation'; -export const customResourceCFNFilename = `cloudformation-template.json`; +export const customResourceCFNFilenameSuffix = `cloudformation-template.json`; export const cdkFileName = 'cdk-stack.ts'; diff --git a/packages/amplify-category-custom/src/utils/dependency-management-utils.ts b/packages/amplify-category-custom/src/utils/dependency-management-utils.ts index a92f281059a..fdccf11eea4 100644 --- a/packages/amplify-category-custom/src/utils/dependency-management-utils.ts +++ b/packages/amplify-category-custom/src/utils/dependency-management-utils.ts @@ -6,7 +6,7 @@ import { glob } from 'glob'; import inquirer, { CheckboxQuestion, DistinctChoice } from 'inquirer'; import _ from 'lodash'; import * as path from 'path'; -import { categoryName, customResourceCFNFilename } from '../utils/constants'; +import { categoryName, customResourceCFNFilenameSuffix } from '../utils/constants'; const cfnTemplateGlobPattern = '*template*.+(yaml|yml|json)'; interface AmplifyDependentResourceDefinition { @@ -262,6 +262,7 @@ export async function addCFNResourceDependency(context: $TSContext, customResour // Add to CFN block const resourceDir = pathManager.getResourceDirectoryPath(undefined, categoryName, customResourceName); + const customResourceCFNFilename = `${customResourceName}-${customResourceCFNFilenameSuffix}`; const customResourceCFNFilepath = path.resolve(path.join(resourceDir, customResourceCFNFilename)); const customResourceCFNTemplate = readCFNTemplate(customResourceCFNFilepath); diff --git a/packages/amplify-category-custom/src/utils/generate-cfn-from-cdk.ts b/packages/amplify-category-custom/src/utils/generate-cfn-from-cdk.ts new file mode 100644 index 00000000000..327f5eaeb99 --- /dev/null +++ b/packages/amplify-category-custom/src/utils/generate-cfn-from-cdk.ts @@ -0,0 +1,14 @@ +import * as path from 'path'; +import * as cdk from '@aws-cdk/core'; +import { JSONUtilities, pathManager } from 'amplify-cli-core'; +import { categoryName } from './constants'; + +export async function generateCloudFormationFromCDK(resourceName: string) { + const targetDir = path.join(pathManager.getBackendDirPath(), categoryName, resourceName); + const { cdkStack } = require(path.resolve(path.join(targetDir, 'build', 'cdk-stack.js'))); + + const customStack: cdk.Stack = new cdkStack(undefined, undefined, undefined, { category: categoryName, resourceName }); + + // @ts-ignore + JSONUtilities.writeJson(path.join(targetDir, 'build', `${resourceName}-cloudformation-template.json`), customStack._toCloudFormation()); +} diff --git a/packages/amplify-category-custom/src/walkthroughs/cdk-walkthrough.ts b/packages/amplify-category-custom/src/walkthroughs/cdk-walkthrough.ts index 18febb79aba..8db106da294 100644 --- a/packages/amplify-category-custom/src/walkthroughs/cdk-walkthrough.ts +++ b/packages/amplify-category-custom/src/walkthroughs/cdk-walkthrough.ts @@ -29,7 +29,6 @@ async function updateAmplifyMetaFiles(context: $TSContext, resourceName: string) const backendConfigs = { service: CDK_SERVICE_NAME, providerPlugin: DEPLOYMENT_PROVIDER_NAME, - build: true, }; context.amplify.updateamplifyMetaAfterResourceAdd(categoryName, resourceName, backendConfigs); diff --git a/packages/amplify-category-custom/src/walkthroughs/cloudformation-walkthrough.ts b/packages/amplify-category-custom/src/walkthroughs/cloudformation-walkthrough.ts index 3a27e72f309..1ed3e4a328a 100644 --- a/packages/amplify-category-custom/src/walkthroughs/cloudformation-walkthrough.ts +++ b/packages/amplify-category-custom/src/walkthroughs/cloudformation-walkthrough.ts @@ -3,7 +3,7 @@ import { printer, prompter } from 'amplify-prompts'; import * as fs from 'fs-extra'; import * as path from 'path'; import { customResourceNameQuestion } from '../utils/common-questions'; -import { categoryName, CFN_SERVICE_NAME, customResourceCFNFilename, DEPLOYMENT_PROVIDER_NAME } from '../utils/constants'; +import { categoryName, CFN_SERVICE_NAME, customResourceCFNFilenameSuffix, DEPLOYMENT_PROVIDER_NAME } from '../utils/constants'; import { addCFNResourceDependency } from '../utils/dependency-management-utils'; const cfnTemplateRoot = path.normalize(path.join(__dirname, '../../resources')); @@ -23,6 +23,7 @@ export async function addCloudFormationWalkthrough(context: $TSContext) { // Open editor const resourceDirPath = path.join(pathManager.getBackendDirPath(), categoryName, resourceName); + const customResourceCFNFilename = `${resourceName}-${customResourceCFNFilenameSuffix}`; const cfnFilepath = path.join(resourceDirPath, customResourceCFNFilename); if (await prompter.yesOrNo('Do you want to edit the CloudFormation stack now?', true)) { @@ -36,6 +37,7 @@ export async function updateCloudFormationWalkthrough(context: $TSContext, resou // Open editor const resourceDirPath = path.join(pathManager.getBackendDirPath(), categoryName, resourceName); + const customResourceCFNFilename = `${resourceName}-${customResourceCFNFilenameSuffix}`; const cfnFilepath = path.join(resourceDirPath, customResourceCFNFilename); if (await prompter.yesOrNo('Do you want to edit the CloudFormation stack now?', true)) { @@ -45,6 +47,7 @@ export async function updateCloudFormationWalkthrough(context: $TSContext, resou async function generateSkeletonDir(context: $TSContext, resourceName: string) { const targetDir = path.join(pathManager.getBackendDirPath(), categoryName, resourceName); + const customResourceCFNFilename = `${resourceName}-${customResourceCFNFilenameSuffix}`; if (fs.existsSync(targetDir)) { throw new Error(`Custom resource with ${resourceName} already exists.`); } diff --git a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform.test.ts b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform.test.ts index 2ed25da6c73..26daf961e35 100644 --- a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform.test.ts +++ b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform.test.ts @@ -22,9 +22,9 @@ jest.mock('amplify-cli-core', () => ({ }, })); jest.mock('fs-extra', () => ({ - readFileSync: () => '{ "Cognito": { "provider": "aws"}}', - existsSync: () => true, - ensureDirSync: jest.fn(), + readFileSync: () => jest.fn().mockReturnValue('{ "Cognito": { "provider": "aws"}}'), + existsSync: () => jest.fn().mockReturnValue(true), + ensureDirSync: jest.fn().mockReturnValue(true), })); jest.mock('path', () => ({ diff --git a/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform.ts b/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform.ts index f0eac17d740..a09f654f09d 100644 --- a/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform.ts +++ b/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/ddb-stack-transform.ts @@ -207,7 +207,7 @@ export class DDBStackTransform { // store files in local-filesysten fs.ensureDirSync(this._cliInputsState.buildFilePath); - const cfnFilePath = path.resolve(path.join(this._cliInputsState.buildFilePath, 'cloudformation-template.json')); + const cfnFilePath = path.resolve(path.join(this._cliInputsState.buildFilePath, `${this._resourceName}-cloudformation-template.json`)); try { JSONUtilities.writeJson(cfnFilePath, this._cfn); } catch (e) { From 8deacf05e239f8e34b8f8ea811767e98fcc3a846 Mon Sep 17 00:00:00 2001 From: Ghosh Date: Fri, 29 Oct 2021 20:28:01 -0700 Subject: [PATCH 2/8] chore: remove empty unit test file --- .../src/__tests__/utils/dependency-management-utils.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts diff --git a/packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts b/packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts deleted file mode 100644 index e69de29bb2d..00000000000 From e61e5e3db2dedd663e55f06ff8ddec6ad3b878e8 Mon Sep 17 00:00:00 2001 From: Ghosh Date: Fri, 29 Oct 2021 21:21:14 -0700 Subject: [PATCH 3/8] fix: add all reuired types in override-helper package --- .eslintrc.js | 2 +- .../resources/overrides-resource/override.ts | 7 +++---- .../overrides-resource/DynamoDB/override.ts | 6 ++---- .../resources/overrides-resource/S3/override.ts | 9 +++------ packages/amplify-category-storage/src/index.ts | 9 ++++----- packages/amplify-category-storage/tsconfig.json | 3 ++- packages/amplify-cli-overrides-helper/package.json | 4 +++- packages/amplify-cli-overrides-helper/src/index.ts | 13 ++++++++++++- packages/amplify-cli-overrides-helper/tsconfig.json | 2 ++ .../resources/overrides-resource/override.ts | 7 +++---- .../resources/overrides-resource/package.json | 3 ++- 11 files changed, 37 insertions(+), 28 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index afb8478d877..60e0355d830 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -327,7 +327,7 @@ module.exports = { '/packages/amplify-category-auth/resources/auth-custom-resource', '/packages/amplify-category-custom/lib', '/packages/amplify-category-custom/resources', - '/packages/amplify-category-custom/src/utils/build-custom-resources.ts', + '/packages/amplify-category-custom/src/utils/generate-cfn-from-cdk.ts', // Ignore CHANGELOG.md files '/packages/*/CHANGELOG.md', diff --git a/packages/amplify-category-auth/resources/overrides-resource/override.ts b/packages/amplify-category-auth/resources/overrides-resource/override.ts index c02cee5ebd4..e5ba67d19cc 100644 --- a/packages/amplify-category-auth/resources/overrides-resource/override.ts +++ b/packages/amplify-category-auth/resources/overrides-resource/override.ts @@ -1,6 +1,5 @@ -/* Add Amplify Helper dependencies */ +import { AmplifyAuthCognitoStackTemplate } from '@aws-amplify/cli-overrides-helper'; -/* TODO: Need to change props to Root-Stack specific props when props are ready */ -export function overrideProps(props: any): void { - /* TODO: Add snippet of how to override in comments */ +export function overrideProps(props: AmplifyAuthCognitoStackTemplate) { + return props; } diff --git a/packages/amplify-category-storage/resources/overrides-resource/DynamoDB/override.ts b/packages/amplify-category-storage/resources/overrides-resource/DynamoDB/override.ts index 95ad315252c..e893913538a 100644 --- a/packages/amplify-category-storage/resources/overrides-resource/DynamoDB/override.ts +++ b/packages/amplify-category-storage/resources/overrides-resource/DynamoDB/override.ts @@ -1,7 +1,5 @@ -/* Add Amplify Helper dependencies */ +import { AmplifyDDBResourceTemplate } from '@aws-amplify/cli-overrides-helper'; -/* TODO: Need to change props to Root-Stack specific props when props are ready */ -export function overrideProps(props: any) { - /* TODO: Add snippet of how to override in comments */ +export function overrideProps(props: AmplifyDDBResourceTemplate) { return props; } diff --git a/packages/amplify-category-storage/resources/overrides-resource/S3/override.ts b/packages/amplify-category-storage/resources/overrides-resource/S3/override.ts index dc41b5b2044..f6de84d4c8d 100644 --- a/packages/amplify-category-storage/resources/overrides-resource/S3/override.ts +++ b/packages/amplify-category-storage/resources/overrides-resource/S3/override.ts @@ -1,8 +1,5 @@ -/* This file is used to override the S3 resource configuration */ -/* TBD: props is of type AmplifyS3ResourceTemplate */ +import { AmplifyS3ResourceTemplate } from '@aws-amplify/cli-overrides-helper'; -/* TODO: Need to change props to Root-Stack specific props when props are ready */ -export function overrideProps(props: any) { - /* Override props (AmplifyS3ResourceTemplate) with new parameters */ - return props; +export function overrideProps(props: AmplifyS3ResourceTemplate) { + return props; } diff --git a/packages/amplify-category-storage/src/index.ts b/packages/amplify-category-storage/src/index.ts index a9e74baf9bc..4cacc5e90ab 100644 --- a/packages/amplify-category-storage/src/index.ts +++ b/packages/amplify-category-storage/src/index.ts @@ -4,7 +4,7 @@ import { validateAddStorageRequest, validateImportStorageRequest, validateRemoveStorageRequest, - validateUpdateStorageRequest + validateUpdateStorageRequest, } from 'amplify-util-headless-input'; import * as path from 'path'; import sequential from 'promise-sequential'; @@ -17,11 +17,10 @@ import { headlessAddStorage, headlessImportStorage, headlessRemoveStorage, - headlessUpdateStorage + headlessUpdateStorage, } from './provider-utils/awscloudformation/storage-configuration-helpers'; export { categoryName as category } from './constants'; -export { AmplifyDDBResourceTemplate } from './provider-utils/awscloudformation/cdk-stack-builder/types'; - +export { AmplifyDDBResourceTemplate, AmplifyS3ResourceTemplate } from './provider-utils/awscloudformation/cdk-stack-builder/types'; async function add(context: any, providerName: any, service: any) { const options = { @@ -77,7 +76,7 @@ async function migrateStorageCategory(context: any) { } async function transformCategoryStack(context: $TSContext, resource: IAmplifyResource) { - if (resource.service === AmplifySupportedService.DYNAMODB ) { + if (resource.service === AmplifySupportedService.DYNAMODB) { if (canResourceBeTransformed(resource.resourceName)) { const stackGenerator = new DDBStackTransform(resource.resourceName); await stackGenerator.transform(); diff --git a/packages/amplify-category-storage/tsconfig.json b/packages/amplify-category-storage/tsconfig.json index 7439532badf..67148798164 100644 --- a/packages/amplify-category-storage/tsconfig.json +++ b/packages/amplify-category-storage/tsconfig.json @@ -14,6 +14,7 @@ { "path": "../amplify-cli-core" }, { "path": "../amplify-headless-interface" }, { "path": "../amplify-prompts" }, - { "path": "../amplify-util-import" } + { "path": "../amplify-util-import" }, + { "path": "../amplify-cli-overrides-helper" } ] } diff --git a/packages/amplify-cli-overrides-helper/package.json b/packages/amplify-cli-overrides-helper/package.json index e542c8c56a8..e1f52cbc8a8 100644 --- a/packages/amplify-cli-overrides-helper/package.json +++ b/packages/amplify-cli-overrides-helper/package.json @@ -29,7 +29,9 @@ "dependencies": { "amplify-prompts": "1.1.2", "amplify-provider-awscloudformation": "4.64.0", - "@aws-amplify/amplify-category-auth": "1.0.0" + "@aws-amplify/amplify-category-auth": "1.0.0", + "@aws-amplify/amplify-category-storage": "1.0.0", + "@aws-amplify/amplify-category-custom": "1.0.0" }, "devDependencies": {}, "jest": { diff --git a/packages/amplify-cli-overrides-helper/src/index.ts b/packages/amplify-cli-overrides-helper/src/index.ts index 2052642562c..5ff6b3739fb 100644 --- a/packages/amplify-cli-overrides-helper/src/index.ts +++ b/packages/amplify-cli-overrides-helper/src/index.ts @@ -1,6 +1,9 @@ import { printer } from 'amplify-prompts'; import { AmplifyRootStackTemplate } from 'amplify-provider-awscloudformation'; +import { AmplifyAuthCognitoStackTemplate } from '@aws-amplify/amplify-category-auth'; +import { AmplifyDDBResourceTemplate, AmplifyS3ResourceTemplate } from '@aws-amplify/amplify-category-storage'; +import { addCDKResourceDependency } from '@aws-amplify/amplify-category-custom'; function getProjectInfo(): void { printer.info('Hello from the skeleton of get project info'); @@ -10,4 +13,12 @@ function addDependency(): void { printer.info('Hello from the skeleton of add dependency'); } -export { getProjectInfo, addDependency, AmplifyRootStackTemplate }; +export { + getProjectInfo, + addDependency, + AmplifyRootStackTemplate, + AmplifyAuthCognitoStackTemplate, + AmplifyDDBResourceTemplate, + AmplifyS3ResourceTemplate, + addCDKResourceDependency, +}; diff --git a/packages/amplify-cli-overrides-helper/tsconfig.json b/packages/amplify-cli-overrides-helper/tsconfig.json index 83af1c6a620..91a745ee20e 100644 --- a/packages/amplify-cli-overrides-helper/tsconfig.json +++ b/packages/amplify-cli-overrides-helper/tsconfig.json @@ -7,6 +7,8 @@ "references": [ { "path": "../amplify-provider-awscloudformation" }, { "path": "../amplify-category-auth" }, + { "path": "../amplify-category-storage" }, + { "path": "../amplify-category-custom" }, ] } \ No newline at end of file diff --git a/packages/amplify-provider-awscloudformation/resources/overrides-resource/override.ts b/packages/amplify-provider-awscloudformation/resources/overrides-resource/override.ts index c02cee5ebd4..a7b26b93c3f 100644 --- a/packages/amplify-provider-awscloudformation/resources/overrides-resource/override.ts +++ b/packages/amplify-provider-awscloudformation/resources/overrides-resource/override.ts @@ -1,6 +1,5 @@ -/* Add Amplify Helper dependencies */ +import { AmplifyRootStackTemplate } from '@aws-amplify/cli-overrides-helper'; -/* TODO: Need to change props to Root-Stack specific props when props are ready */ -export function overrideProps(props: any): void { - /* TODO: Add snippet of how to override in comments */ +export function overrideProps(props: AmplifyRootStackTemplate) { + return props; } diff --git a/packages/amplify-provider-awscloudformation/resources/overrides-resource/package.json b/packages/amplify-provider-awscloudformation/resources/overrides-resource/package.json index 1601dccfc02..003ac8df059 100644 --- a/packages/amplify-provider-awscloudformation/resources/overrides-resource/package.json +++ b/packages/amplify-provider-awscloudformation/resources/overrides-resource/package.json @@ -9,7 +9,8 @@ }, "dependencies": { "@types/fs-extra": "^9.0.11", - "fs-extra": "^9.1.0" + "fs-extra": "^9.1.0", + "@aws-amplify/cli-overrides-helper": "ext8" }, "devDependencies": { "typescript": "^4.2.4" From 9148f496a66891da71358c2bbac4eb49a9522d3d Mon Sep 17 00:00:00 2001 From: Ghosh Date: Sat, 30 Oct 2021 08:57:19 -0700 Subject: [PATCH 4/8] chore: update override helper dependencies --- .../resources/overrides-resource/package.json | 3 ++- .../resources/overrides-resource/DynamoDB/package.json | 4 +++- .../resources/overrides-resource/S3/package.json | 3 ++- packages/amplify-cli-overrides-helper/package.json | 1 - packages/amplify-cli-overrides-helper/src/index.ts | 4 ++-- .../resources/overrides-resource/package.json | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/amplify-category-auth/resources/overrides-resource/package.json b/packages/amplify-category-auth/resources/overrides-resource/package.json index 916c8cc8afb..3735f85262c 100644 --- a/packages/amplify-category-auth/resources/overrides-resource/package.json +++ b/packages/amplify-category-auth/resources/overrides-resource/package.json @@ -9,7 +9,8 @@ }, "dependencies": { "@types/fs-extra": "^9.0.11", - "fs-extra": "^9.1.0" + "fs-extra": "^9.1.0", + "@aws-amplify/cli-overrides-helper": "1.1.0-ext9.0" }, "devDependencies": { "typescript": "^4.2.4" diff --git a/packages/amplify-category-storage/resources/overrides-resource/DynamoDB/package.json b/packages/amplify-category-storage/resources/overrides-resource/DynamoDB/package.json index 1601dccfc02..422947ca89b 100644 --- a/packages/amplify-category-storage/resources/overrides-resource/DynamoDB/package.json +++ b/packages/amplify-category-storage/resources/overrides-resource/DynamoDB/package.json @@ -9,7 +9,9 @@ }, "dependencies": { "@types/fs-extra": "^9.0.11", - "fs-extra": "^9.1.0" + "fs-extra": "^9.1.0", + "@aws-amplify/cli-overrides-helper": "1.1.0-ext9.0" + }, "devDependencies": { "typescript": "^4.2.4" diff --git a/packages/amplify-category-storage/resources/overrides-resource/S3/package.json b/packages/amplify-category-storage/resources/overrides-resource/S3/package.json index 1601dccfc02..260f194c1a5 100644 --- a/packages/amplify-category-storage/resources/overrides-resource/S3/package.json +++ b/packages/amplify-category-storage/resources/overrides-resource/S3/package.json @@ -9,7 +9,8 @@ }, "dependencies": { "@types/fs-extra": "^9.0.11", - "fs-extra": "^9.1.0" + "fs-extra": "^9.1.0", + "@aws-amplify/cli-overrides-helper": "1.1.0-ext9.0" }, "devDependencies": { "typescript": "^4.2.4" diff --git a/packages/amplify-cli-overrides-helper/package.json b/packages/amplify-cli-overrides-helper/package.json index e1f52cbc8a8..0b176dec0f5 100644 --- a/packages/amplify-cli-overrides-helper/package.json +++ b/packages/amplify-cli-overrides-helper/package.json @@ -28,7 +28,6 @@ }, "dependencies": { "amplify-prompts": "1.1.2", - "amplify-provider-awscloudformation": "4.64.0", "@aws-amplify/amplify-category-auth": "1.0.0", "@aws-amplify/amplify-category-storage": "1.0.0", "@aws-amplify/amplify-category-custom": "1.0.0" diff --git a/packages/amplify-cli-overrides-helper/src/index.ts b/packages/amplify-cli-overrides-helper/src/index.ts index 5ff6b3739fb..87cf70aa8f1 100644 --- a/packages/amplify-cli-overrides-helper/src/index.ts +++ b/packages/amplify-cli-overrides-helper/src/index.ts @@ -1,6 +1,6 @@ import { printer } from 'amplify-prompts'; -import { AmplifyRootStackTemplate } from 'amplify-provider-awscloudformation'; +//import { AmplifyRootStackTemplate } from 'amplify-provider-awscloudformation'; import { AmplifyAuthCognitoStackTemplate } from '@aws-amplify/amplify-category-auth'; import { AmplifyDDBResourceTemplate, AmplifyS3ResourceTemplate } from '@aws-amplify/amplify-category-storage'; import { addCDKResourceDependency } from '@aws-amplify/amplify-category-custom'; @@ -16,7 +16,7 @@ function addDependency(): void { export { getProjectInfo, addDependency, - AmplifyRootStackTemplate, + //AmplifyRootStackTemplate, AmplifyAuthCognitoStackTemplate, AmplifyDDBResourceTemplate, AmplifyS3ResourceTemplate, diff --git a/packages/amplify-provider-awscloudformation/resources/overrides-resource/package.json b/packages/amplify-provider-awscloudformation/resources/overrides-resource/package.json index 003ac8df059..260f194c1a5 100644 --- a/packages/amplify-provider-awscloudformation/resources/overrides-resource/package.json +++ b/packages/amplify-provider-awscloudformation/resources/overrides-resource/package.json @@ -10,7 +10,7 @@ "dependencies": { "@types/fs-extra": "^9.0.11", "fs-extra": "^9.1.0", - "@aws-amplify/cli-overrides-helper": "ext8" + "@aws-amplify/cli-overrides-helper": "1.1.0-ext9.0" }, "devDependencies": { "typescript": "^4.2.4" From 6bbb2dbd16f2dbbfd3f4f3785ce66371186e229a Mon Sep 17 00:00:00 2001 From: Ghosh Date: Sat, 30 Oct 2021 19:57:38 -0700 Subject: [PATCH 5/8] test: unit test for dependency management utils --- .../utils/build-custom-resources.test.ts | 6 +- .../utils/dependency-management-utils.test.ts | 287 ++++++++++++++++++ .../src/utils/dependency-management-utils.ts | 6 +- 3 files changed, 293 insertions(+), 6 deletions(-) create mode 100644 packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts diff --git a/packages/amplify-category-custom/src/__tests__/utils/build-custom-resources.test.ts b/packages/amplify-category-custom/src/__tests__/utils/build-custom-resources.test.ts index 0e8aacd3ad4..9526b34c571 100644 --- a/packages/amplify-category-custom/src/__tests__/utils/build-custom-resources.test.ts +++ b/packages/amplify-category-custom/src/__tests__/utils/build-custom-resources.test.ts @@ -1,9 +1,5 @@ -import { $TSContext, JSONUtilities, pathManager, getPackageManager } from 'amplify-cli-core'; -import * as fs from 'fs-extra'; +import { $TSContext } from 'amplify-cli-core'; import execa from 'execa'; -import ora from 'ora'; -import { getAllResources } from '../../utils/dependency-management-utils'; -import { generateCloudFormationFromCDK } from '../../utils/generate-cfn-from-cdk'; import { buildCustomResources } from '../../utils/build-custom-resources'; jest.mock('amplify-cli-core'); diff --git a/packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts b/packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts new file mode 100644 index 00000000000..0d5554bb1e1 --- /dev/null +++ b/packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts @@ -0,0 +1,287 @@ +import { $TSContext, CFNTemplateFormat, readCFNTemplate, pathManager, stateManager } from 'amplify-cli-core'; +import { glob } from 'glob'; +import * as fs from 'fs-extra'; +import { getResourceCfnOutputAttributes, getAllResources, addCDKResourceDependency } from '../../utils/dependency-management-utils'; +import * as cdk from '@aws-cdk/core'; + +jest.mock('amplify-cli-core'); +jest.mock('glob'); +jest.mock('fs-extra'); + +const readCFNTemplate_mock = readCFNTemplate as jest.MockedFunction; +const glob_mock = glob as jest.Mocked; +const fs_mock = fs as jest.Mocked; + +pathManager.getBackendDirPath = jest.fn().mockReturnValue('mockTargetDir'); +pathManager.getResourceDirectoryPath = jest.fn().mockReturnValue('mockResourceDir'); + +describe('getResourceCfnOutputAttributes() scenarios', () => { + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: { + openEditor: jest.fn(), + updateamplifyMetaAfterResourceAdd: jest.fn(), + copyBatch: jest.fn(), + getResourceStatus: jest.fn().mockResolvedValue({ + allResources: [ + { + resourceName: 'mockresource1', + service: 'customCDK', + }, + { + resourceName: 'mockresource2', + service: 'customCDK', + }, + ], + }), + }, + } as unknown as $TSContext; + }); + + it('get resource attr for resources with build folder with one cfn file', async () => { + fs_mock.existsSync.mockReturnValue(true); // if build dir exists + + readCFNTemplate_mock.mockReturnValueOnce({ + templateFormat: CFNTemplateFormat.JSON, + cfnTemplate: { Outputs: { mockKey: { Value: 'mockValue' } } }, + }); + glob_mock.sync.mockReturnValueOnce(['mockFileName']); + + expect(getResourceCfnOutputAttributes('mockCategory', 'mockResourceName')).toEqual(['mockKey']); + }); + + it('get resource attr for resources with build folder with multiple cfn files', async () => { + fs_mock.existsSync.mockReturnValue(true); // if build dir exists + + readCFNTemplate_mock.mockReturnValueOnce({ + templateFormat: CFNTemplateFormat.JSON, + cfnTemplate: { Outputs: { mockKey: { Value: 'mockValue' } } }, + }); + + glob_mock.sync.mockReturnValueOnce(['mockFileName1', 'mockFileName2']); + + expect(getResourceCfnOutputAttributes('mockCategory', 'mockResourceName')).toEqual([]); + }); + + it('get resource attr for resources without build folder', async () => { + fs_mock.existsSync.mockReturnValue(false); // if build dir exists + + readCFNTemplate_mock.mockReturnValueOnce({ + templateFormat: CFNTemplateFormat.JSON, + cfnTemplate: { Outputs: { mockKey: { Value: 'mockValue' } } }, + }); + glob_mock.sync.mockReturnValueOnce(['mockFileName']); + + expect(getResourceCfnOutputAttributes('mockCategory', 'mockResourceName')).toEqual(['mockKey']); + }); + + it('get resource attr for resources without build folder with multiple cfn files', async () => { + fs_mock.existsSync.mockReturnValue(false); // if build dir exists + + readCFNTemplate_mock.mockReturnValueOnce({ + templateFormat: CFNTemplateFormat.JSON, + cfnTemplate: { Outputs: { mockKey: { Value: 'mockValue' } } }, + }); + glob_mock.sync.mockReturnValueOnce(['mockFileName1', 'mockFileName2']); + + expect(getResourceCfnOutputAttributes('mockCategory', 'mockResourceName')).toEqual([]); + }); + + it('get resource attr for resources without any cfn files', async () => { + fs_mock.existsSync.mockReturnValue(false); // if build dir exists + glob_mock.sync.mockReturnValueOnce([]); + + expect(getResourceCfnOutputAttributes('mockCategory', 'mockResourceName')).toEqual([]); + }); +}); + +describe('getAllResources() scenarios', () => { + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: { + openEditor: jest.fn(), + updateamplifyMetaAfterResourceAdd: jest.fn(), + copyBatch: jest.fn(), + getResourceStatus: jest.fn().mockResolvedValue({ + allResources: [ + { + resourceName: 'mockresource1', + service: 'customCDK', + }, + { + resourceName: 'mockresource2', + service: 'customCDK', + }, + ], + }), + }, + } as unknown as $TSContext; + }); + + it('get all resource types', async () => { + fs_mock.existsSync.mockReturnValue(false); // if build dir exists + + readCFNTemplate_mock.mockReturnValue({ + templateFormat: CFNTemplateFormat.JSON, + cfnTemplate: { Outputs: { mockKey: { Value: 'mockValue' } } }, + }); + + glob_mock.sync.mockReturnValue(['mockFileName']); + + stateManager.getMeta = jest.fn().mockReturnValue({ + mockCategory1: { + mockResourceName1: {}, + }, + mockCategory2: { + mockResourceName2: {}, + }, + }); + + expect(getAllResources()).toEqual({ + mockCategory1: { mockResourceName1: { mockKey: 'string' } }, + mockCategory2: { mockResourceName2: { mockKey: 'string' } }, + }); + }); +}); + +describe('addCDKResourceDependency() scenarios', () => { + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: { + openEditor: jest.fn(), + updateamplifyMetaAfterResourceAdd: jest.fn(), + copyBatch: jest.fn(), + getResourceStatus: jest.fn().mockResolvedValue({ + allResources: [ + { + resourceName: 'mockresource1', + service: 'customCDK', + }, + { + resourceName: 'mockresource2', + service: 'customCDK', + }, + ], + }), + }, + } as unknown as $TSContext; + }); + + it('get depenencies for a custom CDK stack', async () => { + const getResourceCfnOutputAttributes_mock = getResourceCfnOutputAttributes as jest.MockedFunction< + typeof getResourceCfnOutputAttributes + >; + + getResourceCfnOutputAttributes_mock.mockImplementation = jest.fn().mockReturnValueOnce(['outputkey']).mockReturnValueOnce([]); + + fs_mock.existsSync.mockReturnValue(false); // if build dir exists + + readCFNTemplate_mock.mockReturnValue({ + templateFormat: CFNTemplateFormat.JSON, + cfnTemplate: { Outputs: { mockKey: { Value: 'mockValue' } } }, + }); + + glob_mock.sync.mockReturnValue(['mockFileName']); + + const mockBackendConfig = { + mockCategory1: { + mockResourceName1: {}, + }, + mockCategory2: { + mockResourceName2: {}, + }, + mockCategory3: { + mockResourceName3: {}, + }, + mockCategory4: { + mockResourceName4: {}, + }, + }; + + stateManager.getBackendConfig = jest.fn().mockReturnValue(mockBackendConfig); + + stateManager.setBackendConfig = jest.fn(); + + stateManager.getMeta = jest.fn().mockReturnValue(mockBackendConfig); + + stateManager.setMeta = jest.fn(); + const mockStack = new cdk.Stack(); + + // test with adding one dependency at once + let retVal = addCDKResourceDependency(mockStack, 'mockCategory1', 'mockResourceName1', [ + { category: 'mockCategory2', resourceName: 'mockResourceName2' }, + ]); + + expect(retVal).toEqual({ + mockCategory2: { + mockResourceName2: { mockKey: 'mockCategory2mockResourceName2mockKey' }, + }, + }); + + const postUpdateBackendConfig: any = mockBackendConfig; + postUpdateBackendConfig.mockCategory1.mockResourceName1.dependsOn = [ + { + attributes: ['mockKey'], + category: 'mockCategory2', + resourceName: 'mockResourceName2', + }, + ]; + + expect(stateManager.setMeta).toBeCalledWith(undefined, postUpdateBackendConfig); + expect(stateManager.setBackendConfig).toBeCalledWith(undefined, postUpdateBackendConfig); + + // test with adding multiple dependencies at once + + retVal = addCDKResourceDependency(mockStack, 'mockCategory1', 'mockResourceName1', [ + { category: 'mockCategory4', resourceName: 'mockResourceName4' }, + { category: 'mockCategory3', resourceName: 'mockResourceName3' }, + ]); + + expect(retVal).toEqual({ + mockCategory4: { + mockResourceName4: { mockKey: 'mockCategory4mockResourceName4mockKey' }, + }, + mockCategory3: { + mockResourceName3: { mockKey: 'mockCategory3mockResourceName3mockKey' }, + }, + }); + + postUpdateBackendConfig.mockCategory1.mockResourceName1.dependsOn = [ + { + attributes: ['mockKey'], + category: 'mockCategory3', + resourceName: 'mockResourceName3', + }, + { + attributes: ['mockKey'], + category: 'mockCategory4', + resourceName: 'mockResourceName4', + }, + ]; + + expect(stateManager.setMeta).toBeCalledWith(undefined, postUpdateBackendConfig); + expect(stateManager.setBackendConfig).toBeCalledWith(undefined, postUpdateBackendConfig); + + // test when adding multiple dependencies but none of the dependencies have outputs exported + + readCFNTemplate_mock.mockReturnValue({ templateFormat: CFNTemplateFormat.JSON, cfnTemplate: {} }); + + retVal = addCDKResourceDependency(mockStack, 'mockCategory1', 'mockResourceName1', [ + { category: 'mockCategory4', resourceName: 'mockResourceName4' }, + { category: 'mockCategory3', resourceName: 'mockResourceName3' }, + ]); + + expect(retVal).toEqual({}); + expect(stateManager.setMeta).toBeCalledTimes(2); // from the previous two successful calls - and skip the last call + expect(stateManager.setBackendConfig).toBeCalledTimes(2); // from the previous two successful calls - and skip the last call + }); +}); diff --git a/packages/amplify-category-custom/src/utils/dependency-management-utils.ts b/packages/amplify-category-custom/src/utils/dependency-management-utils.ts index fdccf11eea4..b54b47e58f1 100644 --- a/packages/amplify-category-custom/src/utils/dependency-management-utils.ts +++ b/packages/amplify-category-custom/src/utils/dependency-management-utils.ts @@ -25,7 +25,7 @@ export function getResourceCfnOutputAttributes(category: string, resourceName: s * This looks for a build directory and uses it if one exists. * Otherwise falls back to the default behavior. */ - if (fs.existsSync(resourceBuildDir) && fs.lstatSync(resourceBuildDir).isDirectory()) { + if (fs.existsSync(resourceBuildDir)) { const cfnFiles = glob.sync(cfnTemplateGlobPattern, { cwd: resourceBuildDir, }); @@ -47,6 +47,10 @@ export function getResourceCfnOutputAttributes(category: string, resourceName: s const cfnFiles = glob.sync(cfnTemplateGlobPattern, { cwd: resourceDir, }); + if (cfnFiles.length > 1) { + printer.warn(`${resourceName} has more than one CloudFormation definitions in the resource folder which isn't permitted.`); + return []; + } if (resourceDir && cfnFiles[0]) { cfnFilePath = path.join(resourceDir, cfnFiles[0]); } From c1fd98b3255b05e7024ede7ae2e1563c1bd5c80b Mon Sep 17 00:00:00 2001 From: Ghosh Date: Sat, 30 Oct 2021 20:49:58 -0700 Subject: [PATCH 6/8] test: add coverage data and add more unit tests for cfn dependency management --- packages/amplify-category-custom/package.json | 6 +- .../utils/dependency-management-utils.test.ts | 112 ++++++++++++++++-- .../src/utils/dependency-management-utils.ts | 6 +- 3 files changed, 112 insertions(+), 12 deletions(-) diff --git a/packages/amplify-category-custom/package.json b/packages/amplify-category-custom/package.json index 1b8ab8406d9..9fde9053d10 100644 --- a/packages/amplify-category-custom/package.json +++ b/packages/amplify-category-custom/package.json @@ -52,9 +52,9 @@ "node" ], "collectCoverage": true, - "coverageReporters": [ - "json", - "html" + "collectCoverageFrom": [ + "src/**/*.ts", + "!**/*.d.ts" ] } } diff --git a/packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts b/packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts index 0d5554bb1e1..a46fd5f4972 100644 --- a/packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts +++ b/packages/amplify-category-custom/src/__tests__/utils/dependency-management-utils.test.ts @@ -1,14 +1,26 @@ -import { $TSContext, CFNTemplateFormat, readCFNTemplate, pathManager, stateManager } from 'amplify-cli-core'; +import { $TSContext, CFNTemplateFormat, readCFNTemplate, pathManager, stateManager, writeCFNTemplate } from 'amplify-cli-core'; import { glob } from 'glob'; +import * as inquirer from 'inquirer'; +import { prompter } from 'amplify-prompts'; import * as fs from 'fs-extra'; -import { getResourceCfnOutputAttributes, getAllResources, addCDKResourceDependency } from '../../utils/dependency-management-utils'; +import { + getResourceCfnOutputAttributes, + getAllResources, + addCDKResourceDependency, + addCFNResourceDependency, +} from '../../utils/dependency-management-utils'; import * as cdk from '@aws-cdk/core'; jest.mock('amplify-cli-core'); +jest.mock('amplify-prompts'); jest.mock('glob'); jest.mock('fs-extra'); +jest.mock('inquirer'); const readCFNTemplate_mock = readCFNTemplate as jest.MockedFunction; +const writeCFNTemplate_mock = writeCFNTemplate as jest.MockedFunction; +writeCFNTemplate_mock.mockResolvedValue(); + const glob_mock = glob as jest.Mocked; const fs_mock = fs as jest.Mocked; @@ -177,12 +189,6 @@ describe('addCDKResourceDependency() scenarios', () => { }); it('get depenencies for a custom CDK stack', async () => { - const getResourceCfnOutputAttributes_mock = getResourceCfnOutputAttributes as jest.MockedFunction< - typeof getResourceCfnOutputAttributes - >; - - getResourceCfnOutputAttributes_mock.mockImplementation = jest.fn().mockReturnValueOnce(['outputkey']).mockReturnValueOnce([]); - fs_mock.existsSync.mockReturnValue(false); // if build dir exists readCFNTemplate_mock.mockReturnValue({ @@ -285,3 +291,93 @@ describe('addCDKResourceDependency() scenarios', () => { expect(stateManager.setBackendConfig).toBeCalledTimes(2); // from the previous two successful calls - and skip the last call }); }); + +describe('addCFNResourceDependency() scenarios', () => { + let mockContext: $TSContext; + + beforeEach(() => { + jest.clearAllMocks(); + mockContext = { + amplify: { + openEditor: jest.fn(), + updateamplifyMetaAfterResourceAdd: jest.fn(), + copyBatch: jest.fn(), + updateamplifyMetaAfterResourceUpdate: jest.fn(), + getResourceStatus: jest.fn().mockResolvedValue({ + allResources: [ + { + resourceName: 'mockresource1', + service: 'customCDK', + }, + { + resourceName: 'mockresource2', + service: 'customCDK', + }, + ], + }), + }, + } as unknown as $TSContext; + }); + + it('add new resource dependency to custom cfn stack ', async () => { + prompter.yesOrNo = jest.fn().mockReturnValueOnce(true); + fs_mock.existsSync.mockReturnValue(false); // if build dir exists + + readCFNTemplate_mock.mockReturnValue({ + templateFormat: CFNTemplateFormat.JSON, + cfnTemplate: { Outputs: { mockKey: { Value: 'mockValue' } } }, + }); + + glob_mock.sync.mockReturnValue(['mockFileName']); + + const mockBackendConfig = { + mockCategory1: { + mockResourceName1: {}, + }, + mockCategory2: { + mockResourceName2: {}, + }, + mockCategory3: { + mockResourceName3: {}, + }, + custom: { + customResourcename: {}, + }, + }; + + stateManager.getBackendConfig = jest.fn().mockReturnValue(mockBackendConfig); + + stateManager.setBackendConfig = jest.fn(); + + stateManager.getMeta = jest.fn().mockReturnValue(mockBackendConfig); + + stateManager.setMeta = jest.fn(); + + // test with adding one dependency at once + + const inqurer_mock = inquirer as jest.Mocked; + inqurer_mock.prompt + .mockResolvedValueOnce({ categories: ['mockCategory1'] }) + .mockResolvedValueOnce({ resources: ['mockResourceName1'] }); + + await addCFNResourceDependency(mockContext, 'customResourcename'); + + expect(writeCFNTemplate_mock).toBeCalledWith( + { + Outputs: { mockKey: { Value: 'mockValue' } }, + Parameters: { + mockCategory1mockResourceName1mockKey: { + Description: 'Input parameter describing mockKey attribute for mockCategory1/mockResourceName1 resource', + Type: 'String', + }, + }, + }, + expect.anything(), + { templateFormat: 'json' }, + ); + + expect(mockContext.amplify.updateamplifyMetaAfterResourceUpdate).toBeCalledWith('custom', 'customResourcename', 'dependsOn', [ + { attributes: ['mockKey'], category: 'mockCategory1', resourceName: 'mockResourceName1' }, + ]); + }); +}); diff --git a/packages/amplify-category-custom/src/utils/dependency-management-utils.ts b/packages/amplify-category-custom/src/utils/dependency-management-utils.ts index b54b47e58f1..c92ade67ae3 100644 --- a/packages/amplify-category-custom/src/utils/dependency-management-utils.ts +++ b/packages/amplify-category-custom/src/utils/dependency-management-utils.ts @@ -273,6 +273,10 @@ export async function addCFNResourceDependency(context: $TSContext, customResour const dependencyInputParams = generateInputParametersForDependencies(resources); + if (!customResourceCFNTemplate.cfnTemplate.Parameters) { + customResourceCFNTemplate.cfnTemplate.Parameters = {}; + } + Object.assign(customResourceCFNTemplate.cfnTemplate.Parameters, dependencyInputParams); await writeCFNTemplate(customResourceCFNTemplate.cfnTemplate, customResourceCFNFilepath, { @@ -281,7 +285,7 @@ export async function addCFNResourceDependency(context: $TSContext, customResour // Update meta and backend-config.json files - await context.amplify.updateamplifyMetaAfterResourceUpdate('custom', customResourceName, 'dependsOn', resources); + await context.amplify.updateamplifyMetaAfterResourceUpdate(categoryName, customResourceName, 'dependsOn', resources); // Show information on usage From c183da45394aa11d723ef98c7cc2e7f84c2542db Mon Sep 17 00:00:00 2001 From: Ghosh Date: Sun, 31 Oct 2021 18:56:12 -0700 Subject: [PATCH 7/8] test: add E2E tests for custom resources --- .../src/utils/dependency-management-utils.ts | 2 +- .../amplify-helpers/push-resources.ts | 2 +- .../amplify-e2e-core/src/categories/custom.ts | 64 +++++++ .../amplify-e2e-core/src/categories/index.ts | 1 + .../custom-resources/custom-cdk-stack.ts | 52 ++++++ .../custom-resources/custom-cfn-stack.json | 162 ++++++++++++++++++ .../src/__tests__/custom_resources.test.ts | 96 +++++++++++ packages/amplify-e2e-tests/tsconfig.json | 2 +- 8 files changed, 378 insertions(+), 3 deletions(-) create mode 100644 packages/amplify-e2e-core/src/categories/custom.ts create mode 100644 packages/amplify-e2e-tests/custom-resources/custom-cdk-stack.ts create mode 100644 packages/amplify-e2e-tests/custom-resources/custom-cfn-stack.json create mode 100644 packages/amplify-e2e-tests/src/__tests__/custom_resources.test.ts diff --git a/packages/amplify-category-custom/src/utils/dependency-management-utils.ts b/packages/amplify-category-custom/src/utils/dependency-management-utils.ts index c92ade67ae3..a91d13ae0b7 100644 --- a/packages/amplify-category-custom/src/utils/dependency-management-utils.ts +++ b/packages/amplify-category-custom/src/utils/dependency-management-utils.ts @@ -289,7 +289,7 @@ export async function addCFNResourceDependency(context: $TSContext, customResour // Show information on usage - showUsageInformation(resources); + // showUsageInformation(resources); } function showUsageInformation(resources: AmplifyDependentResourceDefinition[]) { diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/push-resources.ts b/packages/amplify-cli/src/extensions/amplify-helpers/push-resources.ts index 3525d0ebc76..942dd931527 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/push-resources.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/push-resources.ts @@ -54,7 +54,7 @@ export async function pushResources( // building all CFN stacks here to get the resource Changes const resourcesToBuild: IAmplifyResource[] = await getResources(context); - context.amplify.executeProviderUtils(context, 'awscloudformation', 'buildOverrides', { resourcesToBuild, forceCompile: true }); + await context.amplify.executeProviderUtils(context, 'awscloudformation', 'buildOverrides', { resourcesToBuild, forceCompile: true }); let hasChanges: boolean = false; if (!rebuild) { diff --git a/packages/amplify-e2e-core/src/categories/custom.ts b/packages/amplify-e2e-core/src/categories/custom.ts new file mode 100644 index 00000000000..a5d4fab9c9f --- /dev/null +++ b/packages/amplify-e2e-core/src/categories/custom.ts @@ -0,0 +1,64 @@ +import { nspawn as spawn, KEY_DOWN_ARROW, getCLIPath } from '..'; + +export function addCDKCustomResource(cwd: string, settings: any): Promise { + return new Promise((resolve, reject) => { + spawn(getCLIPath(), ['add', 'custom'], { cwd, stripColors: true }) + .wait('How do you want to define this custom resource?') + .sendCarriageReturn() + .wait('Provide a name for your custom resource') + .sendLine(settings.name || '\r') + .wait('Do you want to edit the CDK stack now?') + .sendConfirmNo() + .sendEof() + .run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + +export function addCFNCustomResource(cwd: string, settings: any): Promise { + return new Promise((resolve, reject) => { + spawn(getCLIPath(), ['add', 'custom'], { cwd, stripColors: true }) + .wait('How do you want to define this custom resource?') + .send(KEY_DOWN_ARROW) + .sendCarriageReturn() + .wait('Provide a name for your custom resource') + .sendLine(settings.name || '\r') + .wait('Do you want to access Amplify generated resources in your custom CloudFormation file?') + .sendConfirmYes() + .wait('Select the categories you want this custom resource to have access to.') + .send(' ') + .sendCarriageReturn() + .wait(/.*/) + .wait('Do you want to edit the CloudFormation stack now?') + .sendConfirmNo() + .sendEof() + .run((err: Error) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + +export function buildCustomResources(cwd: string, settings: {}) { + return new Promise((resolve, reject) => { + const args = ['custom', 'build']; + + spawn(getCLIPath(), args, { cwd, stripColors: true }) + .sendEof() + .run((err: Error) => { + if (!err) { + resolve({}); + } else { + reject(err); + } + }); + }); +} diff --git a/packages/amplify-e2e-core/src/categories/index.ts b/packages/amplify-e2e-core/src/categories/index.ts index af0f293e580..be52e51394a 100644 --- a/packages/amplify-e2e-core/src/categories/index.ts +++ b/packages/amplify-e2e-core/src/categories/index.ts @@ -10,3 +10,4 @@ export * from './notifications'; export * from './predictions'; export * from './storage'; export * from './geo'; +export * from './custom'; diff --git a/packages/amplify-e2e-tests/custom-resources/custom-cdk-stack.ts b/packages/amplify-e2e-tests/custom-resources/custom-cdk-stack.ts new file mode 100644 index 00000000000..e86dd942449 --- /dev/null +++ b/packages/amplify-e2e-tests/custom-resources/custom-cdk-stack.ts @@ -0,0 +1,52 @@ +import * as cdk from '@aws-cdk/core'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; +import * as subs from '@aws-cdk/aws-sns-subscriptions'; +import * as sqs from '@aws-cdk/aws-sqs'; + +export class cdkStack extends cdk.Stack { + constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + /* Do not remove - Amplify CLI automatically injects the current deployment environment in this input paramater */ + new cdk.CfnParameter(this, 'env', { + type: 'String', + description: 'Current Amplify CLI env name', + }); + + /* AWS CDK Code goes here - Learn more: https://docs.aws.amazon.com/cdk/latest/guide/home.html */ + + /* Example 1: Set up an SQS queue with an SNS topic */ + + const queue = new sqs.Queue(this, 'sqs-queue', { + queueName: cdk.Fn.join('-', ['custom-cdk-generated-sqs-queue-test', cdk.Fn.ref('env')]), + }); // For name unqieueness + + // 👇 create sns topic + const topic = new sns.Topic(this, 'sns-topic', { + topicName: cdk.Fn.join('-', ['custom-cdk-generated-sns-topic-test', cdk.Fn.ref('env')]), + }); // For name unqieueness + + // 👇 subscribe queue to topic + topic.addSubscription(new subs.SqsSubscription(queue)); + + new cdk.CfnOutput(this, 'snsTopicArn', { + value: topic.topicArn, + description: 'The arn of the SNS topic', + }); + + /* Example 2: Adding IAM role to the custom stack */ + const role = new iam.Role(this, 'CustomRole', { + roleName: cdk.Fn.join('-', ['custom-cdk-generated-custom-role-test', cdk.Fn.ref('env')]), // For name unqieueness + assumedBy: new iam.AccountRootPrincipal(), + }); + + /*Example 3: Adding policy to the IAM role*/ + role.addToPolicy( + new iam.PolicyStatement({ + actions: ['*'], + resources: [topic.topicArn], + }), + ); + } +} diff --git a/packages/amplify-e2e-tests/custom-resources/custom-cfn-stack.json b/packages/amplify-e2e-tests/custom-resources/custom-cfn-stack.json new file mode 100644 index 00000000000..93853d79709 --- /dev/null +++ b/packages/amplify-e2e-tests/custom-resources/custom-cfn-stack.json @@ -0,0 +1,162 @@ +{ + "Parameters": { + "env": { + "Type": "String", + "Description": "Current Amplify CLI env name" + } + }, + "Resources": { + "sqsqueueE70CFDBB": { + "Type": "AWS::SQS::Queue", + "Properties": { + "QueueName": { + "Fn::Join": [ + "-", + [ + "custom-cfn-generated-sqs-queue", + { + "Ref": "env" + } + ] + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "sqsqueuePolicyC27BD6E4": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "snstopic2C4AE3C1" + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": ["sqsqueueE70CFDBB", "Arn"] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "sqsqueueE70CFDBB" + } + ] + } + }, + "sqsqueuesnstopic5956E741": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "sqs", + "TopicArn": { + "Ref": "snstopic2C4AE3C1" + }, + "Endpoint": { + "Fn::GetAtt": ["sqsqueueE70CFDBB", "Arn"] + } + } + }, + "snstopic2C4AE3C1": { + "Type": "AWS::SNS::Topic", + "Properties": { + "TopicName": { + "Fn::Join": [ + "-", + [ + "custom-cfn-generated-sns-topic", + { + "Ref": "env" + } + ] + ] + } + } + }, + "CustomRole6D8E6809": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "RoleName": { + "Fn::Join": [ + "-", + [ + "custom-cfn-generated-custom-role", + { + "Ref": "env" + } + ] + ] + } + } + }, + "CustomRoleDefaultPolicyC5C189DF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "*", + "Effect": "Allow", + "Resource": { + "Ref": "snstopic2C4AE3C1" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CustomRoleDefaultPolicyC5C189DF", + "Roles": [ + { + "Ref": "CustomRole6D8E6809" + } + ] + } + } + }, + "Outputs": { + "snsTopicArn": { + "Description": "The arn of the SNS topic", + "Value": { + "Ref": "snstopic2C4AE3C1" + } + } + } +} diff --git a/packages/amplify-e2e-tests/src/__tests__/custom_resources.test.ts b/packages/amplify-e2e-tests/src/__tests__/custom_resources.test.ts new file mode 100644 index 00000000000..1ecbfbdd0bc --- /dev/null +++ b/packages/amplify-e2e-tests/src/__tests__/custom_resources.test.ts @@ -0,0 +1,96 @@ +import { $TSAny, JSONUtilities } from 'amplify-cli-core'; +import { + addCDKCustomResource, + addCFNCustomResource, + amplifyPushAuth, + buildCustomResources, + createNewProjectDir, + deleteProject, + deleteProjectDir, + getProjectMeta, + initJSProjectWithProfile, +} from 'amplify-e2e-core'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import uuid from 'uuid'; + +describe('adding custom resources test', () => { + let projRoot: string; + beforeEach(async () => { + projRoot = await createNewProjectDir('custom-resources'); + }); + + afterEach(async () => { + await deleteProject(projRoot); + deleteProjectDir(projRoot); + }); + + it('add/update CDK and CFN custom resources', async () => { + const cdkResourceName = `custom${uuid.v4().split('-')[0]}`; + const cfnResourceName = `custom${uuid.v4().split('-')[0]}`; + + await initJSProjectWithProfile(projRoot, {}); + await addCDKCustomResource(projRoot, { name: cdkResourceName }); + + const srcCustomResourceFilePath = path.join(__dirname, '..', '..', 'custom-resources', 'custom-cdk-stack.ts'); + const destCustomResourceFilePath = path.join(projRoot, 'amplify', 'backend', 'custom', cdkResourceName, 'cdk-stack.ts'); + const cfnFilePath = path.join( + projRoot, + 'amplify', + 'backend', + 'custom', + cdkResourceName, + 'build', + `${cdkResourceName}-cloudformation-template.json`, + ); + + fs.copyFileSync(srcCustomResourceFilePath, destCustomResourceFilePath); + + await buildCustomResources(projRoot, {}); + + await amplifyPushAuth(projRoot); + + // check if cfn file is generated in the build dir + expect(fs.existsSync(cfnFilePath)).toEqual(true); + + let buildCFNFileJSON: any = JSONUtilities.readJson(cfnFilePath); + + // Basic sanity generated CFN file content check + + expect(buildCFNFileJSON?.Parameters).toEqual({ + env: { Type: 'String', Description: 'Current Amplify CLI env name' }, + }); + + expect(Object.keys(buildCFNFileJSON?.Outputs)).toEqual(['snsTopicArn']); + + const meta = getProjectMeta(projRoot); + const { snsTopicArn: customResourceSNSArn } = Object.keys(meta.custom).map(key => meta.custom[key])[0].output; + + expect(customResourceSNSArn).toBeDefined(); + + // Add custom CFN and add dependency of custom CDK resource on the custom CFN + + await addCFNCustomResource(projRoot, { name: cfnResourceName }); + + const customCFNFilePath = path.join( + projRoot, + 'amplify', + 'backend', + 'custom', + cfnResourceName, + `${cfnResourceName}-cloudformation-template.json`, + ); + + const customCFNFileJSON: any = JSONUtilities.readJson(customCFNFilePath); + + // Make sure input params has params from the resource dependency + + expect(customCFNFileJSON?.Parameters).toEqual({ + env: { Type: 'String' }, + [`custom${cdkResourceName}snsTopicArn`]: { + Type: 'String', + Description: `Input parameter describing snsTopicArn attribute for custom/${cdkResourceName} resource`, + }, + }); + }); +}); diff --git a/packages/amplify-e2e-tests/tsconfig.json b/packages/amplify-e2e-tests/tsconfig.json index 8c429d06c65..0d0ec689241 100644 --- a/packages/amplify-e2e-tests/tsconfig.json +++ b/packages/amplify-e2e-tests/tsconfig.json @@ -15,5 +15,5 @@ "references": [ {"path": "../amplify-e2e-core"}, ], - "exclude": ["node_modules", "lib", "__tests__"] + "exclude": ["node_modules", "lib", "__tests__", "custom-resources", "overrides"] } \ No newline at end of file From 757b26edaddfb44f1fc67baa26d49dff720913b7 Mon Sep 17 00:00:00 2001 From: Ghosh Date: Sun, 31 Oct 2021 19:06:57 -0700 Subject: [PATCH 8/8] chore: address PR review comments --- .../src/utils/generate-cfn-from-cdk.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/amplify-category-custom/src/utils/generate-cfn-from-cdk.ts b/packages/amplify-category-custom/src/utils/generate-cfn-from-cdk.ts index 327f5eaeb99..384f02381e4 100644 --- a/packages/amplify-category-custom/src/utils/generate-cfn-from-cdk.ts +++ b/packages/amplify-category-custom/src/utils/generate-cfn-from-cdk.ts @@ -4,8 +4,8 @@ import { JSONUtilities, pathManager } from 'amplify-cli-core'; import { categoryName } from './constants'; export async function generateCloudFormationFromCDK(resourceName: string) { - const targetDir = path.join(pathManager.getBackendDirPath(), categoryName, resourceName); - const { cdkStack } = require(path.resolve(path.join(targetDir, 'build', 'cdk-stack.js'))); + const targetDir = pathManager.getResourceDirectoryPath(undefined, categoryName, resourceName); + const { cdkStack } = await import(path.resolve(path.join(targetDir, 'build', 'cdk-stack.js'))); const customStack: cdk.Stack = new cdkStack(undefined, undefined, undefined, { category: categoryName, resourceName });