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
43 changes: 34 additions & 9 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
- pull_request
- push

permissions:
contents: read

env:
CI: true

Expand All @@ -15,22 +18,44 @@ jobs:
node-version:
- 22.x
- 24.x
firebase-functions:
- pinned
- '7'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3.7.0
- uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1
with:
node-version: ${{ matrix.node-version }}

- name: Cache npm
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}

- run: npm ci
- run: npm run lint
- run: npm run format
- run: npm run test
- name: Lint
if: matrix.firebase-functions == 'pinned'
run: npm run lint
- name: Format
if: matrix.firebase-functions == 'pinned'
run: npm run format
- name: Run tests (pinned firebase-functions)
if: matrix.firebase-functions == 'pinned'
run: npm run test
# Specs do not compile against firebase-functions v7 typings, so
# compile against the pinned version, swap in v7, and run the
# compiled output. This mirrors how users hit version drift: their
# compiled code running against a newer firebase-functions.
- name: Compile specs against pinned firebase-functions
if: matrix.firebase-functions == '7'
run: npx tsc
- name: Swap in firebase-functions@7
if: matrix.firebase-functions == '7'
run: npm install --no-save firebase-functions@^7
- name: Run tests (firebase-functions@7)
if: matrix.firebase-functions == '7'
run: npx mocha .tmp/spec/index.spec.js
integration:
runs-on: ubuntu-latest
strategy:
Expand All @@ -39,13 +64,13 @@ jobs:
- 22.x
- 24.x
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3.7.0
- uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3.9.1
with:
node-version: ${{ matrix.node-version }}

- name: Cache npm
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- chore: drop support for Node 18 and below (minimum supported version is now Node 20)
- fix: mockConfig() now throws a clear migration error when firebase-functions v7+ is installed, since functions.config() was removed upstream (#334)
16 changes: 14 additions & 2 deletions spec/lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,20 @@ import { expect } from 'chai';

import { FirebaseFunctionsTest } from '../src/lifecycle';
import { mockConfig } from '../src/main';
import { _isConfigRemoved } from '../src/v1';
import { afterEach } from 'mocha';

// mockConfig() throws on firebase-functions v7+ because functions.config()
// was removed. These tests only care about CLOUD_RUNTIME_CONFIG being
// restored by cleanup(), so set the variable directly on v7+.
function setRuntimeConfig(conf: { [key: string]: { [key: string]: any } }) {
if (_isConfigRemoved()) {
process.env.CLOUD_RUNTIME_CONFIG = JSON.stringify(conf);
} else {
mockConfig(conf);
}
}

describe('lifecycle', () => {
describe('#init', () => {
let test;
Expand Down Expand Up @@ -87,7 +99,7 @@ describe('lifecycle', () => {
it('deletes all the env variables if they did not previously exist', () => {
let test = new FirebaseFunctionsTest();
test.init();
mockConfig({ foo: { bar: 'faz ' } });
setRuntimeConfig({ foo: { bar: 'faz ' } });
test.cleanup();
expect(process.env.FIREBASE_CONFIG).to.be.undefined;
expect(process.env.GCLOUD_PROJECT).to.be.undefined;
Expand All @@ -103,7 +115,7 @@ describe('lifecycle', () => {
let test = new FirebaseFunctionsTest();

test.init();
mockConfig({ foo: { bar: 'faz ' } });
setRuntimeConfig({ foo: { bar: 'faz ' } });
test.cleanup();

expect(process.env.FIREBASE_CONFIG).to.equal('oldFb');
Expand Down
37 changes: 26 additions & 11 deletions spec/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import * as functions from 'firebase-functions/v1';
import { set } from 'lodash';

import { mockConfig, makeChange, wrap } from '../src/main';
import { _makeResourceName, _extractParams } from '../src/v1';
import { _makeResourceName, _extractParams, _isConfigRemoved } from '../src/v1';
import { features } from '../src/features';
import { FirebaseFunctionsTest } from '../src/lifecycle';
import { alerts } from 'firebase-functions/v2';
Expand Down Expand Up @@ -329,17 +329,32 @@ describe('main', () => {
delete process.env.CLOUD_RUNTIME_CONFIG;
});

it('should mock functions.config()', () => {
mockConfig(config);
expect(functions.config()).to.deep.equal(config);
});
if (_isConfigRemoved()) {
// functions.config() was removed in firebase-functions v7, so
// mockConfig() must fail loudly with migration guidance.
it('should throw explaining that functions.config() was removed', () => {
expect(() => mockConfig(config)).to.throw(
'mockConfig() is not supported with firebase-functions v7+'
);
});

it('should not set CLOUD_RUNTIME_CONFIG when throwing', () => {
expect(() => mockConfig(config)).to.throw();
expect(process.env.CLOUD_RUNTIME_CONFIG).to.be.undefined;
});
} else {
it('should mock functions.config()', () => {
mockConfig(config);
expect(functions.config()).to.deep.equal(config);
});

it('should purge singleton config object when it is present', () => {
mockConfig(config);
config.foo = { baz: 'qux' };
mockConfig(config);
it('should purge singleton config object when it is present', () => {
mockConfig(config);
config.foo = { baz: 'qux' };
mockConfig(config);

expect(functions.config()).to.deep.equal(config);
});
expect(functions.config()).to.deep.equal(config);
});
}
});
});
94 changes: 94 additions & 0 deletions src/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import * as fs from 'fs';
import * as path from 'path';

import { has, merge, random, get } from 'lodash';

import {
Expand Down Expand Up @@ -369,8 +372,99 @@ export function makeChange<T>(before: T, after: T): Change<T> {
return Change.fromObjects(before, after);
}

/**
* Detects the major version of the installed firebase-functions package.
* Exported for internal testing purposes only.
* @internal
*/
export function _firebaseFunctionsMajorVersion(): number | undefined {
try {
// `firebase-functions/package.json` is not exposed by the package's
// exports map, so resolve the main entry point and walk up to the
// package root instead.
let dir = path.dirname(require.resolve('firebase-functions'));
while (dir !== path.dirname(dir)) {
const pkgPath = path.join(dir, 'package.json');
if (fs.existsSync(pkgPath)) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
if (
pkg.name === 'firebase-functions' &&
typeof pkg.version === 'string'
) {
const major = parseInt(pkg.version.split('.')[0], 10);
if (!Number.isNaN(major)) {
return major;
}
}
}
dir = path.dirname(dir);
}
} catch (e) {
// Fall through to feature detection.
}
return undefined;
}
Comment thread
cabljac marked this conversation as resolved.

let isConfigRemovedCache: boolean | undefined;

/**
* Returns true if the installed firebase-functions no longer supports
* `functions.config()` (removed in v7). The result is cached, since the
* installed version cannot change within a process.
* Exported for internal testing purposes only.
* @internal
*/
export function _isConfigRemoved(): boolean {
if (isConfigRemovedCache === undefined) {
isConfigRemovedCache = detectConfigRemoved();
}
return isConfigRemovedCache;
}

function detectConfigRemoved(): boolean {
if (typeof config !== 'function') {
return true;
}
const major = _firebaseFunctionsMajorVersion();
if (major !== undefined) {
return major >= 7;
}
// Fallback: feature-detect on the v1 entry point. In v7+, config()
// throws unconditionally; in v4-v6 it parses CLOUD_RUNTIME_CONFIG.
// K_CONFIGURATION must be cleared for the probe, since v4-v6's config()
// also throws unconditionally when it is set (GCFv2 detection).
const previous = process.env.CLOUD_RUNTIME_CONFIG;
const previousKConfiguration = process.env.K_CONFIGURATION;
process.env.CLOUD_RUNTIME_CONFIG = '{}';
delete process.env.K_CONFIGURATION;
try {
config();
return false;
} catch (e) {
return true;
} finally {
if (previous === undefined) {
delete process.env.CLOUD_RUNTIME_CONFIG;
} else {
process.env.CLOUD_RUNTIME_CONFIG = previous;
}
if (previousKConfiguration === undefined) {
delete process.env.K_CONFIGURATION;
} else {
process.env.K_CONFIGURATION = previousKConfiguration;
}
}
}
Comment thread
cabljac marked this conversation as resolved.

/** Mock values returned by `functions.config()`. */
export function mockConfig(conf: { [key: string]: { [key: string]: any } }) {
if (_isConfigRemoved()) {
throw new Error(
'mockConfig() is not supported with firebase-functions v7+ because ' +
'functions.config() was removed. Migrate to environment parameters ' +
'using the params module.'
);
}
if (resetCache) {
resetCache();
}
Expand Down
Loading