UNPKG

voluptasmollitia

Version:
735 lines (633 loc) 19.9 kB
/** * @license * Copyright 2018 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 firebase from 'firebase'; import 'firebase/database'; import 'firebase/firestore'; import 'firebase/storage'; import type { app } from 'firebase-admin'; import { _FirebaseApp } from '@firebase/app-types/private'; import { FirebaseAuthInternal } from '@firebase/auth-interop-types'; import * as request from 'request'; import { base64 } from '@firebase/util'; import { setLogLevel, LogLevel } from '@firebase/logger'; import { Component, ComponentType } from '@firebase/component'; const { firestore, database, storage } = firebase; export { firestore, database, storage }; /** If this environment variable is set, use it for the database emulator's address. */ const DATABASE_ADDRESS_ENV: string = 'FIREBASE_DATABASE_EMULATOR_HOST'; /** The default address for the local database emulator. */ const DATABASE_ADDRESS_DEFAULT: string = 'localhost:9000'; /** If this environment variable is set, use it for the Firestore emulator. */ const FIRESTORE_ADDRESS_ENV: string = 'FIRESTORE_EMULATOR_HOST'; /** The default address for the local Firestore emulator. */ const FIRESTORE_ADDRESS_DEFAULT: string = 'localhost:8080'; /** If this environment variable is set, use it for the Storage emulator. */ const FIREBASE_STORAGE_ADDRESS_ENV: string = 'FIREBASE_STORAGE_EMULATOR_HOST'; const CLOUD_STORAGE_ADDRESS_ENV: string = 'STORAGE_EMULATOR_HOST'; /** The default address for the local Firestore emulator. */ const STORAGE_ADDRESS_DEFAULT: string = 'localhost:9199'; /** Environment variable to locate the Emulator Hub */ const HUB_HOST_ENV: string = 'FIREBASE_EMULATOR_HUB'; /** The default address for the Emulator Hub */ const HUB_HOST_DEFAULT: string = 'localhost:4400'; /** The actual address for the database emulator */ let _databaseHost: string | undefined = undefined; /** The actual address for the Firestore emulator */ let _firestoreHost: string | undefined = undefined; /** The actual address for the Storage emulator */ let _storageHost: string | undefined = undefined; /** The actual address for the Emulator Hub */ let _hubHost: string | undefined = undefined; export type Provider = | 'custom' | 'email' | 'password' | 'phone' | 'anonymous' | 'google.com' | 'facebook.com' | 'github.com' | 'twitter.com' | 'microsoft.com' | 'apple.com'; export type FirebaseIdToken = { // Always set to https://securetoken.google.com/PROJECT_ID iss: string; // Always set to PROJECT_ID aud: string; // The user's unique id sub: string; // The token issue time, in seconds since epoch iat: number; // The token expiry time, normally 'iat' + 3600 exp: number; // The user's unique id, must be equal to 'sub' user_id: string; // The time the user authenticated, normally 'iat' auth_time: number; // The sign in provider, only set when the provider is 'anonymous' provider_id?: 'anonymous'; // The user's primary email email?: string; // The user's email verification status email_verified?: boolean; // The user's primary phone number phone_number?: string; // The user's display name name?: string; // The user's profile photo URL picture?: string; // Information on all identities linked to this user firebase: { // The primary sign-in provider sign_in_provider: Provider; // A map of providers to the user's list of unique identifiers from // each provider identities?: { [provider in Provider]?: string[] }; }; // Custom claims set by the developer [claim: string]: any; }; // To avoid a breaking change, we accept the 'uid' option here, but // new users should prefer 'sub' instead. export type TokenOptions = Partial<FirebaseIdToken> & { uid?: string }; /** * Host/port configuration for applicable Firebase Emulators. */ export type FirebaseEmulatorOptions = { firestore?: { host: string; port: number; }; database?: { host: string; port: number; }; storage?: { host: string; port: number; }; hub?: { host: string; port: number; }; }; function createUnsecuredJwt(token: TokenOptions, projectId?: string): string { // Unsecured JWTs use "none" as the algorithm. const header = { alg: 'none', kid: 'fakekid', type: 'JWT' }; const project = projectId || 'fake-project'; const iat = token.iat || 0; const uid = token.sub || token.uid || token.user_id; if (!uid) { throw new Error("Auth must contain 'sub', 'uid', or 'user_id' field!"); } const payload: FirebaseIdToken = { // Set all required fields to decent defaults iss: `https://securetoken.google.com/${project}`, aud: project, iat: iat, exp: iat + 3600, auth_time: iat, sub: uid, user_id: uid, firebase: { sign_in_provider: 'custom', identities: {} }, // Override with user options ...token }; // Remove the uid option since it's not actually part of the token spec. if (payload.uid) { delete payload.uid; } // Unsecured JWTs use the empty string as a signature. const signature = ''; return [ base64.encodeString(JSON.stringify(header), /*webSafe=*/ false), base64.encodeString(JSON.stringify(payload), /*webSafe=*/ false), signature ].join('.'); } export function apps(): firebase.app.App[] { return firebase.apps; } export type AppOptions = { databaseName?: string; projectId?: string; storageBucket?: string; auth?: TokenOptions; }; /** Construct an App authenticated with options.auth. */ export function initializeTestApp(options: AppOptions): firebase.app.App { const jwt = options.auth ? createUnsecuredJwt(options.auth, options.projectId) : undefined; return initializeApp( jwt, options.databaseName, options.projectId, options.storageBucket ); } export type AdminAppOptions = { databaseName?: string; projectId?: string; storageBucket?: string; }; /** Construct an App authenticated as an admin user. */ export function initializeAdminApp(options: AdminAppOptions): app.App { const admin = require('firebase-admin'); const app: app.App = admin.initializeApp( getAppOptions( options.databaseName, options.projectId, options.storageBucket ), getRandomAppName() ); if (options.projectId) { app.firestore().settings({ host: getFirestoreHost(), ssl: false }); } return app; } /** * Set the host and port configuration for applicable emulators. This will override any values * found in environment variables. Must be called before initializeAdminApp or initializeTestApp. * * @param options options object. */ export function useEmulators(options: FirebaseEmulatorOptions): void { if (!(options.database || options.firestore || options.storage || options.hub)) { throw new Error( "Argument to useEmulators must contain at least one of 'database', 'firestore', 'storage', or 'hub'." ); } if (options.database) { _databaseHost = getAddress(options.database.host, options.database.port); } if (options.firestore) { _firestoreHost = getAddress(options.firestore.host, options.firestore.port); } if (options.storage) { _storageHost = getAddress(options.storage.host, options.storage.port); } if (options.hub) { _hubHost = getAddress(options.hub.host, options.hub.port); } } /** * Use the Firebase Emulator hub to discover other running emulators. Call useEmulators() with * the result to configure the library to use the discovered emulators. * * @param hubHost the host where the Emulator Hub is running (ex: 'localhost') * @param hubPort the port where the Emulator Hub is running (ex: 4400) */ export async function discoverEmulators( hubHost?: string, hubPort?: number ): Promise<FirebaseEmulatorOptions> { if ((hubHost && !hubPort) || (!hubHost && hubPort)) { throw new Error( `Invalid configuration hubHost=${hubHost} and hubPort=${hubPort}. If either parameter is supplied, both must be defined.` ); } const hubAddress = hubHost && hubPort ? getAddress(hubHost, hubPort) : getHubHost(); const res = await requestPromise(request.get, { method: 'GET', uri: `http://${hubAddress}/emulators` }); if (res.statusCode !== 200) { throw new Error( `HTTP Error ${res.statusCode} when attempting to reach Emulator Hub at ${hubAddress}, are you sure it is running?` ); } const options: FirebaseEmulatorOptions = {}; const data = JSON.parse(res.body); if (data.database) { options.database = { host: data.database.host, port: data.database.port }; } if (data.firestore) { options.firestore = { host: data.firestore.host, port: data.firestore.port }; } if (data.storage) { options.storage = { host: data.storage.host, port: data.storage.port }; } if (data.hub) { options.hub = { host: data.hub.host, port: data.hub.port }; } return options; } function getAddress(host: string, port: number) { if (host.includes('::')) { return `[${host}]:${port}`; } else { return `${host}:${port}`; } } function getDatabaseHost() { if (!_databaseHost) { const fromEnv = process.env[DATABASE_ADDRESS_ENV]; if (fromEnv) { _databaseHost = fromEnv; } else { console.warn( `Warning: ${DATABASE_ADDRESS_ENV} not set, using default value ${DATABASE_ADDRESS_DEFAULT}` ); _databaseHost = DATABASE_ADDRESS_DEFAULT; } } return _databaseHost; } function getFirestoreHost() { if (!_firestoreHost) { const fromEnv = process.env[FIRESTORE_ADDRESS_ENV]; if (fromEnv) { _firestoreHost = fromEnv; } else { console.warn( `Warning: ${FIRESTORE_ADDRESS_ENV} not set, using default value ${FIRESTORE_ADDRESS_DEFAULT}` ); _firestoreHost = FIRESTORE_ADDRESS_DEFAULT; } } return _firestoreHost; } function getStorageHost() { if (!_storageHost) { const fromEnv = process.env[FIREBASE_STORAGE_ADDRESS_ENV] || process.env[CLOUD_STORAGE_ADDRESS_ENV]; if (fromEnv) { // The STORAGE_EMULATOR_HOST env var is an older Cloud Standard which includes http:// while // the FIREBASE_STORAGE_EMULATOR_HOST is a newer variable supported beginning in the Admin // SDK v9.7.0 which does not have the protocol. _storageHost = fromEnv.replace('http://', ''); } else { console.warn( `Warning: ${FIREBASE_STORAGE_ADDRESS_ENV} not set, using default value ${STORAGE_ADDRESS_DEFAULT}` ); _storageHost = STORAGE_ADDRESS_DEFAULT; } } return _storageHost; } function getHubHost() { if (!_hubHost) { const fromEnv = process.env[HUB_HOST_ENV]; if (fromEnv) { _hubHost = fromEnv; } else { console.warn( `Warning: ${HUB_HOST_ENV} not set, using default value ${HUB_HOST_DEFAULT}` ); _hubHost = HUB_HOST_DEFAULT; } } return _hubHost; } function parseHost(host: string): { hostname: string; port: number } { const withProtocol = host.startsWith("http") ? host : `http://${host}`; const u = new URL(withProtocol); return { hostname: u.hostname, port: Number.parseInt(u.port, 10) }; } function getRandomAppName(): string { return 'app-' + new Date().getTime() + '-' + Math.random(); } function getDatabaseUrl(databaseName: string) { return `http://${getDatabaseHost()}?ns=${databaseName}`; } function getAppOptions( databaseName?: string, projectId?: string, storageBucket?: string ): { [key: string]: string } { let appOptions: { [key: string]: string } = {}; if (databaseName) { appOptions['databaseURL'] = getDatabaseUrl(databaseName); } if (projectId) { appOptions['projectId'] = projectId; } if (storageBucket) { appOptions['storageBucket'] = storageBucket; } return appOptions; } function initializeApp( accessToken?: string, databaseName?: string, projectId?: string, storageBucket?: string ): firebase.app.App { const appOptions = getAppOptions(databaseName, projectId, storageBucket); const app = firebase.initializeApp(appOptions, getRandomAppName()); if (accessToken) { const mockAuthComponent = new Component( 'auth-internal', () => ({ getToken: async () => ({ accessToken: accessToken }), getUid: () => null, addAuthTokenListener: listener => { // Call listener once immediately with predefined accessToken. listener(accessToken); }, removeAuthTokenListener: () => {} } as FirebaseAuthInternal), ComponentType.PRIVATE ); ((app as unknown) as _FirebaseApp)._addOrOverwriteComponent( mockAuthComponent ); } if (databaseName) { const { hostname, port } = parseHost(getDatabaseHost()); app.database().useEmulator(hostname, port); // Toggle network connectivity to force a reauthentication attempt. // This mitigates a minor race condition where the client can send the // first database request before authenticating. app.database().goOffline(); app.database().goOnline(); } if (projectId) { const { hostname, port } = parseHost(getFirestoreHost()); app.firestore().useEmulator(hostname, port); } if (storageBucket) { const { hostname, port } = parseHost(getStorageHost()); app.storage().useEmulator(hostname, port); } /** Mute warnings for the previously-created database and whatever other objects were just created. */ setLogLevel(LogLevel.ERROR); return app; } export type LoadDatabaseRulesOptions = { databaseName: string; rules: string; }; export async function loadDatabaseRules( options: LoadDatabaseRulesOptions ): Promise<void> { if (!options.databaseName) { throw Error('databaseName not specified'); } if (!options.rules) { throw Error('must provide rules to loadDatabaseRules'); } const resp = await requestPromise(request.put, { method: 'PUT', uri: `http://${getDatabaseHost()}/.settings/rules.json?ns=${ options.databaseName }`, headers: { Authorization: 'Bearer owner' }, body: options.rules }); if (resp.statusCode !== 200) { throw new Error(JSON.parse(resp.body.error)); } } export type LoadFirestoreRulesOptions = { projectId: string; rules: string; }; export async function loadFirestoreRules( options: LoadFirestoreRulesOptions ): Promise<void> { if (!options.projectId) { throw new Error('projectId not specified'); } if (!options.rules) { throw new Error('must provide rules to loadFirestoreRules'); } const resp = await requestPromise(request.put, { method: 'PUT', uri: `http://${getFirestoreHost()}/emulator/v1/projects/${ options.projectId }:securityRules`, body: JSON.stringify({ rules: { files: [{ content: options.rules }] } }) }); if (resp.statusCode !== 200) { throw new Error(JSON.parse(resp.body.error)); } } export type LoadStorageRulesOptions = { rules: string; }; export async function loadStorageRules( options: LoadStorageRulesOptions ): Promise<void> { if (!options.rules) { throw new Error('must provide rules to loadStorageRules'); } const resp = await requestPromise(request.put, { method: 'PUT', uri: `http://${getStorageHost()}/internal/setRules`, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rules: { files: [{ name: 'storage.rules', content: options.rules }] } }) }); if (resp.statusCode !== 200) { throw new Error(resp.body); } } export type ClearFirestoreDataOptions = { projectId: string; }; export async function clearFirestoreData( options: ClearFirestoreDataOptions ): Promise<void> { if (!options.projectId) { throw new Error('projectId not specified'); } const resp = await requestPromise(request.delete, { method: 'DELETE', uri: `http://${getFirestoreHost()}/emulator/v1/projects/${ options.projectId }/databases/(default)/documents`, body: JSON.stringify({ database: `projects/${options.projectId}/databases/(default)` }) }); if (resp.statusCode !== 200) { throw new Error(JSON.parse(resp.body.error)); } } /** * 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 * emulated Cloud Functions. * * This method only works with Firebase CLI version 8.13.0 or higher. * * @param fn an function which returns a promise. */ export async function withFunctionTriggersDisabled<TResult>( fn: () => TResult | Promise<TResult> ): Promise<TResult> { const hubHost = getHubHost(); // Disable background triggers const disableRes = await requestPromise(request.put, { method: 'PUT', uri: `http://${hubHost}/functions/disableBackgroundTriggers` }); if (disableRes.statusCode !== 200) { throw new Error( `HTTP Error ${disableRes.statusCode} 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 fn(); } finally { // Re-enable background triggers const enableRes = await requestPromise(request.put, { method: 'PUT', uri: `http://${hubHost}/functions/enableBackgroundTriggers` }); if (enableRes.statusCode !== 200) { throw new Error( `HTTP Error ${enableRes.statusCode} when enabling functions triggers, are you using firebase-tools 8.13.0 or higher?` ); } } // Return the user's function result return result; } export function assertFails(pr: Promise<any>): any { return pr.then( (v: any) => { return Promise.reject( new Error('Expected request to fail, but it succeeded.') ); }, (err: any) => { const errCode = (err && err.code && err.code.toLowerCase()) || ''; const errMessage = (err && err.message && err.message.toLowerCase()) || ''; const isPermissionDenied = errCode === 'permission-denied' || errCode === 'permission_denied' || errMessage.indexOf('permission_denied') >= 0 || errMessage.indexOf('permission denied') >= 0; if (!isPermissionDenied) { return Promise.reject( new Error( `Expected PERMISSION_DENIED but got unexpected error: ${err}` ) ); } return err; } ); } export function assertSucceeds(pr: Promise<any>): any { return pr; } function requestPromise( method: typeof request.get, options: request.CoreOptions & request.UriOptions ): Promise<{ statusCode: number; body: any }> { return new Promise((resolve, reject) => { const callback: request.RequestCallback = (err, resp, body) => { if (err) { reject(err); } else { resolve({ statusCode: resp.statusCode, body }); } }; // Unfortunately request's default method is not very test-friendly so having // the caler pass in the method here makes this whole thing compatible with sinon method(options, callback); }); }