Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(mock): handle stack trace & produce meaningful error, resolution messages for mocking API & Func category #12537

Merged
merged 7 commits into from
May 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}`);
}
}
};