From c8dc2c10275ca1bd55d8c1b5dc82c21cefb57a0c Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Mon, 23 Aug 2021 17:41:40 -0700 Subject: [PATCH] Implement rest of RUTv2 features. (#5360) * Implement loading rules and withFunctionTriggersDisabled. * Implement clearFirestore and storage. * Add missing await. * Add default bucketUrl. * Use alternative method to clear bucket. * Use default param (review feedback). --- packages/messaging/src/api.ts | 39 +++++++- packages/messaging/src/helpers/register.ts | 36 ------- .../rules-unit-testing/src/impl/discovery.ts | 33 +------ packages/rules-unit-testing/src/impl/rules.ts | 89 +++++++++++++++++ .../src/impl/test_environment.ts | 31 +++++- packages/rules-unit-testing/src/impl/url.ts | 55 +++++++++++ packages/rules-unit-testing/src/initialize.ts | 32 ++++++- packages/rules-unit-testing/src/util.ts | 66 ++++++++++++- .../test/impl/discovery.test.ts | 22 +---- .../test/impl/rules.test.ts | 95 +++++++++++++++++++ .../rules-unit-testing/test/test_utils.ts | 37 ++++++++ packages/rules-unit-testing/test/util.test.ts | 49 +++++++++- yarn.lock | 16 ---- 13 files changed, 482 insertions(+), 118 deletions(-) create mode 100644 packages/rules-unit-testing/src/impl/rules.ts create mode 100644 packages/rules-unit-testing/src/impl/url.ts create mode 100644 packages/rules-unit-testing/test/impl/rules.test.ts create mode 100644 packages/rules-unit-testing/test/test_utils.ts diff --git a/packages/messaging/src/api.ts b/packages/messaging/src/api.ts index 56af3928503..821c2fe759c 100644 --- a/packages/messaging/src/api.ts +++ b/packages/messaging/src/api.ts @@ -31,9 +31,11 @@ import { import { MessagingService } from './messaging-service'; import { deleteToken as _deleteToken } from './api/deleteToken'; import { getToken as _getToken } from './api/getToken'; +import { isSwSupported, isWindowSupported } from './api/isSupported'; import { onBackgroundMessage as _onBackgroundMessage } from './api/onBackgroundMessage'; import { onMessage as _onMessage } from './api/onMessage'; import { _setDeliveryMetricsExportedToBigQueryEnabled } from './api/setDeliveryMetricsExportedToBigQueryEnabled'; +import { ERROR_FACTORY, ErrorCode } from './util/errors'; /** * Retrieves a Firebase Cloud Messaging instance. @@ -43,6 +45,22 @@ import { _setDeliveryMetricsExportedToBigQueryEnabled } from './api/setDeliveryM * @public */ export function getMessagingInWindow(app: FirebaseApp = getApp()): Messaging { + // Conscious decision to make this async check non-blocking during the messaging instance + // initialization phase for performance consideration. An error would be thrown latter for + // developer's information. Developers can then choose to import and call `isSupported` for + // special handling. + isWindowSupported().then( + isSupported => { + // If `isWindowSupported()` resolved, but returned false. + if (!isSupported) { + throw ERROR_FACTORY.create(ErrorCode.UNSUPPORTED_BROWSER); + } + }, + _ => { + // If `isWindowSupported()` rejected. + throw ERROR_FACTORY.create(ErrorCode.INDEXED_DB_UNSUPPORTED); + } + ); return _getProvider(getModularInstance(app), 'messaging').getImmediate(); } @@ -54,10 +72,23 @@ export function getMessagingInWindow(app: FirebaseApp = getApp()): Messaging { * @public */ export function getMessagingInSw(app: FirebaseApp = getApp()): Messaging { - return _getProvider( - getModularInstance(app), - 'messaging-sw' - ).getImmediate(); + // Conscious decision to make this async check non-blocking during the messaging instance + // initialization phase for performance consideration. An error would be thrown latter for + // developer's information. Developers can then choose to import and call `isSupported` for + // special handling. + isSwSupported().then( + isSupported => { + // If `isSwSupported()` resolved, but returned false. + if (!isSupported) { + throw ERROR_FACTORY.create(ErrorCode.UNSUPPORTED_BROWSER); + } + }, + _ => { + // If `isSwSupported()` rejected. + throw ERROR_FACTORY.create(ErrorCode.INDEXED_DB_UNSUPPORTED); + } + ); + return _getProvider(getModularInstance(app), 'messaging-sw').getImmediate(); } /** diff --git a/packages/messaging/src/helpers/register.ts b/packages/messaging/src/helpers/register.ts index d85e0318e15..97c7dfb6327 100644 --- a/packages/messaging/src/helpers/register.ts +++ b/packages/messaging/src/helpers/register.ts @@ -21,8 +21,6 @@ import { ComponentType, InstanceFactory } from '@firebase/component'; -import { ERROR_FACTORY, ErrorCode } from '../util/errors'; -import { isSwSupported, isWindowSupported } from '../api/isSupported'; import { onNotificationClick, onPush, @@ -40,8 +38,6 @@ import { messageEventListener } from '../listeners/window-listener'; const WindowMessagingFactory: InstanceFactory<'messaging'> = ( container: ComponentContainer ) => { - maybeThrowWindowError(); - const messaging = new MessagingService( container.getProvider('app').getImmediate(), container.getProvider('installations-internal').getImmediate(), @@ -58,8 +54,6 @@ const WindowMessagingFactory: InstanceFactory<'messaging'> = ( const WindowMessagingInternalFactory: InstanceFactory<'messaging-internal'> = ( container: ComponentContainer ) => { - maybeThrowWindowError(); - const messaging = container .getProvider('messaging') .getImmediate() as MessagingService; @@ -71,40 +65,10 @@ const WindowMessagingInternalFactory: InstanceFactory<'messaging-internal'> = ( return messagingInternal; }; -function maybeThrowWindowError(): void { - // Conscious decision to make this async check non-blocking during the messaging instance - // initialization phase for performance consideration. An error would be thrown latter for - // developer's information. Developers can then choose to import and call `isSupported` for - // special handling. - isWindowSupported() - .then(isSupported => { - if (!isSupported) { - throw ERROR_FACTORY.create(ErrorCode.UNSUPPORTED_BROWSER); - } - }) - .catch(_ => { - throw ERROR_FACTORY.create(ErrorCode.INDEXED_DB_UNSUPPORTED); - }); -} - declare const self: ServiceWorkerGlobalScope; const SwMessagingFactory: InstanceFactory<'messaging'> = ( container: ComponentContainer ) => { - // Conscious decision to make this async check non-blocking during the messaging instance - // initialization phase for performance consideration. An error would be thrown latter for - // developer's information. Developers can then choose to import and call `isSupported` for - // special handling. - isSwSupported() - .then(isSupported => { - if (!isSupported) { - throw ERROR_FACTORY.create(ErrorCode.UNSUPPORTED_BROWSER); - } - }) - .catch(_ => { - throw ERROR_FACTORY.create(ErrorCode.INDEXED_DB_UNSUPPORTED); - }); - const messaging = new MessagingService( container.getProvider('app').getImmediate(), container.getProvider('installations-internal').getImmediate(), diff --git a/packages/rules-unit-testing/src/impl/discovery.ts b/packages/rules-unit-testing/src/impl/discovery.ts index d539535ff77..9798f017241 100644 --- a/packages/rules-unit-testing/src/impl/discovery.ts +++ b/packages/rules-unit-testing/src/impl/discovery.ts @@ -17,6 +17,7 @@ import { EmulatorConfig, HostAndPort } from '../public_types'; import nodeFetch from 'node-fetch'; +import { makeUrl, fixHostname } from './url'; /** * Use the Firebase Emulator hub to discover other running emulators. @@ -79,20 +80,6 @@ export interface DiscoveredEmulators { hub?: HostAndPort; } -function makeUrl(hostAndPort: HostAndPort | string, path: string): URL { - if (typeof hostAndPort === 'object') { - const { host, port } = hostAndPort; - if (host.includes(':')) { - hostAndPort = `[${host}]:${port}`; - } else { - hostAndPort = `${host}:${port}`; - } - } - const url = new URL(`http://${hostAndPort}/`); - url.pathname = path; - return url; -} - /** * @private */ @@ -169,21 +156,3 @@ function emulatorFromEnvVar(envVar: string): HostAndPort | undefined { } return { host, port }; } - -/** - * Return a connectable hostname, replacing wildcard 0.0.0.0 or :: with loopback - * addresses 127.0.0.1 / ::1 correspondingly. See below for why this is needed: - * https://github.com/firebase/firebase-tools-ui/issues/286 - * - * This assumes emulators are running on the same device as the Emulator UI - * server, which should hold if both are started from the same CLI command. - */ -function fixHostname(host: string, fallbackHost?: string): string { - host = host.replace('[', '').replace(']', ''); // Remove IPv6 brackets - if (host === '0.0.0.0') { - host = fallbackHost || '127.0.0.1'; - } else if (host === '::') { - host = fallbackHost || '::1'; - } - return host; -} diff --git a/packages/rules-unit-testing/src/impl/rules.ts b/packages/rules-unit-testing/src/impl/rules.ts new file mode 100644 index 00000000000..7d4d900367f --- /dev/null +++ b/packages/rules-unit-testing/src/impl/rules.ts @@ -0,0 +1,89 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HostAndPort } from '../public_types'; +import { makeUrl } from './url'; +import fetch from 'node-fetch'; + +/** + * @private + */ +export async function loadDatabaseRules( + hostAndPort: HostAndPort, + databaseName: string, + rules: string +): Promise { + const url = makeUrl(hostAndPort, '/.settings/rules.json'); + url.searchParams.append('ns', databaseName); + const resp = await fetch(url, { + method: 'PUT', + headers: { Authorization: 'Bearer owner' }, + body: rules + }); + + if (!resp.ok) { + throw new Error(await resp.text()); + } +} + +/** + * @private + */ +export async function loadFirestoreRules( + hostAndPort: HostAndPort, + projectId: string, + rules: string +): Promise { + const resp = await fetch( + makeUrl(hostAndPort, `/emulator/v1/projects/${projectId}:securityRules`), + { + method: 'PUT', + body: JSON.stringify({ + rules: { + files: [{ content: rules }] + } + }) + } + ); + + if (!resp.ok) { + throw new Error(await resp.text()); + } +} + +/** + * @private + */ +export async function loadStorageRules( + hostAndPort: HostAndPort, + rules: string +): Promise { + const resp = await fetch(makeUrl(hostAndPort, '/internal/setRules'), { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + rules: { + files: [{ name: 'storage.rules', content: rules }] + } + }) + }); + if (!resp.ok) { + throw new Error(await resp.text()); + } +} diff --git a/packages/rules-unit-testing/src/impl/test_environment.ts b/packages/rules-unit-testing/src/impl/test_environment.ts index d75578d3e7b..70b2abf9681 100644 --- a/packages/rules-unit-testing/src/impl/test_environment.ts +++ b/packages/rules-unit-testing/src/impl/test_environment.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import fetch from 'node-fetch'; import firebase from 'firebase/compat/app'; import 'firebase/firestore/compat'; import 'firebase/database/compat'; @@ -28,6 +29,7 @@ import { } from '../public_types'; import { DiscoveredEmulators } from './discovery'; +import { makeUrl } from './url'; /** * An implementation of {@code RulesTestEnvironment}. This is private to hide the constructor, @@ -100,14 +102,35 @@ export class RulesTestEnvironmentImpl implements RulesTestEnvironment { }); } - clearFirestore(): Promise { + async clearFirestore(): Promise { this.checkNotDestroyed(); - throw new Error('Method not implemented.'); + assertEmulatorRunning(this.emulators, 'firestore'); + + const resp = await fetch( + makeUrl( + this.emulators.firestore, + `/emulator/v1/projects/${this.projectId}/databases/(default)/documents` + ), + { + method: 'DELETE' + } + ); + + if (!resp.ok) { + throw new Error(await resp.text()); + } } clearStorage(): Promise { this.checkNotDestroyed(); - throw new Error('Method not implemented.'); + return this.withSecurityRulesDisabled(async context => { + const { items } = await context.storage().ref().listAll(); + await Promise.all( + items.map(item => { + return item.delete(); + }) + ); + }); } async cleanup(): Promise { @@ -177,7 +200,7 @@ class RulesTestContextImpl implements RulesTestContext { ); return database; } - storage(bucketUrl?: string): firebase.storage.Storage { + storage(bucketUrl = `gs://${this.projectId}`): firebase.storage.Storage { assertEmulatorRunning(this.emulators, 'storage'); const storage = this.getApp().storage(bucketUrl); storage.useEmulator( diff --git a/packages/rules-unit-testing/src/impl/url.ts b/packages/rules-unit-testing/src/impl/url.ts new file mode 100644 index 00000000000..5ab1c155c19 --- /dev/null +++ b/packages/rules-unit-testing/src/impl/url.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HostAndPort } from '../public_types'; + +/** + * Return a connectable hostname, replacing wildcard 0.0.0.0 or :: with loopback + * addresses 127.0.0.1 / ::1 correspondingly. See below for why this is needed: + * https://github.com/firebase/firebase-tools-ui/issues/286 + * + * This assumes emulators are running on the same device as fallbackHost (e.g. + * hub), which should hold if both are started from the same CLI command. + * @private + */ +export function fixHostname(host: string, fallbackHost?: string): string { + host = host.replace('[', '').replace(']', ''); // Remove IPv6 brackets + if (host === '0.0.0.0') { + host = fallbackHost || '127.0.0.1'; + } else if (host === '::') { + host = fallbackHost || '::1'; + } + return host; +} + +/** + * Create a URL with host, port, and path. Handles IPv6 bracketing correctly. + * @private + */ +export function makeUrl(hostAndPort: HostAndPort | string, path: string): URL { + if (typeof hostAndPort === 'object') { + const { host, port } = hostAndPort; + if (host.includes(':')) { + hostAndPort = `[${host}]:${port}`; + } else { + hostAndPort = `${host}:${port}`; + } + } + const url = new URL(`http://${hostAndPort}/`); + url.pathname = path; + return url; +} diff --git a/packages/rules-unit-testing/src/initialize.ts b/packages/rules-unit-testing/src/initialize.ts index 9d7b3bc9dfc..c86c67a9ce0 100644 --- a/packages/rules-unit-testing/src/initialize.ts +++ b/packages/rules-unit-testing/src/initialize.ts @@ -21,7 +21,15 @@ import { discoverEmulators, getEmulatorHostAndPort } from './impl/discovery'; -import { RulesTestEnvironmentImpl } from './impl/test_environment'; +import { + assertEmulatorRunning, + RulesTestEnvironmentImpl +} from './impl/test_environment'; +import { + loadDatabaseRules, + loadFirestoreRules, + loadStorageRules +} from './impl/rules'; /** * Initializes a test environment for rules unit testing. Call this function first for test setup. @@ -73,7 +81,27 @@ export async function initializeTestEnvironment( } } - // TODO: Set security rules. + if (config.database?.rules) { + assertEmulatorRunning(emulators, 'database'); + await loadDatabaseRules( + emulators.database, + projectId, + config.database.rules + ); + } + if (config.firestore?.rules) { + assertEmulatorRunning(emulators, 'firestore'); + await loadFirestoreRules( + emulators.firestore, + projectId, + config.firestore.rules + ); + } + if (config.storage?.rules) { + assertEmulatorRunning(emulators, 'storage'); + await loadStorageRules(emulators.storage, config.storage.rules); + } + return new RulesTestEnvironmentImpl(projectId, emulators); } diff --git a/packages/rules-unit-testing/src/util.ts b/packages/rules-unit-testing/src/util.ts index 49b448b0913..437cefcf6b5 100644 --- a/packages/rules-unit-testing/src/util.ts +++ b/packages/rules-unit-testing/src/util.ts @@ -15,6 +15,14 @@ * limitations under the License. */ +import { + EMULATOR_HOST_ENV_VARS, + getEmulatorHostAndPort +} from './impl/discovery'; +import { fixHostname, makeUrl } from './impl/url'; +import { HostAndPort } from './public_types'; +import fetch from 'node-fetch'; + /** * Run a setup function with background Cloud Functions triggers disabled. This can be used to * import data into the Realtime Database or Cloud Firestore emulator without triggering locally @@ -41,7 +49,8 @@ export async function withFunctionTriggersDisabled( * @param fn an function which may be sync or async (returns a promise) * @param hub the host and port of the Emulator Hub (ex: `{host: 'localhost', port: 4400}`) * @public - */ export async function withFunctionTriggersDisabled( + */ +export async function withFunctionTriggersDisabled( hub: { host: string; port: number }, fn: () => TResult | Promise ): Promise; @@ -50,7 +59,60 @@ export async function withFunctionTriggersDisabled( fnOrHub: { host: string; port: number } | (() => TResult | Promise), maybeFn?: () => TResult | Promise ): Promise { - throw new Error('unimplemented'); + let hub: HostAndPort | undefined; + if (typeof fnOrHub === 'function') { + maybeFn = fnOrHub; + hub = getEmulatorHostAndPort('hub'); + } else { + hub = getEmulatorHostAndPort('hub', fnOrHub); + if (!maybeFn) { + throw new Error('The callback function must be specified!'); + } + } + if (!hub) { + throw new Error( + 'Please specify the Emulator Hub host and port via arguments or set the environment ' + + `varible ${EMULATOR_HOST_ENV_VARS.hub}!` + ); + } + + hub.host = fixHostname(hub.host); + makeUrl(hub, '/functions/disableBackgroundTriggers'); + // Disable background triggers + const disableRes = await fetch( + makeUrl(hub, '/functions/disableBackgroundTriggers'), + { + method: 'PUT' + } + ); + if (!disableRes.ok) { + throw new Error( + `HTTP Error ${disableRes.status} when disabling functions triggers, are you using firebase-tools 8.13.0 or higher?` + ); + } + + // Run the user's function + let result: TResult | undefined = undefined; + try { + result = await maybeFn(); + } finally { + // Re-enable background triggers + const enableRes = await fetch( + makeUrl(hub, '/functions/enableBackgroundTriggers'), + { + method: 'PUT' + } + ); + + if (!enableRes.ok) { + throw new Error( + `HTTP Error ${enableRes.status} when enabling functions triggers, are you using firebase-tools 8.13.0 or higher?` + ); + } + } + + // Return the user's function result + return result; } /** diff --git a/packages/rules-unit-testing/test/impl/discovery.test.ts b/packages/rules-unit-testing/test/impl/discovery.test.ts index 4ef8773e778..b3f3db86307 100644 --- a/packages/rules-unit-testing/test/impl/discovery.test.ts +++ b/packages/rules-unit-testing/test/impl/discovery.test.ts @@ -17,13 +17,13 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { mock } from 'sinon'; import { discoverEmulators, EMULATOR_HOST_ENV_VARS, getEmulatorHostAndPort } from '../../src/impl/discovery'; import { HostAndPort } from '../../src/public_types'; +import { restoreEnvVars, stashEnvVars } from '../test_utils'; describe('discoverEmulators()', () => { it('finds all running emulators', async () => { @@ -335,23 +335,3 @@ describe('getEmulatorHostAndPort()', () => { }); }); }); - -let envVars: Record; -function stashEnvVars() { - envVars = {}; - for (const envVar of Object.values(EMULATOR_HOST_ENV_VARS)) { - envVars[envVar] = process.env[envVar]; - delete process.env[envVar]; - } -} - -function restoreEnvVars() { - envVars = {}; - for (const envVar of Object.values(EMULATOR_HOST_ENV_VARS)) { - if (envVars[envVar] === undefined) { - delete process.env[envVar]; - } else { - process.env[envVar] = envVars[envVar]; - } - } -} diff --git a/packages/rules-unit-testing/test/impl/rules.test.ts b/packages/rules-unit-testing/test/impl/rules.test.ts new file mode 100644 index 00000000000..3d6fe512ef7 --- /dev/null +++ b/packages/rules-unit-testing/test/impl/rules.test.ts @@ -0,0 +1,95 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { getEmulatorHostAndPort } from '../../src/impl/discovery'; +import { + loadDatabaseRules, + loadFirestoreRules, + loadStorageRules +} from '../../src/impl/rules'; + +describe('loadDatabaseRules()', () => { + it('succeeds on valid input', async function () { + await loadDatabaseRules( + getEmulatorHostAndPort('database')!, + 'foo', + '{ "rules": {} }' + ); + }); + it('fails on invalid input', async function () { + await expect( + loadDatabaseRules( + getEmulatorHostAndPort('database')!, + 'foo', + 'invalid json %{!@[' + ) + ).to.be.rejectedWith(/Parse error/); + }); +}); + +describe('loadFirestoreRules()', () => { + it('loadFirestoreRules() succeeds on valid input', async function () { + await loadFirestoreRules( + getEmulatorHostAndPort('firestore')!, + 'foo', + `service cloud.firestore { + match /databases/{db}/documents/{doc=**} { + allow read, write; + } + }` + ); + }); + it('fails on invalid input', async function () { + await expect( + loadFirestoreRules( + getEmulatorHostAndPort('firestore')!, + 'foo', + `rules_version = '2'; + service cloud.firestore { + banana + }` + ) + ).to.be.rejectedWith(/INVALID_ARGUMENT/); + }); +}); + +describe('loadStorageRules()', () => { + it('loadStorageRules() succeeds on valid input', async function () { + await loadStorageRules( + getEmulatorHostAndPort('storage')!, + `service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if false; + } + } + }` + ); + }); + it('fails on invalid input', async function () { + await expect( + loadStorageRules( + getEmulatorHostAndPort('storage')!, + `rules_version = '2'; + service firebase.storage { + banana + }` + ) + ).to.be.rejectedWith(/error updating rules/); + }); +}); diff --git a/packages/rules-unit-testing/test/test_utils.ts b/packages/rules-unit-testing/test/test_utils.ts new file mode 100644 index 00000000000..b882432f30d --- /dev/null +++ b/packages/rules-unit-testing/test/test_utils.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EMULATOR_HOST_ENV_VARS } from '../src/impl/discovery'; + +let envVars: Record; +export function stashEnvVars() { + envVars = {}; + for (const envVar of Object.values(EMULATOR_HOST_ENV_VARS)) { + envVars[envVar] = process.env[envVar]; + delete process.env[envVar]; + } +} + +export function restoreEnvVars() { + for (const envVar of Object.values(EMULATOR_HOST_ENV_VARS)) { + if (envVars[envVar] === undefined) { + delete process.env[envVar]; + } else { + process.env[envVar] = envVars[envVar]; + } + } +} diff --git a/packages/rules-unit-testing/test/util.test.ts b/packages/rules-unit-testing/test/util.test.ts index 5db60441e9d..9dc778fb93e 100644 --- a/packages/rules-unit-testing/test/util.test.ts +++ b/packages/rules-unit-testing/test/util.test.ts @@ -15,7 +15,14 @@ * limitations under the License. */ -import { assertFails, assertSucceeds } from '../src/util'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { + assertFails, + assertSucceeds, + withFunctionTriggersDisabled +} from '../src/util'; +import { restoreEnvVars, stashEnvVars } from './test_utils'; describe('assertSucceeds()', () => { it('returns a fulfilled promise iff success', async function () { @@ -155,3 +162,43 @@ describe('assertFails()', () => { .catch(() => {}); }); }); + +describe('withFunctionTriggersDisabled()', () => { + it('disabling function triggers does not throw, returns value', async function () { + const fetchSpy = sinon.spy(require('node-fetch'), 'default'); + + const res = await withFunctionTriggersDisabled(() => { + return Promise.resolve(1234); + }); + + expect(res).to.eq(1234); + expect(fetchSpy.callCount).to.equal(2); + }); + + it('disabling function triggers always re-enables, event when the function throws', async function () { + const fetchSpy = sinon.spy(require('node-fetch'), 'default'); + + const res = withFunctionTriggersDisabled(() => { + throw new Error('I throw!'); + }); + + await expect(res).to.eventually.be.rejectedWith('I throw!'); + expect(fetchSpy.callCount).to.equal(2); + }); + + context('without env vars', () => { + beforeEach(() => { + stashEnvVars(); + }); + afterEach(() => { + restoreEnvVars(); + }); + it('throws if hub is not specified', async function () { + await expect( + withFunctionTriggersDisabled(() => { + return Promise.resolve(1234); + }) + ).to.rejectedWith(/specify the Emulator Hub host and port/); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index c98da845cdf..4ba4aec4bde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5675,13 +5675,6 @@ concat-stream@^2.0.0: readable-stream "^3.0.2" typedarray "^0.0.6" -concat-with-sourcemaps@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e" - integrity sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg== - dependencies: - source-map "^0.6.1" - config-chain@^1.1.12: version "1.1.13" resolved "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" @@ -8742,15 +8735,6 @@ gulp-cli@^2.2.0: v8flags "^3.2.0" yargs "^7.1.0" -gulp-concat@2.6.1: - version "2.6.1" - resolved "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz#633d16c95d88504628ad02665663cee5a4793353" - integrity sha1-Yz0WyV2IUEYorQJmVmPO5aR5M1M= - dependencies: - concat-with-sourcemaps "^1.0.0" - through2 "^2.0.0" - vinyl "^2.0.0" - gulp-filter@7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/gulp-filter/-/gulp-filter-7.0.0.tgz#e0712f3e57b5d647f802a1880255cafb54abf158"