Skip to content

Commit

Permalink
feat: support https for mock util plugin (#13700)
Browse files Browse the repository at this point in the history
* feat: allow secure connection for local testing

feat: add https option to mock api

* chore: lockfile

* chore: extract api

* fix: resolve test errors and add a new test

* fix: api interfaces

* fix: improvements on https configuration

* fix: lint
  • Loading branch information
afnx authored Apr 24, 2024
1 parent 6340adb commit cf418f1
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .eslint-dictionary.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@
"junit",
"jwks",
"keyless",
"keyout",
"keyphrase",
"kinesis",
"lang",
Expand Down Expand Up @@ -278,6 +279,7 @@
"openid",
"openpgp",
"opensearch",
"openssl",
"orgs",
"Parti",
"parens",
Expand Down
6 changes: 6 additions & 0 deletions packages/amplify-appsync-simulator/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export class AmplifyAppSyncSimulator {
// (undocumented)
init(config: AmplifyAppSyncSimulatorConfig): void;
// (undocumented)
get isHttps(): boolean;
// (undocumented)
get localhostUrl(): string;
// (undocumented)
get pubsub(): PubSub;
Expand Down Expand Up @@ -268,6 +270,10 @@ export type AppSyncSimulatorSchemaConfig = AppSyncMockFile;
export type AppSyncSimulatorServerConfig = {
port?: number;
wsPort?: number;
httpsConfig?: {
sslKeyPath: string;
sslCertPath: string;
};
};

// @public (undocumented)
Expand Down
3 changes: 3 additions & 0 deletions packages/amplify-appsync-simulator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ export class AmplifyAppSyncSimulator {
get localhostUrl(): string {
return this._server.localhostUrl.graphql;
}
get isHttps(): boolean {
return this._server.isHttps;
}
get config(): AmplifyAppSyncSimulatorConfig {
return this._config;
}
Expand Down
32 changes: 29 additions & 3 deletions packages/amplify-appsync-simulator/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { OperationServer } from './operations';
import { AmplifyAppSyncSimulator } from '..';
import { AppSyncSimulatorServerConfig } from '../type-definition';
import { Server, createServer } from 'http';
import { createServer as createHttpsServer } from 'https';
import { readFileSync } from 'fs';
import { fromEvent } from 'promise-toolbox';
import { address as getLocalIpAddress } from 'ip';
import { AppSyncSimulatorSubscriptionServer } from './websocket-subscription';
Expand All @@ -17,10 +19,30 @@ export class AppSyncSimulatorServer {
private _realTimeSubscriptionServer: AppSyncSimulatorSubscriptionServer;
private _url: string;
private _localhostUrl: string;
private _isHttps = false;

constructor(private config: AppSyncSimulatorServerConfig, private simulatorContext: AmplifyAppSyncSimulator) {
this._operationServer = new OperationServer(config, simulatorContext);
this._httpServer = createServer(this._operationServer.app);

// Check if the https configuration is not provided
if (!config.httpsConfig) {
this._httpServer = createServer(this._operationServer.app);
} else {
try {
// Read the ssl cert and key
const sslOptions = {
key: readFileSync(config.httpsConfig.sslKeyPath),
cert: readFileSync(config.httpsConfig.sslCertPath),
};
// Set the isHttps flag to true
this._isHttps = true;
// Create the https server
this._httpServer = createHttpsServer(sslOptions, this._operationServer.app);
} catch (e) {
throw new Error(`SSL key and certificate path provided are invalid. ${e.message}`);
}
}

this._realTimeSubscriptionServer = new AppSyncSimulatorSubscriptionServer(
simulatorContext,
this._httpServer,
Expand Down Expand Up @@ -49,8 +71,9 @@ export class AppSyncSimulatorServer {

this._httpServer.listen(port);
await fromEvent(this._httpServer, 'listening').then(() => {
this._url = `http://${getLocalIpAddress()}:${port}`;
this._localhostUrl = `http://localhost:${port}`;
const protocol = this._isHttps ? 'https' : 'http';
this._url = `${protocol}://${getLocalIpAddress()}:${port}`;
this._localhostUrl = `${protocol}://localhost:${port}`;
});
}

Expand All @@ -68,4 +91,7 @@ export class AppSyncSimulatorServer {
graphql: this._localhostUrl,
};
}
get isHttps() {
return this._isHttps;
}
}
4 changes: 4 additions & 0 deletions packages/amplify-appsync-simulator/src/type-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ export type AmplifyAppSyncSimulatorConfig = {
export type AppSyncSimulatorServerConfig = {
port?: number;
wsPort?: number;
httpsConfig?: {
sslKeyPath: string;
sslCertPath: string;
};
};

export type AmplifyAppSyncSimulatorRequestContext = {
Expand Down
2 changes: 1 addition & 1 deletion packages/amplify-graphiql-explorer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ This is the package that contains graphiql explorer assets for amplify-appsync-s

## Development Mode

When making changes to grapiql-explore, run `yarn start`. All the requests get proxied to `http://localhost:20002/`
When making changes to grapiql-explorer, run `yarn start`. All the requests get proxied to `http://localhost:20002/` by default (If you use the --https flag on `amplify mock`, change the proxy from `http://localhost:20002/` to `https://localhost:20002/` in package.json.)
29 changes: 29 additions & 0 deletions packages/amplify-util-mock/src/__tests__/api/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,35 @@ describe('Test Mock API methods', () => {
await expect(mockContext.print.error).toHaveBeenCalledWith('Failed to start API Mocking.');
});

it('shows error message and resolution when https enabled if SSL key and certificate paths are not provided', async () => {
ConfigOverrideManager.getInstance = jest.fn().mockReturnValue(jest.fn);
const mockContext = {
print: {
red: jest.fn(),
green: jest.fn(),
error: jest.fn(),
},
parameters: {
options: {
help: false,
},
},
input: {
argv: ['--https'],
},
amplify: {
getEnvInfo: jest.fn().mockReturnValue({ projectPath: mockProjectRoot }),
pathManager: {
getGitIgnoreFilePath: jest.fn(),
},
},
} as unknown as $TSContext;
await run(mockContext);
await expect(mockContext.print.error).toHaveBeenCalledWith(
'\nThe --https option must be followed by the path to the SSL key and the path to the SSL certificate.\n',
);
});

it('attempts to set custom port correctly', async () => {
const GRAPHQL_PORT = 8081;
const mockContext = {
Expand Down
36 changes: 36 additions & 0 deletions packages/amplify-util-mock/src/__tests__/get-https-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { getHttpsConfig } from '../utils/get-https-config';

describe('getHttpsConfig', () => {
let context;

beforeEach(() => {
context = {
input: {
argv: [],
},
print: {
error: jest.fn(),
},
};
});

it('returns paths when --https option is followed by key and cert paths', () => {
context.input.argv = ['--https', '/path/to/key', '/path/to/cert'];

const config = getHttpsConfig(context);

expect(config).toEqual({
sslKeyPath: '/path/to/key',
sslCertPath: '/path/to/cert',
});
});

it('returns null and prints error when --https option is not followed by key and cert paths', () => {
context.input.argv = ['--https'];

const config = getHttpsConfig(context);

expect(config).toEqual(null);
expect(context.print.error).toHaveBeenCalled();
});
});
8 changes: 7 additions & 1 deletion packages/amplify-util-mock/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ export class APITest {
private userOverriddenSlots: string[] = [];
private searchableTables: string[] = [];

async start(context, port: number = MOCK_API_PORT, wsPort: number = MOCK_API_PORT) {
async start(
context,
port: number = MOCK_API_PORT,
wsPort: number = MOCK_API_PORT,
httpsConfig?: { sslKeyPath: string; sslCertPath: string },
) {
try {
context.amplify.addCleanUpTask(async (context) => {
await this.stop(context);
Expand All @@ -72,6 +77,7 @@ export class APITest {
this.appSyncSimulator = new AmplifyAppSyncSimulator({
port,
wsPort,
httpsConfig: httpsConfig,
});
await this.appSyncSimulator.start();
await this.resolverOverrideManager.start();
Expand Down
5 changes: 4 additions & 1 deletion packages/amplify-util-mock/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { APITest } from './api';
import { addMockDataToGitIgnore, addMockAPIResourcesToGitIgnore } from '../utils';
import { getMockConfig } from '../utils/mock-config-file';
import { getHttpsConfig } from '../utils/get-https-config';

export async function start(context) {
const testApi = new APITest();
try {
addMockDataToGitIgnore(context);
addMockAPIResourcesToGitIgnore(context);
const mockConfig = await getMockConfig(context);
await testApi.start(context, mockConfig.graphqlPort, mockConfig.graphqlPort);
const httpsConfig = getHttpsConfig(context);

await testApi.start(context, mockConfig.graphqlPort, mockConfig.graphqlPort, httpsConfig);
} catch (e) {
console.log(e);
// Sending term signal so we clean up after ourselves
Expand Down
29 changes: 29 additions & 0 deletions packages/amplify-util-mock/src/utils/get-https-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export function getHttpsConfig(context): { sslKeyPath: string; sslCertPath: string } | null {
if (!context.input || !context.input.argv) {
return null;
}

const argv = context.input.argv;
const httpsIndex = argv.indexOf('--https');

if (httpsIndex !== -1) {
if (httpsIndex < argv.length - 2) {
const keyPath = argv[httpsIndex + 1];
const certPath = argv[httpsIndex + 2];
if (typeof keyPath === 'string' && typeof certPath === 'string') {
return { sslKeyPath: keyPath, sslCertPath: certPath };
} else {
context.print.error('\nThe provided paths for the SSL key and certificate are not valid.\n');
context.print.error('Please ensure you have entered the correct paths.\n');
}
} else {
context.print.error('\nThe --https option must be followed by the path to the SSL key and the path to the SSL certificate.\n');
context.print.error('Example: amplify mock api --https /path/to/key /path/to/cert\n');
context.print.error('In order to generate a key and certificate, you can use openssl:\n');
context.print.error('openssl req -nodes -new -x509 -keyout server.key -out server.cert\n');
context.print.error('Then, run the command again with the paths to the generated key and certificate.\n');
}
}

return null;
}

0 comments on commit cf418f1

Please sign in to comment.