cypress-firebase
Advanced tools
Comparing version 2.3.0-alpha.2 to 3.0.0-beta.1
@@ -1,3 +0,2 @@ | ||
import type { FieldValue } from 'firebase-admin/firestore'; | ||
import type { AppOptions } from './types'; | ||
import type { firestore } from 'firebase-admin'; | ||
/** | ||
@@ -23,3 +22,3 @@ * Params for attachCustomCommand function for | ||
} | ||
type WhereOptions = [string, FirebaseFirestore.WhereFilterOp, any]; | ||
export type WhereOptions = [string, FirebaseFirestore.WhereFilterOp, any]; | ||
/** | ||
@@ -30,7 +29,2 @@ * Options for callFirestore custom Cypress command. | ||
/** | ||
* Name of Firebase app. Defaults to Firebase's internal setting | ||
* of "[DEFAULT]". | ||
*/ | ||
appName?: string; | ||
/** | ||
* Whether or not to include createdAt and createdBy | ||
@@ -68,3 +62,3 @@ */ | ||
*/ | ||
statics?: typeof FieldValue; | ||
statics?: typeof firestore; | ||
} | ||
@@ -80,7 +74,2 @@ /** | ||
/** | ||
* Name of Firebase app. Defaults to Firebase's internal setting | ||
* of "[DEFAULT]". | ||
*/ | ||
appName?: string; | ||
/** | ||
* Whether or not to include meta data | ||
@@ -140,4 +129,2 @@ */ | ||
} | ||
export type LoginOptions = AppOptions; | ||
export type LogoutOptions = AppOptions; | ||
declare global { | ||
@@ -153,5 +140,3 @@ namespace Cypress { | ||
* @param customClaims - Custom claims to attach to the custom token | ||
* @param options - Options | ||
* @param options.appName - Optional name of firebase-admin app. Defaults to Firebase's default app (i.e DEFAULT) | ||
* @param options.tenantId - Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID | ||
* @param tenantId - Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID | ||
* @example <caption>Env Based Login (TEST_UID)</caption> | ||
@@ -162,13 +147,10 @@ * cy.login() | ||
*/ | ||
login: (uid?: string, customClaims?: any, options?: LoginOptions) => Chainable; | ||
login: (uid?: string, customClaims?: any, tenantId?: string) => Chainable; | ||
/** | ||
* Log current user out of Firebase Auth | ||
* @see https://github.com/prescottprue/cypress-firebase#cylogout | ||
* @param options - Options object | ||
* @param options.appName - Optional name of firebase-admin app. Defaults to Firebase's default app (i.e DEFAULT) | ||
* @param options.tenantId - Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID | ||
* @example | ||
* cy.logout() | ||
*/ | ||
logout: (options?: LogoutOptions) => Chainable; | ||
logout: () => Chainable; | ||
/** | ||
@@ -235,5 +217,5 @@ * Call Real Time Database path with some specified action. Authentication is through | ||
* custom command attachment | ||
* @param customCommandOptions - Custom command options | ||
* @param options - Custom command options | ||
*/ | ||
export default function attachCustomCommands(context: AttachCustomCommandParams, customCommandOptions?: CustomCommandOptions): void; | ||
export default function attachCustomCommands(context: AttachCustomCommandParams, options?: CustomCommandOptions): void; | ||
export {}; |
@@ -34,19 +34,15 @@ /** | ||
* custom command attachment | ||
* @param customCommandOptions - Custom command options | ||
* @param options - Custom command options | ||
*/ | ||
export default function attachCustomCommands(context, customCommandOptions) { | ||
export default function attachCustomCommands(context, options) { | ||
const { Cypress, cy, firebase, app } = context; | ||
const defaultApp = app || firebase.app(); // select default app | ||
/** | ||
* Get firebase auth instance, with tenantId set if provided | ||
* @param authOptions - Settings object | ||
* @param authOptions.tenantId Optional tenant ID | ||
* @param authOptions.appName - Name of app | ||
* @param tenantId Optional tenant ID | ||
* @returns firebase auth instance | ||
*/ | ||
function getAuthWithTenantId(authOptions) { | ||
const { tenantId = Cypress.env('TEST_TENANT_ID'), appName } = authOptions || {}; | ||
const browserAppInstance = app || firebase.app(appName); // select default app | ||
const auth = browserAppInstance.auth(); | ||
// Check for undefined handles null values for removing tenant from instance | ||
if (typeof tenantId !== 'undefined') { | ||
function getAuth(tenantId) { | ||
const auth = defaultApp.auth(); | ||
if (tenantId) { | ||
auth.tenantId = tenantId; | ||
@@ -62,3 +58,3 @@ } | ||
*/ | ||
Cypress.Commands.add(customCommandOptions?.commandNames?.login || 'login', (uid, customClaims, options) => { | ||
Cypress.Commands.add(options?.commandNames?.login || 'login', (uid, customClaims, tenantId = Cypress.env('TEST_TENANT_ID')) => { | ||
const userUid = uid || Cypress.env('TEST_UID'); | ||
@@ -69,6 +65,6 @@ // Handle UID which is passed in | ||
} | ||
const auth = getAuthWithTenantId(options); | ||
const auth = getAuth(tenantId); | ||
// Resolve with current user if they already exist | ||
if (auth.currentUser && userUid === auth.currentUser.uid) { | ||
cy.log('Authenticated user already exists, login complete.'); | ||
cy.log('Authed user already exists, login complete.'); | ||
return undefined; | ||
@@ -82,4 +78,3 @@ } | ||
customClaims, | ||
tenantId: auth.tenantId, | ||
...options, | ||
tenantId, | ||
}) | ||
@@ -95,4 +90,4 @@ .then((customToken) => loginWithCustomToken(auth, customToken)); | ||
*/ | ||
Cypress.Commands.add(customCommandOptions?.commandNames?.logout || 'logout', (options) => new Promise((resolve, reject) => { | ||
const auth = getAuthWithTenantId(options); | ||
Cypress.Commands.add(options?.commandNames?.logout || 'logout', (tenantId = Cypress.env('TEST_TENANT_ID')) => new Promise((resolve, reject) => { | ||
const auth = getAuth(tenantId); | ||
auth.onAuthStateChanged((auth) => { | ||
@@ -113,4 +108,3 @@ if (!auth) { | ||
*/ | ||
Cypress.Commands.add(customCommandOptions?.commandNames?.callRtdb || 'callRtdb', (action, actionPath, dataOrOptions, options) => { | ||
// TODO: Make exposed types dynamic to action (i.e. get has 3rd arg as options) | ||
Cypress.Commands.add(options?.commandNames?.callRtdb || 'callRtdb', (action, actionPath, dataOrOptions, options) => { | ||
const taskSettings = { | ||
@@ -154,3 +148,3 @@ action, | ||
*/ | ||
Cypress.Commands.add(customCommandOptions?.commandNames?.callFirestore || 'callFirestore', (action, actionPath, dataOrOptions, options) => { | ||
Cypress.Commands.add(options?.commandNames?.callFirestore || 'callFirestore', (action, actionPath, dataOrOptions, options) => { | ||
const taskSettings = { | ||
@@ -193,3 +187,3 @@ action, | ||
*/ | ||
Cypress.Commands.add(customCommandOptions?.commandNames?.getAuthUser || 'getAuthUser', (uid, options) => cy.task('getAuthUser', { uid, ...options })); | ||
Cypress.Commands.add(options?.commandNames?.getAuthUser || 'getAuthUser', (uid) => cy.task('getAuthUser', uid)); | ||
} |
@@ -1,3 +0,3 @@ | ||
import type { CollectionReference, DocumentReference, Query } from 'firebase-admin/firestore'; | ||
import { CallFirestoreOptions } from './attachCustomCommands'; | ||
import type { AppOptions, app, firestore } from 'firebase-admin'; | ||
import { CallFirestoreOptions, WhereOptions } from './attachCustomCommands'; | ||
/** | ||
@@ -10,2 +10,10 @@ * Check whether a value is a string or not | ||
/** | ||
* Initialize Firebase instance from service account (from either local | ||
* serviceAccount.json or environment variables) | ||
* @returns Initialized Firebase instance | ||
* @param adminInstance - firebase-admin instance to initialize | ||
* @param overrideConfig - firebase-admin instance to initialize | ||
*/ | ||
export declare function initializeFirebase(adminInstance: any, overrideConfig?: AppOptions): app.App; | ||
/** | ||
* Check with or not a slash path is the path of a document | ||
@@ -17,5 +25,11 @@ * @param slashPath - Path to check for whether or not it is a doc | ||
/** | ||
* | ||
* @param ref | ||
* @param whereSetting | ||
* @param firestoreStatics | ||
*/ | ||
export declare function applyWhere(ref: firestore.CollectionReference | firestore.Query, whereSetting: WhereOptions, firestoreStatics: app.App['firestore']): firestore.Query; | ||
/** | ||
* Convert slash path to Firestore reference | ||
* @param firestoreInstance - Instance on which to | ||
* create ref | ||
* @param firestoreStatics - Firestore instance statics (invoking gets instance) | ||
* @param slashPath - Path to convert into firestore reference | ||
@@ -25,3 +39,3 @@ * @param options - Options object | ||
*/ | ||
export declare function slashPathToFirestoreRef(firestoreInstance: any, slashPath: string, options?: CallFirestoreOptions): CollectionReference | DocumentReference | Query; | ||
export declare function slashPathToFirestoreRef(firestoreStatics: app.App['firestore'], slashPath: string, options?: CallFirestoreOptions): firestore.CollectionReference | firestore.DocumentReference | firestore.Query; | ||
/** | ||
@@ -28,0 +42,0 @@ * @param db - Firestore database instance |
@@ -0,1 +1,3 @@ | ||
import { getServiceAccount } from './node-utils'; | ||
import { convertValueToTimestampOrGeoPointIfPossible } from './tasks'; | ||
/** | ||
@@ -10,2 +12,122 @@ * Check whether a value is a string or not | ||
/** | ||
* Get settings for Firestore from environment. Loads port and servicePath from | ||
* FIRESTORE_EMULATOR_HOST node environment variable if found, otherwise | ||
* defaults to port 8080 and servicePath "localhost". | ||
* @returns Firestore settings to be passed to firebase.firestore().settings | ||
*/ | ||
function firestoreSettingsFromEnv() { | ||
const { FIRESTORE_EMULATOR_HOST } = process.env; | ||
if (typeof FIRESTORE_EMULATOR_HOST === 'undefined' || | ||
!isString(FIRESTORE_EMULATOR_HOST)) { | ||
return { | ||
servicePath: 'localhost', | ||
port: 8080, | ||
}; | ||
} | ||
const [servicePath, portStr] = FIRESTORE_EMULATOR_HOST.split(':'); | ||
return { | ||
servicePath, | ||
port: parseInt(portStr, 10), | ||
}; | ||
} | ||
/** | ||
* @param adminInstance - firebase-admin instance to initialize | ||
* @returns Firebase admin credential | ||
*/ | ||
function getFirebaseCredential(adminInstance) { | ||
const serviceAccount = getServiceAccount(); | ||
// Add service account credential if it exists so that custom auth tokens can be generated | ||
if (serviceAccount) { | ||
return adminInstance.credential.cert(serviceAccount); | ||
} | ||
// Add default credentials if they exist | ||
const defaultCredentials = adminInstance.credential.applicationDefault(); | ||
if (defaultCredentials) { | ||
console.log('cypress-firebase: Using default credentials'); // eslint-disable-line no-console | ||
return defaultCredentials; | ||
} | ||
} | ||
/** | ||
* Get default datbase url | ||
* @param projectId - Project id | ||
* @returns Default database url | ||
*/ | ||
function getDefaultDatabaseUrl(projectId) { | ||
const { FIREBASE_DATABASE_EMULATOR_HOST } = process.env; | ||
return FIREBASE_DATABASE_EMULATOR_HOST | ||
? `http://${FIREBASE_DATABASE_EMULATOR_HOST}?ns=${projectId || 'local'}` | ||
: `https://${projectId}.firebaseio.com`; | ||
} | ||
/** | ||
* Initialize Firebase instance from service account (from either local | ||
* serviceAccount.json or environment variables) | ||
* @returns Initialized Firebase instance | ||
* @param adminInstance - firebase-admin instance to initialize | ||
* @param overrideConfig - firebase-admin instance to initialize | ||
*/ | ||
export function initializeFirebase(adminInstance, overrideConfig) { | ||
try { | ||
// TODO: Look into using @firebase/testing in place of admin here to allow for | ||
// usage of clearFirestoreData (see https://github.com/prescottprue/cypress-firebase/issues/73 for more info) | ||
const { FIREBASE_DATABASE_EMULATOR_HOST } = process.env; | ||
const fbConfig = { | ||
// Initialize RTDB with databaseURL pointed to emulator if FIREBASE_DATABASE_EMULATOR_HOST is set | ||
...overrideConfig, | ||
}; | ||
if (FIREBASE_DATABASE_EMULATOR_HOST) { | ||
/* eslint-disable no-console */ | ||
console.log('cypress-firebase: Using RTDB emulator with host:', FIREBASE_DATABASE_EMULATOR_HOST); | ||
/* eslint-enable no-console */ | ||
} | ||
if (process.env.FIREBASE_AUTH_EMULATOR_HOST) { | ||
/* eslint-disable no-console */ | ||
console.log('cypress-firebase: Using Auth emulator with port:', process.env.FIREBASE_AUTH_EMULATOR_HOST); | ||
/* eslint-enable no-console */ | ||
} | ||
// Add credentials if they do not already exist - starting with application default, falling back to SERVICE_ACCOUNT env variable | ||
if (!fbConfig.credential) { | ||
const credential = getFirebaseCredential(adminInstance); | ||
if (credential) { | ||
fbConfig.credential = credential; | ||
} | ||
} | ||
// Add projectId to fb config if it doesn't already exist | ||
if (!fbConfig.projectId) { | ||
const projectId = process.env.GCLOUD_PROJECT || fbConfig.credential?.projectId; // eslint-disable-line camelcase | ||
if (projectId) { | ||
fbConfig.projectId = projectId; | ||
} | ||
} | ||
// Add databaseURL if it doesn't already exist | ||
if (!fbConfig.databaseURL) { | ||
const databaseURL = getDefaultDatabaseUrl(fbConfig.projectId); | ||
if (databaseURL) { | ||
fbConfig.databaseURL = databaseURL; | ||
} | ||
} | ||
const fbInstance = adminInstance.initializeApp(fbConfig); | ||
// Initialize Firestore with emulator host settings | ||
if (process.env.FIRESTORE_EMULATOR_HOST) { | ||
const firestoreSettings = firestoreSettingsFromEnv(); | ||
/* eslint-disable no-console */ | ||
console.log('cypress-firebase: Using Firestore emulator with settings:', firestoreSettings); | ||
/* eslint-enable no-console */ | ||
adminInstance.firestore().settings(firestoreSettings); | ||
} | ||
/* eslint-disable no-console */ | ||
const dbUrlLog = fbConfig.databaseURL | ||
? ` and databaseURL "${fbConfig.databaseURL}"` | ||
: ''; | ||
console.log(`cypress-firebase: Initialized Firebase app for project "${fbConfig.projectId}"${dbUrlLog}`); | ||
/* eslint-enable no-console */ | ||
return fbInstance; | ||
} | ||
catch (err) { | ||
/* eslint-disable no-console */ | ||
console.error('cypress-firebase: Error initializing firebase-admin instance:', err instanceof Error && err.message); | ||
/* eslint-enable no-console */ | ||
throw err; | ||
} | ||
} | ||
/** | ||
* Check with or not a slash path is the path of a document | ||
@@ -19,5 +141,14 @@ * @param slashPath - Path to check for whether or not it is a doc | ||
/** | ||
* | ||
* @param ref | ||
* @param whereSetting | ||
* @param firestoreStatics | ||
*/ | ||
export function applyWhere(ref, whereSetting, firestoreStatics) { | ||
const [param, filterOp, val] = whereSetting; | ||
return ref.where(param, filterOp, convertValueToTimestampOrGeoPointIfPossible(val, firestoreStatics)); | ||
} | ||
/** | ||
* Convert slash path to Firestore reference | ||
* @param firestoreInstance - Instance on which to | ||
* create ref | ||
* @param firestoreStatics - Firestore instance statics (invoking gets instance) | ||
* @param slashPath - Path to convert into firestore reference | ||
@@ -27,9 +158,11 @@ * @param options - Options object | ||
*/ | ||
export function slashPathToFirestoreRef(firestoreInstance, slashPath, options) { | ||
export function slashPathToFirestoreRef(firestoreStatics, slashPath, options) { | ||
if (!slashPath) { | ||
throw new Error('Path is required to make Firestore Reference'); | ||
} | ||
let ref = isDocPath(slashPath) | ||
? firestoreInstance.doc(slashPath) | ||
: firestoreInstance.collection(slashPath); | ||
const firestoreInstance = firestoreStatics(); | ||
if (isDocPath(slashPath)) { | ||
return firestoreInstance.doc(slashPath); | ||
} | ||
let ref = firestoreInstance.collection(slashPath); | ||
// Apply orderBy to query if it exists | ||
@@ -49,6 +182,7 @@ if (options?.orderBy && typeof ref.orderBy === 'function') { | ||
if (Array.isArray(options.where[0])) { | ||
ref = ref.where(...options.where[0]).where(...options.where[1]); | ||
const [where1, where2] = options.where; | ||
ref = applyWhere(applyWhere(ref, where1, options.statics || firestoreStatics), where2, options.statics || firestoreStatics); | ||
} | ||
else { | ||
ref = ref.where(...options.where); | ||
ref = applyWhere(ref, options.where, options.statics || firestoreStatics); | ||
} | ||
@@ -55,0 +189,0 @@ } |
@@ -0,1 +1,2 @@ | ||
import type { AppOptions } from 'firebase-admin'; | ||
import { ExtendedCypressConfig } from './extendWithFirebaseConfig'; | ||
@@ -10,4 +11,6 @@ /** | ||
* @param cypressConfig - Cypress config | ||
* @param adminInstance - firebase-admin instance | ||
* @param overrideConfig - Override config for firebase instance | ||
* @returns Extended Cypress config | ||
*/ | ||
export default function pluginWithTasks(cypressOnFunc: Cypress.PluginEvents, cypressConfig: Partial<Cypress.PluginConfigOptions>): ExtendedCypressConfig; | ||
export default function pluginWithTasks(cypressOnFunc: Cypress.PluginEvents, cypressConfig: Partial<Cypress.PluginConfigOptions>, adminInstance: any, overrideConfig?: AppOptions): ExtendedCypressConfig; |
import extendWithFirebaseConfig from './extendWithFirebaseConfig'; | ||
import * as tasks from './tasks'; | ||
import { initializeFirebase } from './firebase-utils'; | ||
/** | ||
@@ -11,10 +12,25 @@ * Cypress plugin which attaches tasks used by custom commands | ||
* @param cypressConfig - Cypress config | ||
* @param adminInstance - firebase-admin instance | ||
* @param overrideConfig - Override config for firebase instance | ||
* @returns Extended Cypress config | ||
*/ | ||
export default function pluginWithTasks(cypressOnFunc, cypressConfig) { | ||
export default function pluginWithTasks(cypressOnFunc, cypressConfig, adminInstance, overrideConfig) { | ||
// Only initialize admin instance if it hasn't already been initialized | ||
if (adminInstance.apps?.length === 0) { | ||
initializeFirebase(adminInstance, overrideConfig); | ||
} | ||
const tasksWithFirebase = Object.keys(tasks).reduce((acc, taskName) => { | ||
acc[taskName] = (taskSettings) => { | ||
if (taskSettings?.uid) { | ||
return tasks[taskName](adminInstance, taskSettings.uid, taskSettings); | ||
} | ||
const { action, path: actionPath, options = {}, data } = taskSettings; | ||
return tasks[taskName](adminInstance, action, actionPath, options, data); | ||
}; | ||
return acc; | ||
}, {}); | ||
// Attach tasks to Cypress using on function | ||
// NOTE: any is used because cypress doesn't export Task or Tasks types | ||
cypressOnFunc('task', tasks); | ||
cypressOnFunc('task', tasksWithFirebase); | ||
// Return extended config | ||
return extendWithFirebaseConfig(cypressConfig); | ||
} |
@@ -1,5 +0,13 @@ | ||
import { UserRecord } from 'firebase-admin/auth'; | ||
import type { firestore, auth, app } from 'firebase-admin'; | ||
import { FixtureData, FirestoreAction, RTDBAction, CallRtdbOptions, CallFirestoreOptions } from './attachCustomCommands'; | ||
import { AppOptions } from './types'; | ||
/** | ||
* Convert unique data types which have been stringified and parsed back | ||
* into their original type. | ||
* @param dataVal - Value of data | ||
* @param firestoreStatics - Statics from firestore instance | ||
* @returns Value converted into timestamp object if possible | ||
*/ | ||
export declare function convertValueToTimestampOrGeoPointIfPossible(dataVal: any, firestoreStatics: typeof firestore): firestore.FieldValue; | ||
/** | ||
* @param adminInstance - firebase-admin instance | ||
* @param action - Action to run | ||
@@ -11,4 +19,5 @@ * @param actionPath - Path in RTDB | ||
*/ | ||
export declare function callRtdb(action: RTDBAction, actionPath: string, options?: CallRtdbOptions, data?: FixtureData | string | boolean): Promise<any>; | ||
export declare function callRtdb(adminInstance: any, action: RTDBAction, actionPath: string, options?: CallRtdbOptions, data?: FixtureData | string | boolean): Promise<any>; | ||
/** | ||
* @param adminInstance - firebase-admin instance | ||
* @param action - Action to run | ||
@@ -20,23 +29,18 @@ * @param actionPath - Path to collection or document within Firestore | ||
*/ | ||
export declare function callFirestore(action: FirestoreAction, actionPath: string, options?: CallFirestoreOptions, data?: FixtureData): Promise<any>; | ||
export interface CustomTokenTaskSettings extends AppOptions { | ||
uid: string; | ||
customClaims?: Record<string, unknown>; | ||
} | ||
export declare function callFirestore(adminInstance: app.App, action: FirestoreAction, actionPath: string, options?: CallFirestoreOptions, data?: FixtureData): Promise<any>; | ||
/** | ||
* Create a custom token | ||
* @param adminInstance - Admin SDK instance | ||
* @param uid - UID of user for which the custom token will be generated | ||
* @param settings - Settings object | ||
* @returns Promise which resolves with a custom Firebase Auth token | ||
*/ | ||
export declare function createCustomToken(settings: CustomTokenTaskSettings): Promise<string>; | ||
export interface GetAuthUserTaskSettings extends AppOptions { | ||
uid: string; | ||
} | ||
export declare function createCustomToken(adminInstance: any, uid: string, settings?: any): Promise<string>; | ||
/** | ||
* Get Firebase Auth user based on UID | ||
* @param settings - Task settings | ||
* @param settings.uid - UID of user for which the custom token will be generated | ||
* @param settings.tenantId - Optional ID of tenant used for multi-tenancy | ||
* @param adminInstance - Admin SDK instance | ||
* @param uid - UID of user for which the custom token will be generated | ||
* @param tenantId - Optional ID of tenant used for multi-tenancy | ||
* @returns Promise which resolves with a custom Firebase Auth token | ||
*/ | ||
export declare function getAuthUser(settings: GetAuthUserTaskSettings): Promise<UserRecord>; | ||
export declare function getAuthUser(adminInstance: any, uid: string, tenantId?: string): Promise<auth.UserRecord>; |
@@ -1,5 +0,1 @@ | ||
import { getDatabase } from 'firebase-admin/database'; | ||
import { getAuth as getFirebaseAuth, } from 'firebase-admin/auth'; | ||
import { getFirestore, Timestamp, GeoPoint, FieldValue, } from 'firebase-admin/firestore'; | ||
import { getApp } from 'firebase-admin/app'; | ||
import { slashPathToFirestoreRef, deleteCollection, isDocPath, } from './firebase-utils'; | ||
@@ -40,13 +36,10 @@ /** | ||
* Get Firebase Auth or TenantAwareAuth instance, based on tenantId being provided | ||
* @param authSettings - Optional ID of tenant used for multi-tenancy | ||
* @param authSettings.tenantId - Optional ID of tenant used for multi-tenancy | ||
* @param authSettings.appName - Optional name of Firebase app. Defaults to "[DEFAULT]" | ||
* @param adminInstance - Admin SDK instance | ||
* @param tenantId - Optional ID of tenant used for multi-tenancy | ||
* @returns Firebase Auth or TenantAwareAuth instance | ||
*/ | ||
function getAdminAuthWithTenantId(authSettings) { | ||
const { tenantId, appName } = authSettings || {}; | ||
const authInstance = getFirebaseAuth(appName ? getApp(appName) : undefined); | ||
function getAuth(adminInstance, tenantId) { | ||
const auth = tenantId | ||
? authInstance.tenantManager().authForTenant(tenantId) | ||
: authInstance; | ||
? adminInstance.auth().tenantManager().authForTenant(tenantId) | ||
: adminInstance.auth(); | ||
return auth; | ||
@@ -58,5 +51,6 @@ } | ||
* @param dataVal - Value of data | ||
* @param firestoreStatics - Statics from firestore instance | ||
* @returns Value converted into timestamp object if possible | ||
*/ | ||
function convertValueToTimestampOrGeoPointIfPossible(dataVal) { | ||
export function convertValueToTimestampOrGeoPointIfPossible(dataVal, firestoreStatics) { | ||
/* eslint-disable no-underscore-dangle */ | ||
@@ -66,3 +60,3 @@ if (dataVal?._methodName === 'serverTimestamp' || | ||
) { | ||
return FieldValue.serverTimestamp(); | ||
return firestoreStatics.FieldValue.serverTimestamp(); | ||
} | ||
@@ -72,3 +66,3 @@ if (dataVal?._methodName === 'deleteField' || | ||
) { | ||
return FieldValue.delete(); | ||
return firestoreStatics.FieldValue.delete(); | ||
} | ||
@@ -78,7 +72,7 @@ /* eslint-enable no-underscore-dangle */ | ||
typeof dataVal?.nanoseconds === 'number') { | ||
return new Timestamp(dataVal.seconds, dataVal.nanoseconds); | ||
return new firestoreStatics.Timestamp(dataVal.seconds, dataVal.nanoseconds); | ||
} | ||
if (typeof dataVal?.latitude === 'number' && | ||
typeof dataVal?.longitude === 'number') { | ||
return new GeoPoint(dataVal.latitude, dataVal.longitude); | ||
return new firestoreStatics.GeoPoint(dataVal.latitude, dataVal.longitude); | ||
} | ||
@@ -89,5 +83,10 @@ return dataVal; | ||
* @param data - Data to be set in firestore | ||
* @param firestoreStatics - Statics from Firestore object | ||
* @returns Data to be set in firestore with timestamp | ||
*/ | ||
function getDataWithTimestampsAndGeoPoints(data) { | ||
function getDataWithTimestampsAndGeoPoints(data, firestoreStatics) { | ||
// Exit if no statics are passed | ||
if (!firestoreStatics) { | ||
return data; | ||
} | ||
return Object.entries(data).reduce((acc, [currKey, currData]) => { | ||
@@ -104,3 +103,3 @@ // Convert nested timestamp if item is an object | ||
...acc, | ||
[currKey]: getDataWithTimestampsAndGeoPoints(currData), | ||
[currKey]: getDataWithTimestampsAndGeoPoints(currData, firestoreStatics), | ||
}; | ||
@@ -110,8 +109,8 @@ } | ||
? currData.map((dataItem) => { | ||
const result = convertValueToTimestampOrGeoPointIfPossible(dataItem); | ||
const result = convertValueToTimestampOrGeoPointIfPossible(dataItem, firestoreStatics); | ||
return result.constructor === Object | ||
? getDataWithTimestampsAndGeoPoints(result) | ||
? getDataWithTimestampsAndGeoPoints(result, firestoreStatics) | ||
: result; | ||
}) | ||
: convertValueToTimestampOrGeoPointIfPossible(currData); | ||
: convertValueToTimestampOrGeoPointIfPossible(currData, firestoreStatics); | ||
return { | ||
@@ -124,2 +123,3 @@ ...acc, | ||
/** | ||
* @param adminInstance - firebase-admin instance | ||
* @param action - Action to run | ||
@@ -131,3 +131,3 @@ * @param actionPath - Path in RTDB | ||
*/ | ||
export async function callRtdb(action, actionPath, options, data) { | ||
export async function callRtdb(adminInstance, action, actionPath, options, data) { | ||
// Handle actionPath not being set (see #244 for more info) | ||
@@ -138,4 +138,3 @@ if (!actionPath) { | ||
try { | ||
const dbInstance = getDatabase(options?.appName ? getApp(options.appName) : undefined); | ||
const dbRef = dbInstance.ref(actionPath); | ||
const dbRef = adminInstance.database().ref(actionPath); | ||
if (action === 'get') { | ||
@@ -169,2 +168,3 @@ const snap = await optionsToRtdbRef(dbRef, options).once('value'); | ||
/** | ||
* @param adminInstance - firebase-admin instance | ||
* @param action - Action to run | ||
@@ -176,7 +176,6 @@ * @param actionPath - Path to collection or document within Firestore | ||
*/ | ||
export async function callFirestore(action, actionPath, options, data) { | ||
const firestoreInstance = getFirestore(getApp(options?.appName)); | ||
export async function callFirestore(adminInstance, action, actionPath, options, data) { | ||
try { | ||
if (action === 'get') { | ||
const snap = await slashPathToFirestoreRef(firestoreInstance, actionPath, options).get(); | ||
const snap = await slashPathToFirestoreRef(adminInstance.firestore, actionPath, options).get(); | ||
if (snap?.docs?.length && typeof snap.docs.map === 'function') { | ||
@@ -195,4 +194,4 @@ return snap.docs.map((docSnap) => ({ | ||
const deletePromise = isDocPath(actionPath) | ||
? slashPathToFirestoreRef(firestoreInstance, actionPath, options).delete() | ||
: deleteCollection(firestoreInstance, slashPathToFirestoreRef(firestoreInstance, actionPath, options), options); | ||
? slashPathToFirestoreRef(adminInstance.firestore, actionPath, options).delete() | ||
: deleteCollection(adminInstance.firestore(), slashPathToFirestoreRef(adminInstance.firestore, actionPath, options), options); | ||
await deletePromise; | ||
@@ -206,5 +205,9 @@ // Returning null in the case of falsey value prevents Cypress error with message: | ||
} | ||
const dataToSet = getDataWithTimestampsAndGeoPoints(data); | ||
const dataToSet = getDataWithTimestampsAndGeoPoints(data, | ||
// Use static option if passed (tests), otherwise fallback to statics on adminInstance | ||
// Tests do not have statics since they are using @firebase/testing | ||
options?.statics || adminInstance.firestore); | ||
if (action === 'set') { | ||
return firestoreInstance | ||
return adminInstance | ||
.firestore() | ||
.doc(actionPath) | ||
@@ -215,4 +218,4 @@ .set(dataToSet, options?.merge | ||
} | ||
// "update" action | ||
return slashPathToFirestoreRef(firestoreInstance, actionPath, options)[action](dataToSet); | ||
// "update" and "add" action | ||
return slashPathToFirestoreRef(adminInstance.firestore, actionPath, options)[action](dataToSet); | ||
} | ||
@@ -228,20 +231,22 @@ catch (err) { | ||
* Create a custom token | ||
* @param adminInstance - Admin SDK instance | ||
* @param uid - UID of user for which the custom token will be generated | ||
* @param settings - Settings object | ||
* @returns Promise which resolves with a custom Firebase Auth token | ||
*/ | ||
export function createCustomToken(settings) { | ||
export function createCustomToken(adminInstance, uid, settings) { | ||
// Use custom claims or default to { isTesting: true } | ||
const customClaims = settings?.customClaims || { isTesting: true }; | ||
// Create auth token | ||
return getAdminAuthWithTenantId(settings).createCustomToken(settings.uid, customClaims); | ||
return getAuth(adminInstance, settings.tenantId).createCustomToken(uid, customClaims); | ||
} | ||
/** | ||
* Get Firebase Auth user based on UID | ||
* @param settings - Task settings | ||
* @param settings.uid - UID of user for which the custom token will be generated | ||
* @param settings.tenantId - Optional ID of tenant used for multi-tenancy | ||
* @param adminInstance - Admin SDK instance | ||
* @param uid - UID of user for which the custom token will be generated | ||
* @param tenantId - Optional ID of tenant used for multi-tenancy | ||
* @returns Promise which resolves with a custom Firebase Auth token | ||
*/ | ||
export function getAuthUser(settings) { | ||
return getAdminAuthWithTenantId(settings).getUser(settings.uid); | ||
export function getAuthUser(adminInstance, uid, tenantId) { | ||
return getAuth(adminInstance, tenantId).getUser(uid); | ||
} |
@@ -1,3 +0,2 @@ | ||
import type { FieldValue } from 'firebase-admin/firestore'; | ||
import type { AppOptions } from './types'; | ||
import type { firestore } from 'firebase-admin'; | ||
/** | ||
@@ -23,3 +22,3 @@ * Params for attachCustomCommand function for | ||
} | ||
type WhereOptions = [string, FirebaseFirestore.WhereFilterOp, any]; | ||
export type WhereOptions = [string, FirebaseFirestore.WhereFilterOp, any]; | ||
/** | ||
@@ -30,7 +29,2 @@ * Options for callFirestore custom Cypress command. | ||
/** | ||
* Name of Firebase app. Defaults to Firebase's internal setting | ||
* of "[DEFAULT]". | ||
*/ | ||
appName?: string; | ||
/** | ||
* Whether or not to include createdAt and createdBy | ||
@@ -68,3 +62,3 @@ */ | ||
*/ | ||
statics?: typeof FieldValue; | ||
statics?: typeof firestore; | ||
} | ||
@@ -80,7 +74,2 @@ /** | ||
/** | ||
* Name of Firebase app. Defaults to Firebase's internal setting | ||
* of "[DEFAULT]". | ||
*/ | ||
appName?: string; | ||
/** | ||
* Whether or not to include meta data | ||
@@ -140,4 +129,2 @@ */ | ||
} | ||
export type LoginOptions = AppOptions; | ||
export type LogoutOptions = AppOptions; | ||
declare global { | ||
@@ -153,5 +140,3 @@ namespace Cypress { | ||
* @param customClaims - Custom claims to attach to the custom token | ||
* @param options - Options | ||
* @param options.appName - Optional name of firebase-admin app. Defaults to Firebase's default app (i.e DEFAULT) | ||
* @param options.tenantId - Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID | ||
* @param tenantId - Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID | ||
* @example <caption>Env Based Login (TEST_UID)</caption> | ||
@@ -162,13 +147,10 @@ * cy.login() | ||
*/ | ||
login: (uid?: string, customClaims?: any, options?: LoginOptions) => Chainable; | ||
login: (uid?: string, customClaims?: any, tenantId?: string) => Chainable; | ||
/** | ||
* Log current user out of Firebase Auth | ||
* @see https://github.com/prescottprue/cypress-firebase#cylogout | ||
* @param options - Options object | ||
* @param options.appName - Optional name of firebase-admin app. Defaults to Firebase's default app (i.e DEFAULT) | ||
* @param options.tenantId - Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID | ||
* @example | ||
* cy.logout() | ||
*/ | ||
logout: (options?: LogoutOptions) => Chainable; | ||
logout: () => Chainable; | ||
/** | ||
@@ -235,5 +217,5 @@ * Call Real Time Database path with some specified action. Authentication is through | ||
* custom command attachment | ||
* @param customCommandOptions - Custom command options | ||
* @param options - Custom command options | ||
*/ | ||
export default function attachCustomCommands(context: AttachCustomCommandParams, customCommandOptions?: CustomCommandOptions): void; | ||
export default function attachCustomCommands(context: AttachCustomCommandParams, options?: CustomCommandOptions): void; | ||
export {}; |
@@ -36,19 +36,15 @@ "use strict"; | ||
* custom command attachment | ||
* @param customCommandOptions - Custom command options | ||
* @param options - Custom command options | ||
*/ | ||
function attachCustomCommands(context, customCommandOptions) { | ||
function attachCustomCommands(context, options) { | ||
const { Cypress, cy, firebase, app } = context; | ||
const defaultApp = app || firebase.app(); // select default app | ||
/** | ||
* Get firebase auth instance, with tenantId set if provided | ||
* @param authOptions - Settings object | ||
* @param authOptions.tenantId Optional tenant ID | ||
* @param authOptions.appName - Name of app | ||
* @param tenantId Optional tenant ID | ||
* @returns firebase auth instance | ||
*/ | ||
function getAuthWithTenantId(authOptions) { | ||
const { tenantId = Cypress.env('TEST_TENANT_ID'), appName } = authOptions || {}; | ||
const browserAppInstance = app || firebase.app(appName); // select default app | ||
const auth = browserAppInstance.auth(); | ||
// Check for undefined handles null values for removing tenant from instance | ||
if (typeof tenantId !== 'undefined') { | ||
function getAuth(tenantId) { | ||
const auth = defaultApp.auth(); | ||
if (tenantId) { | ||
auth.tenantId = tenantId; | ||
@@ -64,3 +60,3 @@ } | ||
*/ | ||
Cypress.Commands.add(customCommandOptions?.commandNames?.login || 'login', (uid, customClaims, options) => { | ||
Cypress.Commands.add(options?.commandNames?.login || 'login', (uid, customClaims, tenantId = Cypress.env('TEST_TENANT_ID')) => { | ||
const userUid = uid || Cypress.env('TEST_UID'); | ||
@@ -71,6 +67,6 @@ // Handle UID which is passed in | ||
} | ||
const auth = getAuthWithTenantId(options); | ||
const auth = getAuth(tenantId); | ||
// Resolve with current user if they already exist | ||
if (auth.currentUser && userUid === auth.currentUser.uid) { | ||
cy.log('Authenticated user already exists, login complete.'); | ||
cy.log('Authed user already exists, login complete.'); | ||
return undefined; | ||
@@ -84,4 +80,3 @@ } | ||
customClaims, | ||
tenantId: auth.tenantId, | ||
...options, | ||
tenantId, | ||
}) | ||
@@ -97,4 +92,4 @@ .then((customToken) => loginWithCustomToken(auth, customToken)); | ||
*/ | ||
Cypress.Commands.add(customCommandOptions?.commandNames?.logout || 'logout', (options) => new Promise((resolve, reject) => { | ||
const auth = getAuthWithTenantId(options); | ||
Cypress.Commands.add(options?.commandNames?.logout || 'logout', (tenantId = Cypress.env('TEST_TENANT_ID')) => new Promise((resolve, reject) => { | ||
const auth = getAuth(tenantId); | ||
auth.onAuthStateChanged((auth) => { | ||
@@ -115,4 +110,3 @@ if (!auth) { | ||
*/ | ||
Cypress.Commands.add(customCommandOptions?.commandNames?.callRtdb || 'callRtdb', (action, actionPath, dataOrOptions, options) => { | ||
// TODO: Make exposed types dynamic to action (i.e. get has 3rd arg as options) | ||
Cypress.Commands.add(options?.commandNames?.callRtdb || 'callRtdb', (action, actionPath, dataOrOptions, options) => { | ||
const taskSettings = { | ||
@@ -156,3 +150,3 @@ action, | ||
*/ | ||
Cypress.Commands.add(customCommandOptions?.commandNames?.callFirestore || 'callFirestore', (action, actionPath, dataOrOptions, options) => { | ||
Cypress.Commands.add(options?.commandNames?.callFirestore || 'callFirestore', (action, actionPath, dataOrOptions, options) => { | ||
const taskSettings = { | ||
@@ -195,4 +189,4 @@ action, | ||
*/ | ||
Cypress.Commands.add(customCommandOptions?.commandNames?.getAuthUser || 'getAuthUser', (uid, options) => cy.task('getAuthUser', { uid, ...options })); | ||
Cypress.Commands.add(options?.commandNames?.getAuthUser || 'getAuthUser', (uid) => cy.task('getAuthUser', uid)); | ||
} | ||
exports.default = attachCustomCommands; |
@@ -1,3 +0,3 @@ | ||
import type { CollectionReference, DocumentReference, Query } from 'firebase-admin/firestore'; | ||
import { CallFirestoreOptions } from './attachCustomCommands'; | ||
import type { AppOptions, app, firestore } from 'firebase-admin'; | ||
import { CallFirestoreOptions, WhereOptions } from './attachCustomCommands'; | ||
/** | ||
@@ -10,2 +10,10 @@ * Check whether a value is a string or not | ||
/** | ||
* Initialize Firebase instance from service account (from either local | ||
* serviceAccount.json or environment variables) | ||
* @returns Initialized Firebase instance | ||
* @param adminInstance - firebase-admin instance to initialize | ||
* @param overrideConfig - firebase-admin instance to initialize | ||
*/ | ||
export declare function initializeFirebase(adminInstance: any, overrideConfig?: AppOptions): app.App; | ||
/** | ||
* Check with or not a slash path is the path of a document | ||
@@ -17,5 +25,11 @@ * @param slashPath - Path to check for whether or not it is a doc | ||
/** | ||
* | ||
* @param ref | ||
* @param whereSetting | ||
* @param firestoreStatics | ||
*/ | ||
export declare function applyWhere(ref: firestore.CollectionReference | firestore.Query, whereSetting: WhereOptions, firestoreStatics: app.App['firestore']): firestore.Query; | ||
/** | ||
* Convert slash path to Firestore reference | ||
* @param firestoreInstance - Instance on which to | ||
* create ref | ||
* @param firestoreStatics - Firestore instance statics (invoking gets instance) | ||
* @param slashPath - Path to convert into firestore reference | ||
@@ -25,3 +39,3 @@ * @param options - Options object | ||
*/ | ||
export declare function slashPathToFirestoreRef(firestoreInstance: any, slashPath: string, options?: CallFirestoreOptions): CollectionReference | DocumentReference | Query; | ||
export declare function slashPathToFirestoreRef(firestoreStatics: app.App['firestore'], slashPath: string, options?: CallFirestoreOptions): firestore.CollectionReference | firestore.DocumentReference | firestore.Query; | ||
/** | ||
@@ -28,0 +42,0 @@ * @param db - Firestore database instance |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.deleteCollection = exports.slashPathToFirestoreRef = exports.isDocPath = exports.isString = void 0; | ||
exports.deleteCollection = exports.slashPathToFirestoreRef = exports.applyWhere = exports.isDocPath = exports.initializeFirebase = exports.isString = void 0; | ||
const node_utils_1 = require("./node-utils"); | ||
const tasks_1 = require("./tasks"); | ||
/** | ||
@@ -14,2 +16,123 @@ * Check whether a value is a string or not | ||
/** | ||
* Get settings for Firestore from environment. Loads port and servicePath from | ||
* FIRESTORE_EMULATOR_HOST node environment variable if found, otherwise | ||
* defaults to port 8080 and servicePath "localhost". | ||
* @returns Firestore settings to be passed to firebase.firestore().settings | ||
*/ | ||
function firestoreSettingsFromEnv() { | ||
const { FIRESTORE_EMULATOR_HOST } = process.env; | ||
if (typeof FIRESTORE_EMULATOR_HOST === 'undefined' || | ||
!isString(FIRESTORE_EMULATOR_HOST)) { | ||
return { | ||
servicePath: 'localhost', | ||
port: 8080, | ||
}; | ||
} | ||
const [servicePath, portStr] = FIRESTORE_EMULATOR_HOST.split(':'); | ||
return { | ||
servicePath, | ||
port: parseInt(portStr, 10), | ||
}; | ||
} | ||
/** | ||
* @param adminInstance - firebase-admin instance to initialize | ||
* @returns Firebase admin credential | ||
*/ | ||
function getFirebaseCredential(adminInstance) { | ||
const serviceAccount = (0, node_utils_1.getServiceAccount)(); | ||
// Add service account credential if it exists so that custom auth tokens can be generated | ||
if (serviceAccount) { | ||
return adminInstance.credential.cert(serviceAccount); | ||
} | ||
// Add default credentials if they exist | ||
const defaultCredentials = adminInstance.credential.applicationDefault(); | ||
if (defaultCredentials) { | ||
console.log('cypress-firebase: Using default credentials'); // eslint-disable-line no-console | ||
return defaultCredentials; | ||
} | ||
} | ||
/** | ||
* Get default datbase url | ||
* @param projectId - Project id | ||
* @returns Default database url | ||
*/ | ||
function getDefaultDatabaseUrl(projectId) { | ||
const { FIREBASE_DATABASE_EMULATOR_HOST } = process.env; | ||
return FIREBASE_DATABASE_EMULATOR_HOST | ||
? `http://${FIREBASE_DATABASE_EMULATOR_HOST}?ns=${projectId || 'local'}` | ||
: `https://${projectId}.firebaseio.com`; | ||
} | ||
/** | ||
* Initialize Firebase instance from service account (from either local | ||
* serviceAccount.json or environment variables) | ||
* @returns Initialized Firebase instance | ||
* @param adminInstance - firebase-admin instance to initialize | ||
* @param overrideConfig - firebase-admin instance to initialize | ||
*/ | ||
function initializeFirebase(adminInstance, overrideConfig) { | ||
try { | ||
// TODO: Look into using @firebase/testing in place of admin here to allow for | ||
// usage of clearFirestoreData (see https://github.com/prescottprue/cypress-firebase/issues/73 for more info) | ||
const { FIREBASE_DATABASE_EMULATOR_HOST } = process.env; | ||
const fbConfig = { | ||
// Initialize RTDB with databaseURL pointed to emulator if FIREBASE_DATABASE_EMULATOR_HOST is set | ||
...overrideConfig, | ||
}; | ||
if (FIREBASE_DATABASE_EMULATOR_HOST) { | ||
/* eslint-disable no-console */ | ||
console.log('cypress-firebase: Using RTDB emulator with host:', FIREBASE_DATABASE_EMULATOR_HOST); | ||
/* eslint-enable no-console */ | ||
} | ||
if (process.env.FIREBASE_AUTH_EMULATOR_HOST) { | ||
/* eslint-disable no-console */ | ||
console.log('cypress-firebase: Using Auth emulator with port:', process.env.FIREBASE_AUTH_EMULATOR_HOST); | ||
/* eslint-enable no-console */ | ||
} | ||
// Add credentials if they do not already exist - starting with application default, falling back to SERVICE_ACCOUNT env variable | ||
if (!fbConfig.credential) { | ||
const credential = getFirebaseCredential(adminInstance); | ||
if (credential) { | ||
fbConfig.credential = credential; | ||
} | ||
} | ||
// Add projectId to fb config if it doesn't already exist | ||
if (!fbConfig.projectId) { | ||
const projectId = process.env.GCLOUD_PROJECT || fbConfig.credential?.projectId; // eslint-disable-line camelcase | ||
if (projectId) { | ||
fbConfig.projectId = projectId; | ||
} | ||
} | ||
// Add databaseURL if it doesn't already exist | ||
if (!fbConfig.databaseURL) { | ||
const databaseURL = getDefaultDatabaseUrl(fbConfig.projectId); | ||
if (databaseURL) { | ||
fbConfig.databaseURL = databaseURL; | ||
} | ||
} | ||
const fbInstance = adminInstance.initializeApp(fbConfig); | ||
// Initialize Firestore with emulator host settings | ||
if (process.env.FIRESTORE_EMULATOR_HOST) { | ||
const firestoreSettings = firestoreSettingsFromEnv(); | ||
/* eslint-disable no-console */ | ||
console.log('cypress-firebase: Using Firestore emulator with settings:', firestoreSettings); | ||
/* eslint-enable no-console */ | ||
adminInstance.firestore().settings(firestoreSettings); | ||
} | ||
/* eslint-disable no-console */ | ||
const dbUrlLog = fbConfig.databaseURL | ||
? ` and databaseURL "${fbConfig.databaseURL}"` | ||
: ''; | ||
console.log(`cypress-firebase: Initialized Firebase app for project "${fbConfig.projectId}"${dbUrlLog}`); | ||
/* eslint-enable no-console */ | ||
return fbInstance; | ||
} | ||
catch (err) { | ||
/* eslint-disable no-console */ | ||
console.error('cypress-firebase: Error initializing firebase-admin instance:', err instanceof Error && err.message); | ||
/* eslint-enable no-console */ | ||
throw err; | ||
} | ||
} | ||
exports.initializeFirebase = initializeFirebase; | ||
/** | ||
* Check with or not a slash path is the path of a document | ||
@@ -24,5 +147,15 @@ * @param slashPath - Path to check for whether or not it is a doc | ||
/** | ||
* | ||
* @param ref | ||
* @param whereSetting | ||
* @param firestoreStatics | ||
*/ | ||
function applyWhere(ref, whereSetting, firestoreStatics) { | ||
const [param, filterOp, val] = whereSetting; | ||
return ref.where(param, filterOp, (0, tasks_1.convertValueToTimestampOrGeoPointIfPossible)(val, firestoreStatics)); | ||
} | ||
exports.applyWhere = applyWhere; | ||
/** | ||
* Convert slash path to Firestore reference | ||
* @param firestoreInstance - Instance on which to | ||
* create ref | ||
* @param firestoreStatics - Firestore instance statics (invoking gets instance) | ||
* @param slashPath - Path to convert into firestore reference | ||
@@ -32,9 +165,11 @@ * @param options - Options object | ||
*/ | ||
function slashPathToFirestoreRef(firestoreInstance, slashPath, options) { | ||
function slashPathToFirestoreRef(firestoreStatics, slashPath, options) { | ||
if (!slashPath) { | ||
throw new Error('Path is required to make Firestore Reference'); | ||
} | ||
let ref = isDocPath(slashPath) | ||
? firestoreInstance.doc(slashPath) | ||
: firestoreInstance.collection(slashPath); | ||
const firestoreInstance = firestoreStatics(); | ||
if (isDocPath(slashPath)) { | ||
return firestoreInstance.doc(slashPath); | ||
} | ||
let ref = firestoreInstance.collection(slashPath); | ||
// Apply orderBy to query if it exists | ||
@@ -54,6 +189,7 @@ if (options?.orderBy && typeof ref.orderBy === 'function') { | ||
if (Array.isArray(options.where[0])) { | ||
ref = ref.where(...options.where[0]).where(...options.where[1]); | ||
const [where1, where2] = options.where; | ||
ref = applyWhere(applyWhere(ref, where1, options.statics || firestoreStatics), where2, options.statics || firestoreStatics); | ||
} | ||
else { | ||
ref = ref.where(...options.where); | ||
ref = applyWhere(ref, options.where, options.statics || firestoreStatics); | ||
} | ||
@@ -60,0 +196,0 @@ } |
@@ -0,1 +1,2 @@ | ||
import type { AppOptions } from 'firebase-admin'; | ||
import { ExtendedCypressConfig } from './extendWithFirebaseConfig'; | ||
@@ -10,4 +11,6 @@ /** | ||
* @param cypressConfig - Cypress config | ||
* @param adminInstance - firebase-admin instance | ||
* @param overrideConfig - Override config for firebase instance | ||
* @returns Extended Cypress config | ||
*/ | ||
export default function pluginWithTasks(cypressOnFunc: Cypress.PluginEvents, cypressConfig: Partial<Cypress.PluginConfigOptions>): ExtendedCypressConfig; | ||
export default function pluginWithTasks(cypressOnFunc: Cypress.PluginEvents, cypressConfig: Partial<Cypress.PluginConfigOptions>, adminInstance: any, overrideConfig?: AppOptions): ExtendedCypressConfig; |
@@ -6,2 +6,3 @@ "use strict"; | ||
const tasks = tslib_1.__importStar(require("./tasks")); | ||
const firebase_utils_1 = require("./firebase-utils"); | ||
/** | ||
@@ -15,8 +16,23 @@ * Cypress plugin which attaches tasks used by custom commands | ||
* @param cypressConfig - Cypress config | ||
* @param adminInstance - firebase-admin instance | ||
* @param overrideConfig - Override config for firebase instance | ||
* @returns Extended Cypress config | ||
*/ | ||
function pluginWithTasks(cypressOnFunc, cypressConfig) { | ||
function pluginWithTasks(cypressOnFunc, cypressConfig, adminInstance, overrideConfig) { | ||
// Only initialize admin instance if it hasn't already been initialized | ||
if (adminInstance.apps?.length === 0) { | ||
(0, firebase_utils_1.initializeFirebase)(adminInstance, overrideConfig); | ||
} | ||
const tasksWithFirebase = Object.keys(tasks).reduce((acc, taskName) => { | ||
acc[taskName] = (taskSettings) => { | ||
if (taskSettings?.uid) { | ||
return tasks[taskName](adminInstance, taskSettings.uid, taskSettings); | ||
} | ||
const { action, path: actionPath, options = {}, data } = taskSettings; | ||
return tasks[taskName](adminInstance, action, actionPath, options, data); | ||
}; | ||
return acc; | ||
}, {}); | ||
// Attach tasks to Cypress using on function | ||
// NOTE: any is used because cypress doesn't export Task or Tasks types | ||
cypressOnFunc('task', tasks); | ||
cypressOnFunc('task', tasksWithFirebase); | ||
// Return extended config | ||
@@ -23,0 +39,0 @@ return (0, extendWithFirebaseConfig_1.default)(cypressConfig); |
@@ -1,5 +0,13 @@ | ||
import { UserRecord } from 'firebase-admin/auth'; | ||
import type { firestore, auth, app } from 'firebase-admin'; | ||
import { FixtureData, FirestoreAction, RTDBAction, CallRtdbOptions, CallFirestoreOptions } from './attachCustomCommands'; | ||
import { AppOptions } from './types'; | ||
/** | ||
* Convert unique data types which have been stringified and parsed back | ||
* into their original type. | ||
* @param dataVal - Value of data | ||
* @param firestoreStatics - Statics from firestore instance | ||
* @returns Value converted into timestamp object if possible | ||
*/ | ||
export declare function convertValueToTimestampOrGeoPointIfPossible(dataVal: any, firestoreStatics: typeof firestore): firestore.FieldValue; | ||
/** | ||
* @param adminInstance - firebase-admin instance | ||
* @param action - Action to run | ||
@@ -11,4 +19,5 @@ * @param actionPath - Path in RTDB | ||
*/ | ||
export declare function callRtdb(action: RTDBAction, actionPath: string, options?: CallRtdbOptions, data?: FixtureData | string | boolean): Promise<any>; | ||
export declare function callRtdb(adminInstance: any, action: RTDBAction, actionPath: string, options?: CallRtdbOptions, data?: FixtureData | string | boolean): Promise<any>; | ||
/** | ||
* @param adminInstance - firebase-admin instance | ||
* @param action - Action to run | ||
@@ -20,23 +29,18 @@ * @param actionPath - Path to collection or document within Firestore | ||
*/ | ||
export declare function callFirestore(action: FirestoreAction, actionPath: string, options?: CallFirestoreOptions, data?: FixtureData): Promise<any>; | ||
export interface CustomTokenTaskSettings extends AppOptions { | ||
uid: string; | ||
customClaims?: Record<string, unknown>; | ||
} | ||
export declare function callFirestore(adminInstance: app.App, action: FirestoreAction, actionPath: string, options?: CallFirestoreOptions, data?: FixtureData): Promise<any>; | ||
/** | ||
* Create a custom token | ||
* @param adminInstance - Admin SDK instance | ||
* @param uid - UID of user for which the custom token will be generated | ||
* @param settings - Settings object | ||
* @returns Promise which resolves with a custom Firebase Auth token | ||
*/ | ||
export declare function createCustomToken(settings: CustomTokenTaskSettings): Promise<string>; | ||
export interface GetAuthUserTaskSettings extends AppOptions { | ||
uid: string; | ||
} | ||
export declare function createCustomToken(adminInstance: any, uid: string, settings?: any): Promise<string>; | ||
/** | ||
* Get Firebase Auth user based on UID | ||
* @param settings - Task settings | ||
* @param settings.uid - UID of user for which the custom token will be generated | ||
* @param settings.tenantId - Optional ID of tenant used for multi-tenancy | ||
* @param adminInstance - Admin SDK instance | ||
* @param uid - UID of user for which the custom token will be generated | ||
* @param tenantId - Optional ID of tenant used for multi-tenancy | ||
* @returns Promise which resolves with a custom Firebase Auth token | ||
*/ | ||
export declare function getAuthUser(settings: GetAuthUserTaskSettings): Promise<UserRecord>; | ||
export declare function getAuthUser(adminInstance: any, uid: string, tenantId?: string): Promise<auth.UserRecord>; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getAuthUser = exports.createCustomToken = exports.callFirestore = exports.callRtdb = void 0; | ||
const database_1 = require("firebase-admin/database"); | ||
const auth_1 = require("firebase-admin/auth"); | ||
const firestore_1 = require("firebase-admin/firestore"); | ||
const app_1 = require("firebase-admin/app"); | ||
exports.getAuthUser = exports.createCustomToken = exports.callFirestore = exports.callRtdb = exports.convertValueToTimestampOrGeoPointIfPossible = void 0; | ||
const firebase_utils_1 = require("./firebase-utils"); | ||
@@ -43,13 +39,10 @@ /** | ||
* Get Firebase Auth or TenantAwareAuth instance, based on tenantId being provided | ||
* @param authSettings - Optional ID of tenant used for multi-tenancy | ||
* @param authSettings.tenantId - Optional ID of tenant used for multi-tenancy | ||
* @param authSettings.appName - Optional name of Firebase app. Defaults to "[DEFAULT]" | ||
* @param adminInstance - Admin SDK instance | ||
* @param tenantId - Optional ID of tenant used for multi-tenancy | ||
* @returns Firebase Auth or TenantAwareAuth instance | ||
*/ | ||
function getAdminAuthWithTenantId(authSettings) { | ||
const { tenantId, appName } = authSettings || {}; | ||
const authInstance = (0, auth_1.getAuth)(appName ? (0, app_1.getApp)(appName) : undefined); | ||
function getAuth(adminInstance, tenantId) { | ||
const auth = tenantId | ||
? authInstance.tenantManager().authForTenant(tenantId) | ||
: authInstance; | ||
? adminInstance.auth().tenantManager().authForTenant(tenantId) | ||
: adminInstance.auth(); | ||
return auth; | ||
@@ -61,5 +54,6 @@ } | ||
* @param dataVal - Value of data | ||
* @param firestoreStatics - Statics from firestore instance | ||
* @returns Value converted into timestamp object if possible | ||
*/ | ||
function convertValueToTimestampOrGeoPointIfPossible(dataVal) { | ||
function convertValueToTimestampOrGeoPointIfPossible(dataVal, firestoreStatics) { | ||
/* eslint-disable no-underscore-dangle */ | ||
@@ -69,3 +63,3 @@ if (dataVal?._methodName === 'serverTimestamp' || | ||
) { | ||
return firestore_1.FieldValue.serverTimestamp(); | ||
return firestoreStatics.FieldValue.serverTimestamp(); | ||
} | ||
@@ -75,3 +69,3 @@ if (dataVal?._methodName === 'deleteField' || | ||
) { | ||
return firestore_1.FieldValue.delete(); | ||
return firestoreStatics.FieldValue.delete(); | ||
} | ||
@@ -81,15 +75,21 @@ /* eslint-enable no-underscore-dangle */ | ||
typeof dataVal?.nanoseconds === 'number') { | ||
return new firestore_1.Timestamp(dataVal.seconds, dataVal.nanoseconds); | ||
return new firestoreStatics.Timestamp(dataVal.seconds, dataVal.nanoseconds); | ||
} | ||
if (typeof dataVal?.latitude === 'number' && | ||
typeof dataVal?.longitude === 'number') { | ||
return new firestore_1.GeoPoint(dataVal.latitude, dataVal.longitude); | ||
return new firestoreStatics.GeoPoint(dataVal.latitude, dataVal.longitude); | ||
} | ||
return dataVal; | ||
} | ||
exports.convertValueToTimestampOrGeoPointIfPossible = convertValueToTimestampOrGeoPointIfPossible; | ||
/** | ||
* @param data - Data to be set in firestore | ||
* @param firestoreStatics - Statics from Firestore object | ||
* @returns Data to be set in firestore with timestamp | ||
*/ | ||
function getDataWithTimestampsAndGeoPoints(data) { | ||
function getDataWithTimestampsAndGeoPoints(data, firestoreStatics) { | ||
// Exit if no statics are passed | ||
if (!firestoreStatics) { | ||
return data; | ||
} | ||
return Object.entries(data).reduce((acc, [currKey, currData]) => { | ||
@@ -106,3 +106,3 @@ // Convert nested timestamp if item is an object | ||
...acc, | ||
[currKey]: getDataWithTimestampsAndGeoPoints(currData), | ||
[currKey]: getDataWithTimestampsAndGeoPoints(currData, firestoreStatics), | ||
}; | ||
@@ -112,8 +112,8 @@ } | ||
? currData.map((dataItem) => { | ||
const result = convertValueToTimestampOrGeoPointIfPossible(dataItem); | ||
const result = convertValueToTimestampOrGeoPointIfPossible(dataItem, firestoreStatics); | ||
return result.constructor === Object | ||
? getDataWithTimestampsAndGeoPoints(result) | ||
? getDataWithTimestampsAndGeoPoints(result, firestoreStatics) | ||
: result; | ||
}) | ||
: convertValueToTimestampOrGeoPointIfPossible(currData); | ||
: convertValueToTimestampOrGeoPointIfPossible(currData, firestoreStatics); | ||
return { | ||
@@ -126,2 +126,3 @@ ...acc, | ||
/** | ||
* @param adminInstance - firebase-admin instance | ||
* @param action - Action to run | ||
@@ -133,3 +134,3 @@ * @param actionPath - Path in RTDB | ||
*/ | ||
async function callRtdb(action, actionPath, options, data) { | ||
async function callRtdb(adminInstance, action, actionPath, options, data) { | ||
// Handle actionPath not being set (see #244 for more info) | ||
@@ -140,4 +141,3 @@ if (!actionPath) { | ||
try { | ||
const dbInstance = (0, database_1.getDatabase)(options?.appName ? (0, app_1.getApp)(options.appName) : undefined); | ||
const dbRef = dbInstance.ref(actionPath); | ||
const dbRef = adminInstance.database().ref(actionPath); | ||
if (action === 'get') { | ||
@@ -172,2 +172,3 @@ const snap = await optionsToRtdbRef(dbRef, options).once('value'); | ||
/** | ||
* @param adminInstance - firebase-admin instance | ||
* @param action - Action to run | ||
@@ -179,7 +180,6 @@ * @param actionPath - Path to collection or document within Firestore | ||
*/ | ||
async function callFirestore(action, actionPath, options, data) { | ||
const firestoreInstance = (0, firestore_1.getFirestore)((0, app_1.getApp)(options?.appName)); | ||
async function callFirestore(adminInstance, action, actionPath, options, data) { | ||
try { | ||
if (action === 'get') { | ||
const snap = await (0, firebase_utils_1.slashPathToFirestoreRef)(firestoreInstance, actionPath, options).get(); | ||
const snap = await (0, firebase_utils_1.slashPathToFirestoreRef)(adminInstance.firestore, actionPath, options).get(); | ||
if (snap?.docs?.length && typeof snap.docs.map === 'function') { | ||
@@ -198,4 +198,4 @@ return snap.docs.map((docSnap) => ({ | ||
const deletePromise = (0, firebase_utils_1.isDocPath)(actionPath) | ||
? (0, firebase_utils_1.slashPathToFirestoreRef)(firestoreInstance, actionPath, options).delete() | ||
: (0, firebase_utils_1.deleteCollection)(firestoreInstance, (0, firebase_utils_1.slashPathToFirestoreRef)(firestoreInstance, actionPath, options), options); | ||
? (0, firebase_utils_1.slashPathToFirestoreRef)(adminInstance.firestore, actionPath, options).delete() | ||
: (0, firebase_utils_1.deleteCollection)(adminInstance.firestore(), (0, firebase_utils_1.slashPathToFirestoreRef)(adminInstance.firestore, actionPath, options), options); | ||
await deletePromise; | ||
@@ -209,5 +209,9 @@ // Returning null in the case of falsey value prevents Cypress error with message: | ||
} | ||
const dataToSet = getDataWithTimestampsAndGeoPoints(data); | ||
const dataToSet = getDataWithTimestampsAndGeoPoints(data, | ||
// Use static option if passed (tests), otherwise fallback to statics on adminInstance | ||
// Tests do not have statics since they are using @firebase/testing | ||
options?.statics || adminInstance.firestore); | ||
if (action === 'set') { | ||
return firestoreInstance | ||
return adminInstance | ||
.firestore() | ||
.doc(actionPath) | ||
@@ -218,4 +222,4 @@ .set(dataToSet, options?.merge | ||
} | ||
// "update" action | ||
return (0, firebase_utils_1.slashPathToFirestoreRef)(firestoreInstance, actionPath, options)[action](dataToSet); | ||
// "update" and "add" action | ||
return (0, firebase_utils_1.slashPathToFirestoreRef)(adminInstance.firestore, actionPath, options)[action](dataToSet); | ||
} | ||
@@ -232,10 +236,12 @@ catch (err) { | ||
* Create a custom token | ||
* @param adminInstance - Admin SDK instance | ||
* @param uid - UID of user for which the custom token will be generated | ||
* @param settings - Settings object | ||
* @returns Promise which resolves with a custom Firebase Auth token | ||
*/ | ||
function createCustomToken(settings) { | ||
function createCustomToken(adminInstance, uid, settings) { | ||
// Use custom claims or default to { isTesting: true } | ||
const customClaims = settings?.customClaims || { isTesting: true }; | ||
// Create auth token | ||
return getAdminAuthWithTenantId(settings).createCustomToken(settings.uid, customClaims); | ||
return getAuth(adminInstance, settings.tenantId).createCustomToken(uid, customClaims); | ||
} | ||
@@ -245,10 +251,10 @@ exports.createCustomToken = createCustomToken; | ||
* Get Firebase Auth user based on UID | ||
* @param settings - Task settings | ||
* @param settings.uid - UID of user for which the custom token will be generated | ||
* @param settings.tenantId - Optional ID of tenant used for multi-tenancy | ||
* @param adminInstance - Admin SDK instance | ||
* @param uid - UID of user for which the custom token will be generated | ||
* @param tenantId - Optional ID of tenant used for multi-tenancy | ||
* @returns Promise which resolves with a custom Firebase Auth token | ||
*/ | ||
function getAuthUser(settings) { | ||
return getAdminAuthWithTenantId(settings).getUser(settings.uid); | ||
function getAuthUser(adminInstance, uid, tenantId) { | ||
return getAuth(adminInstance, tenantId).getUser(uid); | ||
} | ||
exports.getAuthUser = getAuthUser; |
{ | ||
"name": "cypress-firebase", | ||
"version": "2.3.0-alpha.2", | ||
"version": "3.0.0-beta.1", | ||
"description": "Utilities to help testing Firebase projects with Cypress.", | ||
@@ -31,18 +31,18 @@ "main": "lib/index.js", | ||
"devDependencies": { | ||
"@commitlint/cli": "17.1.2", | ||
"@commitlint/config-conventional": "17.0.3", | ||
"@firebase/rules-unit-testing": "2.0.4", | ||
"@commitlint/cli": "17.3.0", | ||
"@commitlint/config-conventional": "17.3.0", | ||
"@firebase/rules-unit-testing": "2.0.5", | ||
"@istanbuljs/nyc-config-typescript": "1.0.2", | ||
"@size-limit/preset-small-lib": "8.0.1", | ||
"@size-limit/preset-small-lib": "8.1.0", | ||
"@size-limit/webpack": "8.1.0", | ||
"@tsconfig/node16": "1.0.3", | ||
"@types/chai": "4.3.4", | ||
"@types/mocha": "9.1.1", | ||
"@types/node": "16.18.10", | ||
"@types/mocha": "10.0.1", | ||
"@types/node": "16.18.11", | ||
"@types/sinon-chai": "3.2.9", | ||
"@typescript-eslint/eslint-plugin": "5.46.1", | ||
"@typescript-eslint/parser": "5.46.1", | ||
"@typescript-eslint/eslint-plugin": "5.47.1", | ||
"@typescript-eslint/parser": "5.47.1", | ||
"chai": "4.3.7", | ||
"cypress": "12.2.0", | ||
"eslint": "8.29.0", | ||
"eslint": "8.31.0", | ||
"eslint-config-airbnb-base": "15.0.0", | ||
@@ -55,14 +55,14 @@ "eslint-config-prettier": "8.5.0", | ||
"eslint-plugin-prettier": "4.2.1", | ||
"firebase": "9.9.4", | ||
"firebase": "9.15.0", | ||
"firebase-admin": "11.4.1", | ||
"firebase-tools": "11.8.0", | ||
"husky": "8.0.1", | ||
"lint-staged": "13.0.3", | ||
"mocha": "10.0.0", | ||
"firebase-tools": "11.19.0", | ||
"husky": "8.0.2", | ||
"lint-staged": "13.1.0", | ||
"mocha": "10.2.0", | ||
"nyc": "15.1.0", | ||
"prettier": "2.8.1", | ||
"rimraf": "3.0.2", | ||
"sinon": "14.0.0", | ||
"sinon": "15.0.1", | ||
"sinon-chai": "3.7.0", | ||
"size-limit": "8.0.1", | ||
"size-limit": "8.1.0", | ||
"ts-node": "10.9.1", | ||
@@ -136,3 +136,3 @@ "typescript": "4.9.4" | ||
"import": "{ plugin }", | ||
"limit": "2.75kb", | ||
"limit": "3kb", | ||
"webpack": false | ||
@@ -139,0 +139,0 @@ } |
@@ -28,5 +28,4 @@ # cypress-firebase | ||
1. If you do not already have it installed, install Cypress and firebase-admin and add them to your package file: `npm i --save-dev cypress firebase-admin` or `yarn add -D cypress firebase-admin` | ||
1. If you do not already have it installed, install Cypress and add it to your package file: `npm i --save-dev cypress` or `yarn add -D cypress` | ||
1. Make sure you have a `cypress` folder containing Cypress tests | ||
1. `cypress-firebase` v3 uses firebase-admin v11 - if you want to use an earlier version of firebase-admin, use `^2` versions of `cypress-firebase` | ||
@@ -42,9 +41,6 @@ ### Setup | ||
```js | ||
import { initializeApp } from 'firebase-admin/app'; | ||
import admin from 'firebase-admin'; | ||
import { defineConfig } from 'cypress'; | ||
import { plugin as cypressFirebasePlugin } from 'cypress-firebase'; | ||
// Initialize firebase-admin default app | ||
initializeApp(); | ||
const cypressConfig = defineConfig({ | ||
@@ -71,7 +67,4 @@ e2e: { | ||
const cypressFirebasePlugin = require('cypress-firebase').plugin; | ||
const { initializeApp } = require('firebase-admin/app'); | ||
const admin = require('firebase-admin'); | ||
// Initialize firebase-admin default app | ||
initializeApp(); | ||
module.exports = defineConfig({ | ||
@@ -415,3 +408,3 @@ e2e: { | ||
Plugin attaches cypress tasks, which are called by custom commands, and initializes firebase-admin instance. By default cypress-firebase internally initializes firebase-admin using `GCLOUD_PROJECT` environment variable for project identification and application-default credentials (set by providing path to service account in `GOOGLE_APPLICATION_CREDENTIALS` environment variable) [matching Google documentation](https://firebase.google.com/docs/admin/setup#initialize-sdk). | ||
Plugin attaches cypress tasks, which are called by custom commands, and initializes firebase-admin instance. By default cypress-firebase internally initializes firebase-admin using `GCLOUD_PROJECT` environment variable for project identification and application-default credentials (set by providing path to service account in `GOOGLE_APPLICATION_CREDENTIALS` environment variable) [matching Google documentation](https://firebase.google.com/docs/admin/setup#initialize-sdk). This default functionality can be overriden by passing a forth argument to the plugin - this argument is passed directly into the firebase-admin instance as [AppOptions](https://firebase.google.com/docs/reference/admin/dotnet/class/firebase-admin/app-options#constructors-and-destructors) on init which means any other config such as `databaseURL`, `credential`, or `databaseAuthVariableOverride` can be included. | ||
@@ -429,3 +422,3 @@ ```js | ||
// e2e testing node events setup code | ||
return cypressFirebasePlugin(on, config); | ||
return cypressFirebasePlugin(on, config, admin); | ||
// NOTE: If not setting GCLOUD_PROJECT env variable, project can be set like so: | ||
@@ -432,0 +425,0 @@ // return cypressFirebasePlugin(on, config, admin, { projectId: 'some-project' }); |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
136478
2462
835
42