Skip to content
Open
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
10 changes: 9 additions & 1 deletion docs/testing-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ provides a developer friendly API for writing HTTP integration tests in javascri
`testing` module includes a `getTestServer` helper to help you test your functions using
SuperTest.

By default, requests to `/favicon.ico` and `/robots.txt` return 404 without
invoking the function. Pass an empty `ignoredRoutes` value to test a deployment
that disables those default ignored routes:

```js
const server = getTestServer("HelloTests", {ignoredRoutes: ""});
```

```js
import supertest from 'supertest';
import {getTestServer} from '@google-cloud/functions-framework/testing';
Expand All @@ -198,4 +206,4 @@ describe("HelloTests", function () {
.expect(200);
});
});
```
```
23 changes: 20 additions & 3 deletions src/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ import {HandlerFunction} from '.';
import {getRegisteredFunction} from './function_registry';
import {getServer} from './server';

/**
* Options for configuring a Functions Framework test server.
*
* @beta
*/
export interface TestServerOptions {
/**
* Routes that should return 404 without invoking the function. An empty
* string disables the default ignored routes.
*/
ignoredRoutes?: string | null;
}

/**
* Testing utility for retrieving a function registered with the Functions Framework
* @param functionName - The name of the function to get
Expand All @@ -37,11 +50,15 @@ export const getFunction = (
* registered with the Functions Framework. This is a useful utility for testing functions
* using [supertest](https://www.npmjs.com/package/supertest).
* @param functionName - The name of the function to wrap in the test server
* @returns A function that was registered with the Functions Framework
* @param options - Options for configuring the test server
* @returns A server configured to invoke the registered function
*
* @beta
*/
export const getTestServer = (functionName: string): Server => {
export const getTestServer = (
functionName: string,
options: TestServerOptions = {},
): Server => {
const registeredFunction = getRegisteredFunction(functionName);
if (!registeredFunction) {
throw new Error(
Expand All @@ -56,6 +73,6 @@ export const getTestServer = (functionName: string): Server => {
target: '',
sourceLocation: '',
printHelp: false,
ignoredRoutes: null,
ignoredRoutes: options.ignoredRoutes ?? null,
});
};
67 changes: 66 additions & 1 deletion test/integration/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import * as sinon from 'sinon';
import * as supertest from 'supertest';

import * as functions from '../../src/index';
import {getTestServer} from '../../src/testing';
import {getTestServer, TestServerOptions} from '../../src/testing';

describe('HTTP Function', () => {
let callCount = 0;
Expand Down Expand Up @@ -117,4 +117,69 @@ describe('HTTP Function', () => {
assert.strictEqual(callCount, test.expectedCallCount);
});
});

const defaultOptions: {name: string; options?: TestServerOptions}[] = [
{name: 'undefined options'},
{name: 'an empty options object', options: {}},
{name: 'an undefined route', options: {ignoredRoutes: undefined}},
{name: 'a null route', options: {ignoredRoutes: null}},
];

for (const {name, options} of defaultOptions) {
for (const path of ['/favicon.ico', '/robots.txt']) {
it(`keeps ${path} ignored with ${name}`, async () => {
const response = await supertest(
getTestServer('testHttpFunction', options),
).get(path);

assert.strictEqual(response.status, 404);
assert.strictEqual(callCount, 0);
});
}
}

for (const ignoredRoutes of ['', ' ']) {
for (const path of ['/favicon.ico', '/robots.txt']) {
it(`serves ${path} with ignoredRoutes=${JSON.stringify(ignoredRoutes)}`, async () => {
const response = await supertest(
getTestServer('testHttpFunction', {ignoredRoutes}),
)
.get(path)
.query({param: 'served'});

assert.strictEqual(response.status, 200);
assert.deepStrictEqual(response.body, {query: 'served'});
assert.strictEqual(callCount, 1);
});
}
}

for (const path of ['/healthz', '/favicon.ico', '/robots.txt', '/hello']) {
it(`uses custom ignored routes for ${path}`, async () => {
const response = await supertest(
getTestServer('testHttpFunction', {ignoredRoutes: '/healthz'}),
)
.get(path)
.query({param: 'served'});
const ignored = path === '/healthz';

assert.strictEqual(response.status, ignored ? 404 : 200);
assert.deepStrictEqual(response.body, ignored ? {} : {query: 'served'});
assert.strictEqual(callCount, ignored ? 0 : 1);
});
}

it('preserves the error for an unregistered function', () => {
assert.throws(
() => getTestServer('unregisteredHttpFunction', {ignoredRoutes: ''}),
/was not registered/,
);
});

it('propagates an invalid ignored route expression', () => {
assert.throws(
() => getTestServer('testHttpFunction', {ignoredRoutes: '['}),
TypeError,
);
});
});
56 changes: 56 additions & 0 deletions test/system-test/pack_n_play.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,60 @@ describe('📦 pack-n-play test', () => {
};
await packNTest(options);
});

it('exposes configurable routes through the testing entry point', async function () {
this.timeout(300000);
await packNTest({
packageDir: process.cwd(),
sample: {
description: 'JavaScript user can configure the public testing helper',
js: `
const assert = require('assert');
const http = require('http');
const functions = require('@google-cloud/functions-framework');
const {getTestServer} = require('@google-cloud/functions-framework/testing');
let calls = 0;
functions.http('packagedHttpFunction', (_req, res) => {
calls++;
res.send('handler reached');
});

async function check(options, path, expectedStatus) {
const server = getTestServer('packagedHttpFunction', options);
calls = 0;
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
try {
const response = await new Promise((resolve, reject) => {
const request = http.get({
hostname: '127.0.0.1', port: server.address().port, path,
agent: false,
}, response => {
let body = '';
response.setEncoding('utf8');
response.on('data', chunk => body += chunk);
response.on('error', reject);
response.on('end', () => resolve({status: response.statusCode, body}));
});
request.on('error', reject);
});
assert.strictEqual(response.status, expectedStatus);
assert.strictEqual(calls, expectedStatus === 200 ? 1 : 0);
if (expectedStatus === 200) assert.strictEqual(response.body, 'handler reached');
} finally {
await new Promise((resolve, reject) => server.close(err => err ? reject(err) : resolve()));
}
}

(async () => {
for (const path of ['/favicon.ico', '/robots.txt']) {
await check(undefined, path, 404);
await check({ignoredRoutes: ''}, path, 200);
await check({ignoredRoutes: '/healthz'}, path, 200);
}
await check({ignoredRoutes: '/healthz'}, '/healthz', 404);
})().catch(error => { console.error(error); process.exitCode = 1; });
`,
},
});
});
});
Loading