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]); }