From 329a272d4006f73348dbbc17b4f4264c68ffb10a Mon Sep 17 00:00:00 2001 From: hugosmoreira Date: Fri, 7 Aug 2026 18:32:43 -0700 Subject: [PATCH 1/2] feat(testing): configure ignored routes in test server --- docs/testing-functions.md | 10 +++++++++- src/testing.ts | 23 ++++++++++++++++++++--- test/integration/http.ts | 9 +++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/docs/testing-functions.md b/docs/testing-functions.md index f1d7ae3f0..941d7920f 100644 --- a/docs/testing-functions.md +++ b/docs/testing-functions.md @@ -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'; @@ -198,4 +206,4 @@ describe("HelloTests", function () { .expect(200); }); }); -``` \ No newline at end of file +``` diff --git a/src/testing.ts b/src/testing.ts index 16f465f3b..b30d1572f 100644 --- a/src/testing.ts +++ b/src/testing.ts @@ -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 @@ -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( @@ -56,6 +73,6 @@ export const getTestServer = (functionName: string): Server => { target: '', sourceLocation: '', printHelp: false, - ignoredRoutes: null, + ignoredRoutes: options.ignoredRoutes ?? null, }); }; diff --git a/test/integration/http.ts b/test/integration/http.ts index ec78d4f91..3c2f4f678 100644 --- a/test/integration/http.ts +++ b/test/integration/http.ts @@ -117,4 +117,13 @@ describe('HTTP Function', () => { assert.strictEqual(callCount, test.expectedCallCount); }); }); + + it('can disable the default ignored routes', async () => { + const response = await supertest( + getTestServer('testHttpFunction', {ignoredRoutes: ''}), + ).get('/favicon.ico'); + + assert.strictEqual(response.status, 200); + assert.strictEqual(callCount, 1); + }); }); From ced228f5c789ade721a38feb10f94ef54b9c3c37 Mon Sep 17 00:00:00 2001 From: hugosmoreira Date: Sun, 20 Sep 2026 09:20:25 -0700 Subject: [PATCH 2/2] test(testing): cover ignored routes and packaged helper --- test/integration/http.ts | 70 +++++++++++++++++++++++++++++---- test/system-test/pack_n_play.ts | 56 ++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 7 deletions(-) diff --git a/test/integration/http.ts b/test/integration/http.ts index 3c2f4f678..3482a9ef4 100644 --- a/test/integration/http.ts +++ b/test/integration/http.ts @@ -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; @@ -118,12 +118,68 @@ describe('HTTP Function', () => { }); }); - it('can disable the default ignored routes', async () => { - const response = await supertest( - getTestServer('testHttpFunction', {ignoredRoutes: ''}), - ).get('/favicon.ico'); + 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/, + ); + }); - assert.strictEqual(response.status, 200); - assert.strictEqual(callCount, 1); + it('propagates an invalid ignored route expression', () => { + assert.throws( + () => getTestServer('testHttpFunction', {ignoredRoutes: '['}), + TypeError, + ); }); }); diff --git a/test/system-test/pack_n_play.ts b/test/system-test/pack_n_play.ts index 96fd46b1d..2bea194cd 100644 --- a/test/system-test/pack_n_play.ts +++ b/test/system-test/pack_n_play.ts @@ -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; }); + `, + }, + }); + }); });