diff --git a/packages/amplify-appsync-simulator/src/__tests__/index.test.ts b/packages/amplify-appsync-simulator/src/__tests__/index.test.ts index 3a72a74ae02..5f496ee57e2 100644 --- a/packages/amplify-appsync-simulator/src/__tests__/index.test.ts +++ b/packages/amplify-appsync-simulator/src/__tests__/index.test.ts @@ -9,6 +9,7 @@ import { AppSyncMockFile, AppSyncSimulatorBaseResolverConfig, RESOLVER_KIND, + AppSyncSimulatorUnitResolverConfig, } from '../type-definition'; jest.mock('../schema'); @@ -48,9 +49,10 @@ describe('AmplifyAppSyncSimulator', () => { }); it('should retain the original configuration when config has error', () => { - const resolver: AppSyncSimulatorBaseResolverConfig = { + const resolver: AppSyncSimulatorUnitResolverConfig = { fieldName: 'echo', typeName: 'Query', + dataSourceName: 'echoFn', kind: RESOLVER_KIND.UNIT, requestMappingTemplateLocation: 'missing/Resolver.req.vtl', responseMappingTemplateLocation: 'missing/Resolver.resp.vtl', diff --git a/packages/amplify-appsync-simulator/src/__tests__/resolvers/pipeline-resolver.test.ts b/packages/amplify-appsync-simulator/src/__tests__/resolvers/pipeline-resolver.test.ts new file mode 100644 index 00000000000..798da1505d7 --- /dev/null +++ b/packages/amplify-appsync-simulator/src/__tests__/resolvers/pipeline-resolver.test.ts @@ -0,0 +1,202 @@ +import { AppSyncPipelineResolver } from '../../resolvers/pipeline-resolver'; +import { AmplifyAppSyncSimulator } from '../..'; +import { RESOLVER_KIND, AppSyncSimulatorPipelineResolverConfig } from '../../type-definition'; +describe('Pipeline Resolvers', () => { + const getFunction = jest.fn(); + const getMappingTemplate = jest.fn(); + const simulatorContext: AmplifyAppSyncSimulator = ({ + getFunction, + getMappingTemplate, + } as any) as AmplifyAppSyncSimulator; + let baseConfig; + beforeEach(() => { + jest.resetAllMocks(); + getFunction.mockReturnValue({ resolve: () => 'foo' }); + getMappingTemplate.mockReturnValue('TEMPLATE'); + baseConfig = { + fieldName: 'fn1', + typeName: 'Query', + kind: RESOLVER_KIND.PIPELINE, + functions: ['fn1', 'fn2'], + }; + }); + it('should initialize when the request and response mapping templates are inline templates', () => { + const config: AppSyncSimulatorPipelineResolverConfig = { + ...baseConfig, + requestMappingTemplate: 'request', + responseMappingTemplate: 'response', + }; + expect(() => new AppSyncPipelineResolver(config, simulatorContext)).not.toThrow(); + expect(getMappingTemplate).not.toHaveBeenCalled(); + }); + + it('should work when the request and response mapping are external template', () => { + const config: AppSyncSimulatorPipelineResolverConfig = { + ...baseConfig, + requestMappingTemplateLocation: 'resolvers/request', + responseMappingTemplateLocation: 'resolvers/response', + }; + expect(() => new AppSyncPipelineResolver(config, simulatorContext)).not.toThrow(); + expect(getMappingTemplate).toHaveBeenCalledTimes(2); + }); + + it('should throw error when request templates are missing', () => { + getMappingTemplate.mockImplementation(() => { + throw new Error('Missing template'); + }); + expect(() => new AppSyncPipelineResolver(baseConfig, simulatorContext)).toThrowError('Missing request mapping template'); + expect(getMappingTemplate).toHaveBeenCalled(); + }); + + describe('resolve', () => { + let resolver: AppSyncPipelineResolver; + const baseConfig: AppSyncSimulatorPipelineResolverConfig = { + fieldName: 'fn1', + typeName: 'Query', + kind: RESOLVER_KIND.PIPELINE, + functions: ['fn1', 'fn2'], + requestMappingTemplateLocation: 'request', + responseMappingTemplateLocation: 'response', + }; + let templates; + let fnImpl; + beforeEach(() => { + fnImpl = { + fn1: { + resolve: jest.fn().mockImplementation((source, args, stash, prevResult, context, info) => { + return { + result: 'FN1-RESULT', + stash: { ...stash, exeSeq: [...(stash.exeSeq || []), 'fn1'] }, + }; + }), + }, + fn2: { + resolve: jest.fn().mockImplementation((source, args, stash, prevResult, context, info) => { + return { + result: 'FN2-RESULT', + stash: { ...stash, exeSeq: [...(stash.exeSeq || []), 'fn2'] }, + }; + }), + }, + }; + getFunction.mockImplementation(fnName => fnImpl[fnName]); + templates = { + request: { + render: jest.fn().mockImplementation(({ stash }) => ({ + result: 'REQUEST_TEMPLATE_RESULT', + errors: [], + stash: { ...stash, exeSeq: [...(stash.exeSeq || []), 'REQUEST-MAPPING-TEMPLATE'] }, + })), + }, + response: { + render: jest.fn().mockImplementation(({ stash }) => ({ + result: 'RESPONSE_TEMPLATE_RESULT', + errors: [], + stash: { ...stash, exeSeq: [...stash.exeSeq, 'fn2'] }, + })), + }, + }; + + getMappingTemplate.mockImplementation(templateName => templates[templateName]); + resolver = new AppSyncPipelineResolver(baseConfig, simulatorContext); + }); + + it('should render requestMapping template', async () => { + const source = 'SOURCE'; + const args = { arg1: 'val' }; + const context = { + appsyncErrors: [], + }; + const info = {}; + const result = await resolver.resolve(source, args, context, info); + expect(result).toEqual('RESPONSE_TEMPLATE_RESULT'); + expect(templates['request'].render).toHaveBeenCalledWith( + { + source, + arguments: args, + stash: {}, + }, + context, + info, + ); + + expect(getMappingTemplate).toHaveBeenCalledTimes(4); // 2 times in constructor and 2 times for resolving + expect(getFunction).toHaveBeenCalled(); + }); + + it('should pass stash and prevResult between functions and templates', async () => { + const source = 'SOURCE'; + const args = { arg1: 'val' }; + const context = { + appsyncErrors: [], + }; + const info = {}; + const result = await resolver.resolve(source, args, context, info); + expect(result).toEqual('RESPONSE_TEMPLATE_RESULT'); + expect(fnImpl.fn1.resolve).toHaveBeenLastCalledWith( + source, + args, + { exeSeq: ['REQUEST-MAPPING-TEMPLATE'] }, + 'REQUEST_TEMPLATE_RESULT', + context, + info, + ); + + expect(fnImpl.fn2.resolve).toHaveBeenLastCalledWith( + source, + args, + { exeSeq: ['REQUEST-MAPPING-TEMPLATE', 'fn1'] }, + 'FN1-RESULT', + context, + info, + ); + + expect(templates['response'].render).toHaveBeenCalledWith( + { + source, + arguments: args, + prevResult: 'FN2-RESULT', + stash: { exeSeq: ['REQUEST-MAPPING-TEMPLATE', 'fn1', 'fn2'] }, + }, + context, + info, + ); + }); + + it('should not call response mapping template when #return is called', async () => { + templates.request.render.mockReturnValue({ isReturn: true, result: 'REQUEST_TEMPLATE_RESULT', templateErrors: [] }); + const source = 'SOURCE'; + const args = { arg1: 'val' }; + const context = { + appsyncErrors: [], + }; + const info = {}; + const result = await resolver.resolve(source, args, context, info); + expect(result).toEqual('REQUEST_TEMPLATE_RESULT'); + }); + + it('should merge template errors', async () => { + templates.request.render.mockReturnValue({ + isReturn: false, + stash: {}, + result: 'REQUEST_TEMPLATE_RESULT', + errors: ['REQUEST_TEMPLATE_ERROR'], + }); + templates.response.render.mockReturnValue({ + isReturn: false, + result: 'RESPONSE_TEMPLATE_RESULT', + errors: ['RESPONSE_TEMPLATE_ERROR'], + }); + + const source = 'SOURCE'; + const args = { arg1: 'val' }; + const context = { + appsyncErrors: [], + }; + const info = {}; + const result = await resolver.resolve(source, args, context, info); + expect(result).toEqual('RESPONSE_TEMPLATE_RESULT'); + expect(context.appsyncErrors).toEqual(['REQUEST_TEMPLATE_ERROR', 'RESPONSE_TEMPLATE_ERROR']); + }); + }); +}); diff --git a/packages/amplify-appsync-simulator/src/__tests__/resolvers/unit-resolver.test.ts b/packages/amplify-appsync-simulator/src/__tests__/resolvers/unit-resolver.test.ts new file mode 100644 index 00000000000..b457ca10253 --- /dev/null +++ b/packages/amplify-appsync-simulator/src/__tests__/resolvers/unit-resolver.test.ts @@ -0,0 +1,176 @@ +import { AppSyncUnitResolver } from '../../resolvers/unit-resolver'; +import { AmplifyAppSyncSimulator } from '../..'; +import { RESOLVER_KIND, AppSyncSimulatorUnitResolverConfig } from '../../type-definition'; + +describe('Unit resolver', () => { + const getDataLoader = jest.fn(); + const getMappingTemplate = jest.fn(); + const simulatorContext: AmplifyAppSyncSimulator = ({ + getDataLoader, + getMappingTemplate, + } as any) as AmplifyAppSyncSimulator; + let baseConfig; + + beforeEach(() => { + jest.resetAllMocks(); + getDataLoader.mockReturnValue({ + load: () => { + return 'DATA'; + }, + }); + getMappingTemplate.mockReturnValue('TEMPLATE'); + baseConfig = { + fieldName: 'getPost', + typeName: 'Query', + kind: RESOLVER_KIND.UNIT, + dataSourceName: 'TodoTable', + }; + }); + + it('should initialize when the request and response mapping templates are inline templates', () => { + const config: AppSyncSimulatorUnitResolverConfig = { + ...baseConfig, + requestMappingTemplate: 'request', + responseMappingTemplate: 'response', + }; + expect(() => new AppSyncUnitResolver(config, simulatorContext)).not.toThrow(); + expect(getMappingTemplate).not.toHaveBeenCalled(); + }); + + it('should work when the request and response mapping are external template', () => { + const config: AppSyncSimulatorUnitResolverConfig = { + ...baseConfig, + requestMappingTemplateLocation: 'resolvers/request', + responseMappingTemplateLocation: 'resolvers/response', + }; + expect(() => new AppSyncUnitResolver(config, simulatorContext)).not.toThrow(); + expect(getMappingTemplate).toHaveBeenCalledTimes(2); + }); + + it('should throw error when request templates are missing', () => { + getMappingTemplate.mockImplementation(() => { + throw new Error('Missing template'); + }); + expect(() => new AppSyncUnitResolver(baseConfig, simulatorContext)).toThrowError('Missing request mapping template'); + expect(getMappingTemplate).toHaveBeenCalled(); + }); + + describe('resolve', () => { + let templates; + let resolver; + + const info = {}; + const DATA_FROM_DATA_SOURCE = 'DATA FROM DATA SOURCE'; + const REQUEST_TEMPLATE_RESULT = { + version: '2017-02-29', + result: 'REQUEST_TEMPLATE_RESULT', + }; + const RESPONSE_TEMPLATE_RESULT = { + version: '2017-02-29', + data: 'RESPONSE_TEMPLATE_RESULT', + }; + let dataFetcher; + + const source = 'SOURCE'; + const args = { key: 'value' }; + const context = { + appsyncErrors: [], + }; + + beforeEach(() => { + context.appsyncErrors = []; + templates = { + request: { + render: jest.fn().mockImplementation(() => ({ + result: REQUEST_TEMPLATE_RESULT, + errors: [], + })), + }, + response: { + render: jest.fn().mockImplementation(() => ({ + result: RESPONSE_TEMPLATE_RESULT, + errors: [], + })), + }, + }; + + dataFetcher = jest.fn().mockResolvedValue(DATA_FROM_DATA_SOURCE); + + getDataLoader.mockReturnValue({ + load: dataFetcher, + }); + getMappingTemplate.mockImplementation(templateName => { + return templates[templateName]; + }); + + resolver = new AppSyncUnitResolver( + { ...baseConfig, requestMappingTemplateLocation: 'request', responseMappingTemplateLocation: 'response' }, + simulatorContext, + ); + }); + + it('should resolve', async () => { + const result = await resolver.resolve(source, args, context, info); + expect(result).toEqual(RESPONSE_TEMPLATE_RESULT); + expect(templates.request.render).toHaveBeenCalledWith({ source, arguments: args }, context, info); + expect(dataFetcher).toHaveBeenCalledWith(REQUEST_TEMPLATE_RESULT); + expect(getDataLoader).toBeCalledWith('TodoTable'); + expect(templates.response.render).toHaveBeenCalledWith({ source, arguments: args, result: DATA_FROM_DATA_SOURCE }, context, info); + }); + + it('should not call the response mapping template with template version 2017-02-29 and data fetcher throws error', async () => { + dataFetcher.mockImplementation(() => { + throw new Error('Some request template error'); + }); + + await expect(() => resolver.resolve(source, args, context, info)).rejects.toThrowError('Some request template error'); + expect(templates.request.render).toHaveBeenCalledWith({ source, arguments: args }, context, info); + expect(dataFetcher).toHaveBeenCalledWith(REQUEST_TEMPLATE_RESULT); + expect(getDataLoader).toBeCalledWith('TodoTable'); + expect(templates.response.render).not.toHaveBeenCalled(); + }); + + it('should render response mapping when data fetcher throws error and template version is 2018-05-29', async () => { + REQUEST_TEMPLATE_RESULT.version = '2018-05-29'; + const error = new Error('Some request template error'); + dataFetcher.mockImplementation(() => { + throw error; + }); + const result = await resolver.resolve(source, args, context, info); + expect(result).toEqual(RESPONSE_TEMPLATE_RESULT); + expect(templates.request.render).toHaveBeenCalledWith({ source, arguments: args }, context, info); + expect(dataFetcher).toHaveBeenCalledWith(REQUEST_TEMPLATE_RESULT); + expect(getDataLoader).toBeCalledWith('TodoTable'); + expect(templates.response.render).toHaveBeenCalledWith({ source, arguments: args, error: error, result: null }, context, info); + }); + + it('should not render response mapping template when #return is used in request mapping template', async () => { + REQUEST_TEMPLATE_RESULT.version = '2018-05-29'; + templates.request.render.mockReturnValue({ + ...REQUEST_TEMPLATE_RESULT, + errors: [], + isReturn: true, + }); + + const result = await resolver.resolve(source, args, context, info); + expect(result).toEqual(REQUEST_TEMPLATE_RESULT.result); + expect(templates.request.render).toHaveBeenCalledWith({ source, arguments: args }, context, info); + expect(dataFetcher).not.toHaveBeenCalledWith(REQUEST_TEMPLATE_RESULT); + expect(templates.response.render).not.toHaveBeenCalled(); + }); + + it('should collect all the errors in context object', async () => { + templates.request.render.mockReturnValue({ + ...REQUEST_TEMPLATE_RESULT, + errors: ['request error'], + }); + templates.response.render.mockReturnValue({ + ...REQUEST_TEMPLATE_RESULT, + errors: ['response error'], + }); + const result = await resolver.resolve(source, args, context, info); + expect(result).toEqual(REQUEST_TEMPLATE_RESULT.result); + expect(context.appsyncErrors).toEqual(['request error', 'response error']); + }); + }); +}); diff --git a/packages/amplify-appsync-simulator/src/resolvers/pipeline-resolver.ts b/packages/amplify-appsync-simulator/src/resolvers/pipeline-resolver.ts index 4789cff3339..fcd7487bcc5 100644 --- a/packages/amplify-appsync-simulator/src/resolvers/pipeline-resolver.ts +++ b/packages/amplify-appsync-simulator/src/resolvers/pipeline-resolver.ts @@ -33,26 +33,22 @@ export class AppSyncPipelineResolver extends AppSyncBaseResolver { info, )); - context.appsyncErrors = [...context.appsyncErrors, ...templateErrors]; + context.appsyncErrors = [...context.appsyncErrors, ...(templateErrors || [])]; if (isReturn) { //Request mapping template called #return, don't process further return result; } - let prevResult; + let prevResult = result; for (let fnName of this.config.functions) { const fnResolver = this.simulatorContext.getFunction(fnName); ({ result: prevResult, stash } = await fnResolver.resolve(source, args, stash, prevResult, context, info)); } // pipeline response mapping template - ({ result, errors: templateErrors } = responseMappingTemplate.render( - { source, arguments: args, result, prevResult, stash }, - context, - info, - )); - context.appsyncErrors = [...context.appsyncErrors, ...templateErrors]; + ({ result, errors: templateErrors } = responseMappingTemplate.render({ source, arguments: args, prevResult, stash }, context, info)); + context.appsyncErrors = [...context.appsyncErrors, ...(templateErrors || [])]; return result; } }