Skip to content

feat: extract runtime-handler and lazyLoading #252

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

Merged
merged 8 commits into from
May 20, 2021
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
2 changes: 1 addition & 1 deletion jest.config.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = {
// A set of global variables that need to be available in all test environments
globals: {
'ts-jest': {
tsConfig: 'tsconfig.test.json',
tsconfig: 'tsconfig.test.json',
},
},
// The test environment that will be used for testing
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"lerna": "^3.22.1",
"lint-staged": "^8.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^1.18.2",
"prettier": "^2.2.1",
"rimraf": "^3.0.2",
"ts-jest": "^26.0.0",
"typescript": "^3.9.7"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`with an express app with forked process function handling Function integration tests basic-twiml.js should match snapshot 1`] = `
Object {
"body": Object {},
"headers": Object {
"cache-control": "no-store, no-cache, must-revalidate, proxy-revalidate",
"connection": "close",
"content-type": "text/xml; charset=utf-8",
"expires": "0",
"pragma": "no-cache",
"surrogate-control": "no-store",
"x-powered-by": "Express",
},
"statusCode": 200,
"text": "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><Response><Message>Hello World</Message></Response>",
"type": "text/xml",
}
`;

exports[`with an express app with inline function handling Assets integration tests hello.js should match snapshot 1`] = `
Object {
"body": Object {},
"headers": Object {
"accept-ranges": "bytes",
"cache-control": "no-store, no-cache, must-revalidate, proxy-revalidate",
"connection": "close",
"content-type": "application/javascript; charset=UTF-8",
"expires": "0",
"pragma": "no-cache",
"surrogate-control": "no-store",
"x-powered-by": "Express",
},
"statusCode": 200,
"text": "alert('Hello world!');
",
"type": "application/javascript",
}
`;

exports[`with an express app with inline function handling Function integration tests basic-twiml.js should match snapshot 1`] = `
Object {
"body": Object {},
"headers": Object {
"cache-control": "no-store, no-cache, must-revalidate, proxy-revalidate",
"connection": "close",
"content-type": "text/xml; charset=utf-8",
"expires": "0",
"pragma": "no-cache",
"surrogate-control": "no-store",
"x-powered-by": "Express",
},
"statusCode": 200,
"text": "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><Response><Message>Hello World</Message></Response>",
"type": "text/xml",
}
`;
187 changes: 187 additions & 0 deletions packages/runtime-handler/__tests__/dev-runtime/integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
jest.unmock('twilio');

import { Express } from 'express';
import { readdirSync, readFileSync } from 'fs';
import { basename, resolve } from 'path';
import request from 'supertest';
import { LocalDevelopmentServer } from '../../src/dev-runtime/server';
import {
ServerConfig,
ServerlessResourceConfigWithFilePath,
} from '../../src/dev-runtime/types';

const TEST_DIR = resolve(__dirname, '../../fixtures');

const TEST_FUNCTIONS_DIR = resolve(TEST_DIR, 'functions');
const TEST_ASSETS_DIR = resolve(TEST_DIR, 'assets');
const TEST_ENV = {};

const availableFunctions: ServerlessResourceConfigWithFilePath[] = readdirSync(
TEST_FUNCTIONS_DIR
).map((name: string) => {
const filePath = resolve(TEST_FUNCTIONS_DIR, name);
const content = readFileSync(filePath, 'utf8');
const url = `/${basename(name, '.js')}`;
return { name, path: url, filePath, access: 'public', content };
});
const availableAssets: ServerlessResourceConfigWithFilePath[] = readdirSync(
TEST_ASSETS_DIR
).map((name: string) => {
const filePath = resolve(TEST_ASSETS_DIR, name);
const url = `/${name}`;
const content = readFileSync(filePath);
return { name, filePath, path: url, access: 'public', content };
});

const BASE_CONFIG: ServerConfig = {
baseDir: TEST_DIR,
env: TEST_ENV,
port: 9000,
url: 'http://localhost:9000',
detailedLogs: false,
live: true,
logs: false,
legacyMode: false,
appName: 'integration-test',
forkProcess: false,
logger: undefined,
routes: { assets: [], functions: [] },
enableDebugLogs: false,
};

type InternalResponse = request.Response & {
statusCode: number;
headers: {
[key: string]: string | undefined;
};
};

function responseToSnapshotJson(response: InternalResponse) {
let { statusCode, type, body, text, headers } = response;
delete headers['date'];
delete headers['last-modified'];

if (text && text.startsWith('Error')) {
// stack traces are different in every environment
// let's not snapshot values that rely on it
text = `${text.split('\n')[0]} ...`;
}
delete headers['content-length'];
delete headers['etag'];

return {
statusCode,
type,
body,
text,
headers,
};
}

describe('with an express app', () => {
let app: Express;

describe('with inline function handling', () => {
beforeAll(async () => {
app = new LocalDevelopmentServer(9000, {
...BASE_CONFIG,
routes: {
assets: availableAssets,
functions: availableFunctions,
},
forkProcess: false,
} as ServerConfig).getApp();
});

describe('Function integration tests', () => {
for (const testFnCode of availableFunctions) {
test(`${testFnCode.name} should match snapshot`, async () => {
const response = await request(app).get(testFnCode.path);
if (response.status === 500) {
expect(response.text).toMatch(/Error/);
} else {
const result = responseToSnapshotJson(response as InternalResponse);
expect(result).toMatchSnapshot();
}
});
}
});

describe('Assets integration tests', () => {
for (const testAsset of availableAssets) {
test(`${testAsset.name} should match snapshot`, async () => {
const response = await request(app).get(testAsset.path);
const result = responseToSnapshotJson(response as InternalResponse);
expect(result).toMatchSnapshot();
});

test(`OPTIONS request to ${testAsset.name} should return CORS headers and no body`, async () => {
const response = (await request(app).options(
testAsset.path
)) as InternalResponse;
expect(response.headers['access-control-allow-origin']).toEqual('*');
expect(response.headers['access-control-allow-headers']).toEqual(
'Accept, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, User-Agent'
);
expect(response.headers['access-control-allow-methods']).toEqual(
'GET, POST, OPTIONS'
);
expect(response.headers['access-control-expose-headers']).toEqual(
'ETag'
);
expect(response.headers['access-control-max-age']).toEqual('86400');
expect(response.headers['access-control-allow-credentials']).toEqual(
'true'
);
expect(response.text).toEqual('');
});

test(`GET request to ${testAsset.name} should not return CORS headers`, async () => {
const response = (await request(app).get(
testAsset.path
)) as InternalResponse;
expect(
response.headers['access-control-allow-origin']
).toBeUndefined();
expect(
response.headers['access-control-allow-headers']
).toBeUndefined();
expect(
response.headers['access-control-allow-methods']
).toBeUndefined();
expect(
response.headers['access-control-expose-headers']
).toBeUndefined();
expect(response.headers['access-control-max-age']).toBeUndefined();
expect(
response.headers['access-control-allow-credentials']
).toBeUndefined();
});
}
});
});

describe('with forked process function handling', () => {
beforeAll(async () => {
app = new LocalDevelopmentServer(9000, {
...BASE_CONFIG,
routes: { assets: availableAssets, functions: availableFunctions },
forkProcess: true,
} as ServerConfig).getApp();
});

describe('Function integration tests', () => {
for (const testFnCode of availableFunctions) {
test(`${testFnCode.name} should match snapshot`, async () => {
const response = await request(app).get(testFnCode.path);
if (response.status === 500) {
expect(response.text).toMatch(/Error/);
} else {
const result = responseToSnapshotJson(response as InternalResponse);
expect(result).toMatchSnapshot();
}
});
}
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Response as ExpressResponse } from 'express';
import { Response } from '../../../src/dev-runtime/internal/response';

test('has correct defaults', () => {
const response = new Response();
expect(response['body']).toBeUndefined();
expect(response['statusCode']).toBe(200);
expect(response['headers']).toEqual({});
});

test('sets status code', () => {
const response = new Response();
expect(response['statusCode']).toBe(200);
response.setStatusCode(418);
expect(response['statusCode']).toBe(418);
});

test('sets body correctly', () => {
const response = new Response();
expect(response['body']).toBeUndefined();
response.setBody('Hello');
expect(response['body']).toBe('Hello');
response.setBody({ url: 'https://dkundel.com' });
expect(response['body']).toEqual({ url: 'https://dkundel.com' });
});

test('sets headers correctly', () => {
const response = new Response();
expect(response['headers']).toEqual({});
response.setHeaders({
'Access-Control-Allow-Origin': 'example.com',
'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE',
'Access-Control-Allow-Headers': 'Content-Type',
});
const expected = {
'Access-Control-Allow-Origin': 'example.com',
'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE',
'Access-Control-Allow-Headers': 'Content-Type',
};
expect(response['headers']).toEqual(expected);
// @ts-ignore
response.setHeaders(undefined);
expect(response['headers']).toEqual(expected);
});

test('appends a new header correctly', () => {
const response = new Response();
expect(response['headers']).toEqual({});
response.appendHeader('Access-Control-Allow-Origin', 'dkundel.com');
expect(response['headers']).toEqual({
'Access-Control-Allow-Origin': 'dkundel.com',
});
response.appendHeader('Content-Type', 'application/json');
expect(response['headers']).toEqual({
'Access-Control-Allow-Origin': 'dkundel.com',
'Content-Type': 'application/json',
});
});

test('appends a header correctly with no existing one', () => {
const response = new Response();
expect(response['headers']).toEqual({});
// @ts-ignore
response['headers'] = undefined;
response.appendHeader('Access-Control-Allow-Origin', 'dkundel.com');
expect(response['headers']).toEqual({
'Access-Control-Allow-Origin': 'dkundel.com',
});
});

test('calls express response correctly', () => {
const mockRes = ({
status: jest.fn(),
set: jest.fn(),
send: jest.fn(),
} as unknown) as ExpressResponse;
const response = new Response();
response.setBody(`I'm a teapot!`);
response.setStatusCode(418);
response.appendHeader('Content-Type', 'text/plain');
response.applyToExpressResponse(mockRes);

expect(mockRes.send).toHaveBeenCalledWith(`I'm a teapot!`);
expect(mockRes.status).toHaveBeenCalledWith(418);
expect(mockRes.set).toHaveBeenCalledWith({ 'Content-Type': 'text/plain' });
});
Loading