Skip to content

Commit

Permalink
Merge pull request #12537 from aws-amplify/fix/stack-trace-mockAPI
Browse files Browse the repository at this point in the history
fix(mock): handle stack trace & produce meaningful error, resolution messages for mocking API & Func category
  • Loading branch information
aws-eddy authored May 1, 2023
2 parents 4bcb846 + 4387f34 commit 0f20518
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 25 deletions.
61 changes: 55 additions & 6 deletions packages/amplify-util-mock/src/__tests__/api/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { $TSContext, AmplifyFault, pathManager } from '@aws-amplify/amplify-cli-core';
import { $TSContext, AmplifyError, pathManager } from '@aws-amplify/amplify-cli-core';
import { APITest } from '../../api/api';
import * as lambdaInvoke from '../../api/lambda-invoke';
import { getMockSearchableTriggerDirectory } from '../../utils';
import { ConfigOverrideManager } from '../../utils/config-override';
import { run } from '../../commands/mock/api';

jest.mock('@aws-amplify/amplify-cli-core', () => ({
...(jest.requireActual('@aws-amplify/amplify-cli-core') as Record<string, unknown>),
Expand All @@ -16,6 +17,10 @@ jest.mock('@aws-amplify/amplify-cli-core', () => ({
FeatureFlags: {
getNumber: jest.fn(),
},
stateManager: {
getLocalEnvInfo: jest.fn(),
localEnvInfoExists: jest.fn(),
},
}));
jest.mock('amplify-dynamodb-simulator', () => jest.fn());
jest.mock('fs-extra');
Expand Down Expand Up @@ -66,10 +71,12 @@ describe('Test Mock API methods', () => {
);
});

it('Shows the error when no appsync api exist', async () => {
it('Shows the error message, resolution & link to docs when no appsync api exist', async () => {
ConfigOverrideManager.getInstance = jest.fn().mockReturnValue(jest.fn);
const mockContext = {
print: {
red: jest.fn(),
green: jest.fn(),
error: jest.fn(),
},
amplify: {
Expand All @@ -82,12 +89,54 @@ describe('Test Mock API methods', () => {
} as unknown as $TSContext;

const testApi = new APITest();
const testApiStartPromise = testApi.start(mockContext);
await testApi.start(mockContext);

await expect(testApiStartPromise).rejects.toThrow(
new AmplifyFault('MockProcessFault', {
message: 'Failed to start API Mocking.. Reason: No AppSync API is added to the project',
await expect(testApi['getAppSyncAPI'](mockContext)).rejects.toThrow(
new AmplifyError('MockProcessError', {
message: 'No AppSync API is added to the project',
resolution: `Use 'amplify add api' in the root of your app directory to create a GraphQL API.`,
link: 'https://docs.amplify.aws/cli/graphql/troubleshooting/',
}),
);
expect(mockContext.print.green).toHaveBeenCalledWith(
'\n For troubleshooting the GraphQL API, visit https://docs.amplify.aws/cli/graphql/troubleshooting/ ',
);
});

it('shows error message & resolution when amplify environment is not initialized', async () => {
ConfigOverrideManager.getInstance = jest.fn().mockReturnValue(jest.fn);
const mockContext = {
print: {
red: jest.fn(),
green: jest.fn(),
error: jest.fn(),
},
parameters: {
options: {
help: false,
},
},
amplify: {
getEnvInfo: jest.fn(() => {
throw new AmplifyError('EnvironmentNotInitializedError', {
message: 'Current environment cannot be determined.',
resolution: `Use 'amplify init' in the root of your app directory to create a new environment.`,
});
}),
loadRuntimePlugin: jest.fn().mockReturnValue({}),
addCleanUpTask: jest.fn,
pathManager: {
getAmplifyMetaFilePath: jest.fn(),
getGitIgnoreFilePath: jest.fn(),
},
stateManager: {
localEnvInfoExists: false,
},
readJsonFile: jest.fn().mockReturnValue({ api: {} }),
getProjectDetails: {},
},
} as unknown as $TSContext;
await run(mockContext);
await expect(mockContext.print.error).toHaveBeenCalledWith('Failed to start API Mocking.');
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { $TSContext } from '@aws-amplify/amplify-cli-core';
import { $TSContext, AmplifyError } from '@aws-amplify/amplify-cli-core';
import { lambdaArnToConfig } from '../../api/lambda-arn-to-config';
import { ProcessedLambdaFunction } from '../../CFNParser/stack/types';
import { loadLambdaConfig } from '../../utils/lambda/load-lambda-config';

jest.mock('@aws-amplify/amplify-cli-core', () => ({
...(jest.requireActual('@aws-amplify/amplify-cli-core') as Record<string, unknown>),
pathManager: {
getAmplifyPackageLibDirPath: jest.fn().mockReturnValue('test/path'),
},
Expand Down Expand Up @@ -46,7 +47,6 @@ describe('lambda arn to config', () => {
expect(loadLambdaConfig_mock.mock.calls[0][1]).toEqual('lambda1');
expect(result).toEqual(expectedLambdaConfig);
});

it('resolves Fn::Sub with params when lambda name is in template string', async () => {
const result = await lambdaArnToConfig(context_stub, { 'Fn::Sub': [`some::arn::lambda2::{withsubs}::stuff`, { withsubs: 'a value' }] });
expect(loadLambdaConfig_mock.mock.calls[0][1]).toEqual('lambda2');
Expand Down Expand Up @@ -74,6 +74,12 @@ describe('lambda arn to config', () => {
});

it('throws when arn is valid but no matching lambda found in the project', async () => {
expect(lambdaArnToConfig(context_stub, 'validformat::but::no::matchinglambda')).rejects.toThrowError();
expect(lambdaArnToConfig(context_stub, 'validformat::but::no::matchinglambda')).rejects.toThrowError(
new AmplifyError('MockProcessError', {
message: `Did not find a Lambda matching ARN [\"validformat::but::no::matchinglambda\"] in the project. Local mocking only supports Lambdas that are configured in the project.`,
resolution: `Use 'amplify add function' in the root of your app directory to create a new Lambda Function. To connect an AWS Lambda resolver to the GraphQL API, add the @function directive to a field in your schema.`,
link: expect.any(String),
}),
);
});
});
20 changes: 12 additions & 8 deletions packages/amplify-util-mock/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as fs from 'fs-extra';
import * as dynamoEmulator from 'amplify-dynamodb-simulator';
import { AmplifyAppSyncSimulator, AmplifyAppSyncSimulatorConfig } from '@aws-amplify/amplify-appsync-simulator';
import * as opensearchEmulator from '@aws-amplify/amplify-opensearch-simulator';
import { $TSContext, $TSAny, AmplifyFault, AMPLIFY_SUPPORT_DOCS, isWindowsPlatform } from '@aws-amplify/amplify-cli-core';
import { $TSContext, $TSAny, AmplifyFault, AMPLIFY_SUPPORT_DOCS, isWindowsPlatform, AmplifyError } from '@aws-amplify/amplify-cli-core';
import { add, generate, isCodegenConfigured, switchToSDLSchema } from 'amplify-codegen';
import * as path from 'path';
import * as chokidar from 'chokidar';
Expand Down Expand Up @@ -38,6 +38,7 @@ export const GRAPHQL_API_ENDPOINT_OUTPUT = 'GraphQLAPIEndpointOutput';
export const GRAPHQL_API_KEY_OUTPUT = 'GraphQLAPIKeyOutput';
export const MOCK_API_KEY = 'da2-fakeApiId123456';
export const MOCK_API_PORT = 20002;
const errorSuffix = `\n For troubleshooting the GraphQL API, visit ${AMPLIFY_SUPPORT_DOCS.CLI_GRAPHQL_TROUBLESHOOTING.url} `;

export class APITest {
private apiName: string;
Expand Down Expand Up @@ -92,10 +93,12 @@ export class APITest {
const errMessage = 'Failed to start API Mocking.';
context.print.error(errMessage + ' Running cleanup tasks.');
await this.stop(context);
throw new AmplifyFault('MockProcessFault', {
message: `${errMessage}. Reason: ${e?.message}`,
link: AMPLIFY_SUPPORT_DOCS.CLI_GRAPHQL_TROUBLESHOOTING.url,
});
if (e.resolution == undefined || e.link == undefined) {
context.print.red(`Reason: ${e.message}`);
} else {
context.print.red(`Reason: ${e.message}\nResolution: ${e.resolution}`);
context.print.green(`${e.link}`);
}
}
}

Expand Down Expand Up @@ -369,7 +372,7 @@ export class APITest {
const ddbConfig = this.ddbClient.config;
return configureDDBDataSource(config, ddbConfig);
}
private async getAppSyncAPI(context) {
public async getAppSyncAPI(context) {
const currentMeta = await getAmplifyMeta(context);
const { api: apis = {} } = currentMeta;
let name = null;
Expand All @@ -381,9 +384,10 @@ export class APITest {
return undefined;
});
if (!name) {
throw new AmplifyFault('MockProcessFault', {
throw new AmplifyError('MockProcessError', {
message: 'No AppSync API is added to the project',
link: AMPLIFY_SUPPORT_DOCS.CLI_GRAPHQL_TROUBLESHOOTING.url,
resolution: `Use 'amplify add api' in the root of your app directory to create a GraphQL API.`,
link: `${errorSuffix}`,
});
}
return name;
Expand Down
20 changes: 14 additions & 6 deletions packages/amplify-util-mock/src/api/lambda-arn-to-config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { keys } from 'lodash';
import { $TSAny, $TSContext, stateManager, ApiCategoryFacade, getGraphQLTransformerFunctionDocLink } from '@aws-amplify/amplify-cli-core';
import {
$TSAny,
$TSContext,
stateManager,
ApiCategoryFacade,
getGraphQLTransformerFunctionDocLink,
AmplifyError,
} from '@aws-amplify/amplify-cli-core';
import _ = require('lodash');
import { ServiceName } from '@aws-amplify/amplify-category-function';
import { loadLambdaConfig } from '../utils/lambda/load-lambda-config';
import { ProcessedLambdaFunction } from '../CFNParser/stack/types';

/**
* Attempts to match an arn object against the array of lambdas configured in the project
*/
Expand Down Expand Up @@ -32,11 +38,13 @@ export const lambdaArnToConfig = async (context: $TSContext, arn: $TSAny): Promi
.map(([key]) => key);
const foundLambdaName = lambdaNames.find((name) => searchString.includes(name));
if (!foundLambdaName) {
throw new Error(
`Did not find a Lambda matching ARN [${JSON.stringify(
throw new AmplifyError('MockProcessError', {
message: `Did not find a Lambda matching ARN [${JSON.stringify(
arn,
)}] in the project. Local mocking only supports Lambdas that are configured in the project.${errorSuffix}`,
);
)}] in the project. Local mocking only supports Lambdas that are configured in the project.`,
resolution: `Use 'amplify add function' in the root of your app directory to create a new Lambda Function. To connect an AWS Lambda resolver to the GraphQL API, add the @function directive to a field in your schema.`,
link: `${errorSuffix}`,
});
}
// lambdaArnToConfig is only called in the context of initializing a mock API, so setting overrideApiToLocal to true here
return loadLambdaConfig(context, foundLambdaName, true);
Expand Down
11 changes: 9 additions & 2 deletions packages/amplify-util-mock/src/commands/mock/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { $TSContext } from '@aws-amplify/amplify-cli-core';
import { start } from '../../api';

export const name = 'api';

export const run = async (context: $TSContext) => {
Expand All @@ -11,8 +10,16 @@ export const run = async (context: $TSContext) => {
return;
}
try {
// added here to get the Env info before starting to mock
await context.amplify.getEnvInfo();
await start(context);
} catch (e) {
context.print.error(e.message);
context.print.error(`Failed to start API Mocking.`);
if (e.resolution == undefined || e.link == undefined) {
context.print.red(`Reason: ${e.message}`);
} else {
context.print.red(`Reason: ${e.message}\nResolution: ${e.resolution}`);
context.print.green(`For troubleshooting guide, visit: ${e.link}`);
}
}
};

0 comments on commit 0f20518

Please sign in to comment.