Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
cypress-firebase
Advanced tools
Cypress plugin and custom commands for testing Firebase projects
0 dependency plugin which adds custom cypress commands for interactions with Firebase:
If you are interested in what drove the need for this checkout the why section
npm i --save-dev cypress
or yarn add -D cypress
cypress
folder containing Cypress testsMake Google Cloud project name available to cypress-firebase to pass to firebase-admin on initialization by doing one of the following:
GCLOUD_PROJECT
environment variable to match the Google Project you would like to use. This needs to be on the process running cypress, so it should be before cypress open
or cypress run
in npm scripts. cross-env is a helpful way to do this to support multiple platforms and is how it is done in examples.projectId
into cypressFirebasePlugin
options when initializing (see comment in next step)Generate and download a service account as described in the firebase-admin setup documentation. Save this to a local file within the project which you confirm is within your .gitignore
- often ./serviceAccount.json
. Make sure YOU DO NOT COMMIT THIS FILE - it is sensitive and will give others admin access to your project.
Set the following config in your cypress.config.js
or cypress.config.ts
import admin from 'firebase-admin';
import { defineConfig } from 'cypress';
import { plugin as cypressFirebasePlugin } from 'cypress-firebase';
const cypressConfig = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
// NOTE: Make supportFile exists if separate location is provided
setupNodeEvents(on, config) {
// e2e testing node events setup code
return cypressFirebasePlugin(on, config, admin);
// NOTE: If not setting GCLOUD_PROJECT env variable, project can be set like so:
// return cypressFirebasePlugin(on, config, admin, { projectId: 'some-project' });
},
},
});
export default cypressConfig;
or if you are not using TS, then within cypress.config.js
:
const { defineConfig } = require('cypress');
const cypressFirebasePlugin = require('cypress-firebase').plugin;
const admin = require('firebase-admin');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
// NOTE: Make supportFile exists if separate location is provided
setupNodeEvents(on, config) {
// e2e testing node events setup code
return cypressFirebasePlugin(on, config, admin);
// NOTE: If not setting GCLOUD_PROJECT env variable, project can be set like so:
// return cypressFirebasePlugin(on, config, admin, { projectId: 'some-project' });
},
},
});
Add the following your custom commands file (cypress/support/e2e.js
or cypress/support/e2e.ts
):
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/database';
import 'firebase/firestore';
import { attachCustomCommands } from 'cypress-firebase';
const fbConfig = {
// Your config from Firebase Console
};
firebase.initializeApp(fbConfig);
attachCustomCommands({ Cypress, cy, firebase });
With Firebase Web SDK version 9
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
import 'firebase/compat/database';
import 'firebase/compat/firestore';
import { attachCustomCommands } from 'cypress-firebase';
const fbConfig = {
// Your config from Firebase Console
};
firebase.initializeApp(fbConfig);
attachCustomCommands({ Cypress, cy, firebase });
To confirm things are working, create a new test file (cypress/integration/examples/test_hello_world.cy.js
) adding a test that uses the cypress-firebase custom command (cy.callFirestore
):
describe('Some Test', () => {
it('Adds document to test_hello_world collection of Firestore', () => {
cy.callFirestore('add', 'test_hello_world', { some: 'value' });
});
});
From the root of your project, start Cypress with the command $(npm bin)/cypress open
. In the Cypress window, click your new test (test_hello_world.js
) to run it.
Look in your Firestore instance and see the test_hello_world
collection to confirm that a document was added.
Pat yourself on the back, you are all setup to access Firebase/Firestore from within your tests!
Go to Authentication page of the Firebase Console and select an existing user to use as the testing account or create a new user. This will be the account which you use to login while running tests.
Get the UID of the account you have selected, we will call this UID TEST_UID
Set the UID of the user you created earlier to the Cypress environment. You can do this using a number of methods:
Adding CYPRESS_TEST_UID
to a .env
file which is gitignored
Adding TEST_UID
to cypress.env.json
(make sure you place this within your .gitignore
)
Adding as part of your npm script to run tests with a tool such as cross-env
here:
"test": "cross-env CYPRESS_TEST_UID=your-uid cypress open"
Call cy.login()
with the before
or beforeEach
sections of your tests
npm start
) - for faster alternative checkout the test built version sectionnpm run test:open
in another terminal windowAdd the following environment variables in your CI:
CYPRESS_TEST_UID
- UID of your test userSERVICE_ACCOUNT
- service account objectWhen using a custom app name or running more than one firebase instance in your app:
const namedApp = firebase.initializeApp(fbConfig, 'app_name');
attachCustomCommands({ Cypress, cy, firebase, app: namedApp });
Login to Firebase using custom auth token.
To specify a tenant ID, either pass the ID as a parameter to cy.login
, or set it as environment variable TEST_TENANT_ID
. Read more about Firebase multi-tenancy.
Loading TEST_UID
automatically from Cypress env:
cy.login();
Passing a UID
const uid = '123SomeUid';
cy.login(uid);
Passing a tenant ID
const uid = '123SomeUid';
const tenantId = '123SomeTenantId';
cy.login(uid, undefined, tenantId);
Log out of Firebase instance
cy.logout();
Call Real Time Database path with some specified action such as set
, update
and remove
action
String The action type to call with (set, push, update, remove)actionPath
String Path within RTDB that action should be appliedoptions
object Options
options.limitToFirst
number|boolean Limit to the first <num>
results. If true is passed than query is limited to last 1 item.options.limitToLast
number|boolean Limit to the last <num>
results. If true is passed than query is limited to last 1 item.options.orderByKey
boolean Order by key nameoptions.orderByValue
boolean Order by primitive valueoptions.orderByChild
string Select a child key by which to order resultsoptions.equalTo
string Restrict results to <val>
(based on specified ordering)options.startAt
string Start results at <val>
(based on specified ordering)options.endAt
string End results at <val>
(based on specified ordering)Set data
const fakeProject = { some: 'data' };
cy.callRtdb('set', 'projects/ABC123', fakeProject);
Set Data With Meta
const fakeProject = { some: 'data' };
// Adds createdAt and createdBy (current user's uid) on data
cy.callRtdb('set', 'projects/ABC123', fakeProject, { withMeta: true });
Set Data With Timestamps
import firebase from 'firebase/app';
import 'firebase/database';
const fakeProject = {
some: 'data',
createdAt: firebase.database.ServerValue.TIMESTAMP,
};
cy.callRtdb('set', 'projects/ABC123', fakeProject);
With Firebase Web SDK version 9
import firebase from 'firebase/compat/app';
import 'firebase/compat/database';
const fakeProject = {
some: 'data',
createdAt: firebase.database.ServerValue.TIMESTAMP,
};
cy.callRtdb('set', 'projects/ABC123', fakeProject);
Delete Data
// Delete document
cy.callRtdb('delete', 'projects/ABC123');
// Delete filtered collection
cy.callRtdb('delete', 'projects', { where: ['name', '==', 'Test Project'] });
// Delete whole collection - BE CAREFUL!!
cy.callRtdb('delete', 'projectsToDelete');
Get/Verify Data
cy.callRtdb('get', 'projects/ABC123').then((project) => {
// Confirm new data has users uid
cy.wrap(project).its('createdBy').should('equal', Cypress.env('TEST_UID'));
});
Other Args
const opts = { args: ['-d'] };
const fakeProject = { some: 'data' };
cy.callRtdb('update', 'project/test-project', fakeProject, opts);
Call Firestore instance with some specified action. Authentication is through serviceAccount.json since it is at the base level.
action
String The action type to call with (set, push, update, delete)actionPath
String Path within Firestore that action should be applieddataOrOptions
String|Object Data for write actions or options for get actionoptions
Object Options
options.withMeta
boolean Whether or not to include createdAt
and createdBy
options.merge
boolean Merge data during setoptions.batchSize
number Size of batch to use while deletingoptions.where
Array Filter documents by the specified field and the value should satisfyoptions.orderBy
string|Array Order documentsoptions.limit
number Limit to n number of documentsoptions.limitToLast
number Limit to last n number of documentsoptions.statics
admin.firestore Firestore statics (i.e. admin.firestore
). This should only be needed during testing due to @firebase/testing not containing staticsBasic
cy.callFirestore('set', 'project/test-project', 'fakeProject.json');
Set Data With Server Timestamps
import firebase from 'firebase/app';
import 'firebase/firestore';
const fakeProject = {
some: 'data',
// Use new firebase.firestore.Timestamp.now in place of serverTimestamp()
createdAt: firebase.firestore.Timestamp.now(),
// Or use fromDate if you would like to specify a date
// createdAt: firebase.firestore.Timestamp.fromDate(new Date())
};
cy.callFirestore('set', 'projects/ABC123', fakeProject);
With Firebase Web SDK version 9
import firebase from 'firebase/compat/app';
import 'firebase/compat/firestore';
const fakeProject = {
some: 'data',
// Use new firebase.firestore.Timestamp.now in place of serverTimestamp()
createdAt: firebase.firestore.Timestamp.now(),
// Or use fromDate if you would like to specify a date
// createdAt: firebase.firestore.Timestamp.fromDate(new Date())
};
cy.callFirestore('set', 'projects/ABC123', fakeProject);
Full
describe('Test firestore', () => {
const TEST_UID = Cypress.env('TEST_UID');
const mockAge = 8;
beforeEach(() => {
cy.visit('/');
cy.callFirestore('delete', 'testCollection');
});
it('read/write test', () => {
cy.log('Starting test');
cy.callFirestore('set', `testCollection/${TEST_UID}`, {
name: 'axa',
age: 8,
});
cy.callFirestore('get', `testCollection/${TEST_UID}`).then((r) => {
cy.log('get returned: ', r);
cy.wrap(r).its('data.age').should('equal', mockAge);
});
cy.log('Ended test');
});
});
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. 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 on init which means any other config such as databaseURL
, credential
, or databaseAuthVariableOverride
can be included.
import admin from 'firebase-admin';
import { defineConfig } from 'cypress';
import { plugin as cypressFirebasePlugin } from 'cypress-firebase';
const cypressConfig = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
// NOTE: Make supportFile exists if separate location is provided
setupNodeEvents(on, config) {
// e2e testing node events setup code
return cypressFirebasePlugin(on, config, admin);
// NOTE: If not setting GCLOUD_PROJECT env variable, project can be set like so:
// return cypressFirebasePlugin(on, config, admin, { projectId: 'some-project' });
},
},
});
export default cypressConfig;
Install cross-env for cross system environment variable support: npm i --save-dev cross-env
Add the following to the scripts
section of your package.json
:
"emulators": "firebase emulators:start --only database,firestore",
"test": "cypress run",
"test:open": "cypress open",
"test:emulate": "cross-env FIREBASE_AUTH_EMULATOR_HOST=\"localhost:$(cat firebase.json | jq .emulators.auth.port)\" FIREBASE_DATABASE_EMULATOR_HOST=\"localhost:$(cat firebase.json | jq .emulators.database.port)\" FIRESTORE_EMULATOR_HOST=\"localhost:$(cat firebase.json | jq .emulators.firestore.port)\" yarn test:open"
If not already set by firebase init
, add emulator ports to firebase.json
:
"emulators": {
"database": {
"port": 9000
},
"firestore": {
"port": 8080
},
"auth": {
"port": 9099
}
}
Modify your application code to connect to the emulators (where your code calls firebase.initializeApp(...)
), updating the localhost ports as appropriate from the emulators
values in the previous step:
const shouldUseEmulator = window.location.hostname === 'localhost'; // or other logic to determine when to use
// Emulate RTDB
if (shouldUseEmulator) {
fbConfig.databaseURL = `http://localhost:9000?ns=${fbConfig.projectId}`;
console.debug(`Using RTDB emulator: ${fbConfig.databaseURL}`);
}
// Initialize Firebase instance
firebase.initializeApp(fbConfig);
const firestoreSettings = {};
// Pass long polling setting to Firestore when running in Cypress
if (window.Cypress) {
// Needed for Firestore support in Cypress (see https://github.com/cypress-io/cypress/issues/6350)
firestoreSettings.experimentalForceLongPolling = true;
}
// Emulate Firestore
if (shouldUseEmulator) {
firestoreSettings.host = 'localhost:8080';
firestoreSettings.ssl = false;
console.debug(`Using Firestore emulator: ${firestoreSettings.host}`);
firebase.firestore().settings(firestoreSettings);
}
// Emulate Auth
if (shouldUseEmulator) {
firebase.auth().useEmulator(`http://localhost:9099/`);
console.debug(`Using Auth emulator: http://localhost:9099/`);
}
Make sure you also have init logic in cypress/support/commands.js
or cypress/support/index.js
:
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/database';
import 'firebase/firestore';
import { attachCustomCommands } from 'cypress-firebase';
const fbConfig = {
// Your Firebase Config
};
// Emulate RTDB if Env variable is passed
const rtdbEmulatorHost = Cypress.env('FIREBASE_DATABASE_EMULATOR_HOST');
if (rtdbEmulatorHost) {
fbConfig.databaseURL = `http://${rtdbEmulatorHost}?ns=${fbConfig.projectId}`;
}
firebase.initializeApp(fbConfig);
// Emulate Firestore if Env variable is passed
const firestoreEmulatorHost = Cypress.env('FIRESTORE_EMULATOR_HOST');
if (firestoreEmulatorHost) {
firebase.firestore().settings({
host: firestoreEmulatorHost,
ssl: false,
});
}
const authEmulatorHost = Cypress.env('FIREBASE_AUTH_EMULATOR_HOST');
if (authEmulatorHost) {
firebase.auth().useEmulator(`http://${authEmulatorHost}/`);
console.debug(`Using Auth emulator: http://${authEmulatorHost}/`);
}
attachCustomCommands({ Cypress, cy, firebase });
With Firebase Web SDK version 9 in compat mode (same API as v8 with different import)
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
import 'firebase/compat/database';
import 'firebase/compat/firestore';
import { attachCustomCommands } from 'cypress-firebase';
const fbConfig = {
// Your Firebase Config
};
// Emulate RTDB if Env variable is passed
const rtdbEmulatorHost = Cypress.env('FIREBASE_DATABASE_EMULATOR_HOST');
if (rtdbEmulatorHost) {
fbConfig.databaseURL = `http://${rtdbEmulatorHost}?ns=${fbConfig.projectId}`;
}
firebase.initializeApp(fbConfig);
// Emulate Firestore if Env variable is passed
const firestoreEmulatorHost = Cypress.env('FIRESTORE_EMULATOR_HOST');
if (firestoreEmulatorHost) {
firebase.firestore().settings({
host: firestoreEmulatorHost,
ssl: false,
});
}
const authEmulatorHost = Cypress.env('FIREBASE_AUTH_EMULATOR_HOST');
if (authEmulatorHost) {
firebase.auth().useEmulator(`http://${authEmulatorHost}/`);
console.debug(`Using Auth emulator: http://${authEmulatorHost}/`);
}
attachCustomCommands({ Cypress, cy, firebase });
npm run emulators
npm start
npm run test:emulate
NOTE: If you are using react-scripts
(from create-react-app) or other environment management, you can use environment variables to pass settings into your app:
const {
REACT_APP_FIREBASE_DATABASE_EMULATOR_HOST,
REACT_APP_FIRESTORE_EMULATOR_HOST,
} = process.env;
// Emulate RTDB if REACT_APP_FIREBASE_DATABASE_EMULATOR_HOST exists in environment
if (REACT_APP_FIREBASE_DATABASE_EMULATOR_HOST) {
console.debug(`Using RTDB emulator: ${fbConfig.databaseURL}`);
fbConfig.databaseURL = `http://${REACT_APP_FIREBASE_DATABASE_EMULATOR_HOST}?ns=${fbConfig.projectId}`;
}
// Initialize Firebase instance
firebase.initializeApp(fbConfig);
const firestoreSettings = {};
if (window.Cypress) {
// Needed for Firestore support in Cypress (see https://github.com/cypress-io/cypress/issues/6350)
firestoreSettings.experimentalForceLongPolling = true;
}
// Emulate RTDB if REACT_APP_FIRESTORE_EMULATOR_HOST exists in environment
if (REACT_APP_FIRESTORE_EMULATOR_HOST) {
firestoreSettings.host = REACT_APP_FIRESTORE_EMULATOR_HOST;
firestoreSettings.ssl = false;
console.debug(`Using Firestore emulator: ${firestoreSettings.host}`);
firebase.firestore().settings(firestoreSettings);
}
Firebase instance config can be overriden by passing another argument to the cypress-firebase plugin. We can use this to override the databaseURL
:
Setup the config within plugin (cypress/plugins/index.js
):
const admin = require('firebase-admin');
const cypressFirebasePlugin = require('cypress-firebase').plugin;
module.exports = (on, config) => {
const overrideFirebaseConfig = {
databaseURL: 'http://localhost:9000?ns=my-other-namespace',
};
const extendedConfig = cypressFirebasePlugin(
on,
config,
admin,
overrideFirebaseConfig,
);
// Add other plugins/tasks such as code coverage here
return extendedConfig;
};
Make sure you use the same databaseURL
when initializing the firebase instance within cypress (cypress/support/index.js
)
Make sure you use the same databaseURL
when initializing the firebase instance within your app code
It is often required to run tests against the built version of your app instead of your dev version (with hot module reloading and other dev tools). You can do that by running a build script before spinning up the:
"start:dist": "npm run build && firebase emulators:start --only hosting",
firebase.json
:
"emulators": {
"hosting": {
"port": 3000
}
}
npm run start:dist
to build your app and serve it with firebasenpm run test:open
NOTE: You can also use firebase serve
:
"start:dist": "npm run build && firebase serve --only hosting -p 3000",
Pass commandNames
in the options
object to attachCustomCommands
:
const options = {
// Key is current command name, value is new command name
commandNames: {
login: 'newNameForLogin',
logout: 'newNameForLogout',
callRtdb: 'newNameForCallRtdb',
callFirestore: 'newNameForCallFirestore',
getAuthUser: 'newNameForGetAuthUser',
},
};
attachCustomCommands({ Cypress, cy, firebase }, options);
For more information about this feature, please see the original feature request.
If you are using a file preprocessor which is building for the browser environment, such as Webpack, you will need to make sure usage of fs
is handled since it is used within the cypress-firebase plugin. To do this with webpack, add the following to your config:
node: {
fs: 'empty';
}
See #120 for more info
Separate Install
name: Test Build
on: [pull_request]
jobs:
ui-tests:
name: UI Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v2
# Cypress action manages installing/caching npm dependencies and Cypress binary.
- name: Cypress Run
uses: cypress-io/github-action@v2
with:
group: 'E2E Tests'
env:
# pass the Dashboard record key as an environment variable
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_KEY }}
# UID of User to login as during tests
CYPRESS_TEST_UID: ${{ secrets.TEST_UID }}
# Service Account (used for creating custom auth tokens)
SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }}
# Branch settings
GITHUB_HEAD_REF: ${{ github.head_ref }}
GITHUB_REF: ${{ github.ref }}
Using Start For Local
name: Test
on: [pull_request]
jobs:
ui-tests:
name: UI Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v2
# Cypress action manages installing/caching npm dependencies and Cypress binary
- name: Cypress Run
uses: cypress-io/github-action@v2
with:
group: 'E2E Tests'
start: npm start
wait-on: http://localhost:3000
env:
# pass the Dashboard record key as an environment variable
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_KEY }}
# UID of User to login as during tests
CYPRESS_TEST_UID: ${{ secrets.TEST_UID }}
# Service Account (used for creating custom auth tokens)
SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }}
# Branch settings
GITHUB_HEAD_REF: ${{ github.head_ref }}
GITHUB_REF: ${{ github.ref }}
When testing, tests should have admin read/write access to the database for seeding/verifying data. It isn't currently possible to use Firebase's firebase-admin
SDK directly within Cypress tests due to dependencies not being able to be loaded into the Browser environment. Since the admin SDK is necessary to generate custom tokens and interact with Real Time Database and Firestore with admin privileges, this library provides convenience methods (cy.callRtdb
, cy.callFirestore
, cy.login
, etc...) which call custom tasks which have access to the node environment.
The issue is most likely due to a circular object, such as a timestamp, being included in data you are attempting to write to Firestore. Instead of using firebase.firestore.FieldValue.serverTimestamp()
you should instead use firebase.firestore.Timestamp.now()
or you would like to specify a certain date firebase.firestore.Timestamp.fromDate(new Date('01/01/18'))
.
This comes from the fact that cypress stringifies values as it is passing them from the browser environment to the node environment through cy.task
.
This has to do with the Firebase JS SDK having problems calling a Google API - this issue has mostly been seen with older versions of Firebase SDK (pre v8) when being tested on Firebase Hosting (as opposed to a local server).
The following should help prevent the issue from failing tests:
Cypress.on('uncaught:exception', (err) => {
// Prevent test failure from errors from firebase installations API
return err.message.includes('firebaseinstallations.googleapis.com');
});
If you experience this with an SDK version newer than v7 please create a new issue.
GOOGLE_APPLICATION_CREDENTIALS
)FAQs
Utilities to help testing Firebase projects with Cypress.
The npm package cypress-firebase receives a total of 18,371 weekly downloads. As such, cypress-firebase popularity was classified as popular.
We found that cypress-firebase demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.