Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
@getcircuit/firestore-test
Advanced tools
<!-- @import "[TOC]" {cmd="toc" depthFrom=2 depthTo=4 orderedList=false} -->
@getcircuit/firestore-test
This package is comprised of a set of utilities to help you write tests for your Firestore code. It mainly provides two significant features:
jest
test environment.The CLI is a simple wrapper around the firebase
CLI, but with the project
set to test
and pointing to this package's firebase.json
configuration file.
# Start the emulator indefinitely
firestore-test emulator:start
# Start the emulator and run a command
firestore-test emulator:exec "<command>"
Example:
# Start the emulator and run jest
firestore-test emulator:exec "jest"
To start, create a Jest setup file and reference it in the setupFilesAfterEnv
property within your jest.config.js
. The first line in this setup file should import one of the following:
@getcircuit/firestore-test/setup/client
if you're running tests in a client environment.@getcircuit/firestore-test/setup/admin
if you're running tests in a admin environment.// jest.config.js
export default {
setupFilesAfterEnv: ['@getcircuit/firestore-test/setup/client'],
}
The test firebase app is automatically initialized once getTestFirestore
, getTestAuth
or getTestApp
is called.
⚠️ Note: If you opt for the Node SDK, make sure to follow the instructions in the
firebase-types
package to override the default Firebase types. This is essential as the default types are set for the browser SDK.
For more information about how we support both firebase-admin
and firebase
in the same project, check out this document.
@getcircuit/firestore-test
provides pre-configured Jest setup files for both browser and Node.js environments. You can extend these in your project's Jest configuration:
@getcircuit/firestore-test/jest/config.browser
for a browser environment@getcircuit/firestore-test/jest/config.node
for a Node.js environmentWhen integrating these configurations, make sure to merge the globalSetup
and setupFilesAfterEnv
fields to avoid overwriting any existing configurations in your project's Jest setup.
// client-project/jest.config.js
const sharedConfig = require('@getcircuit/firestore-test/jest/config.browser')
module.exports = {
...sharedConfig,
globalSetup: [
...(sharedConfig?.globalSetup ?? []),
// your globalSetup here
],
setupFilesAfterEnv: [
...(sharedConfig?.setupFilesAfterEnv ?? []),
// your setupFilesAfterEnv here
],
// Additional custom Jest configurations
}
This package offers three distinct types of utilities to seed data into Firestore, each serving a different purpose and level of complexity:
seed{Model}
methods: These methods are your starting point for data initialization. They allow you to create just the basic Firestore document for a given model—no more, no less. Ideal for setting up minimal, isolated test scenarios or manually constructing a non-standard data setup (i.e member with broken user references, etc).
populate{Model}
methods: Once you've sown your "seed" with the basic document, you can use these methods to grow it by adding related data. These are particularly useful when you need to build upon a basic setup incrementally. For example, you can populate a team with members and depots after a seedTeam
.
bootstrap{Model}
methods: These utilities are designed for comprehensive data setup in a single step, ensuring that all relationships between the data entities are correctly configured. Use bootstrap when you need a fully-integrated, relationally coherent data set.
Available seeding methods:
import {
// Team
seedTeam, bootstrapTeam, populateTeam,
// User
seedUser, seedUsers, bootstrapUser, bootstrapUsers,
// Member
seedMember, seedMembers, bootstrapMember, bootstrapMembers,
// Plan
seedPlan, bootstrapPlan, populatePlan, seedPlanDriver, seedPlanDrivers,
// Route
seedRoute, seedRoutes, bootstrapRoute, populateRoute,
// Depot
seedDepot, seedDepots,
// Recipient
seedRecipient, seedRecipients, populateRecipient, bootstrapRecipient,
// Stop
seedStop, seedStopOnRoot, seedStops, seedStopsOnRoot,
// Package
seedPackages, seedPackagesOnRecipient,
// Products
seedProducts,
// Patches
seedPatches,
// Experiments
seedExperiment, seedExperiments,
// DriverZone
seedDriverZone, seedDriverZones,
// Breaks
seedBreaks,
// LocationLogs
seedLocationLogs,
// Shopify Sessions
seedShopifySession, seedShopifySessions
} from '@getcircuit/firestore-test';
Understanding which utility to use depends on the testing scenario you're facing:
Standard, Interconnected Data: If you're looking to simulate a real-world, production-like environment where all data entities maintain standard and correct relationships, bootstrap{Model}
methods are your go-to. They provide a comprehensive, functional data structure out of the box.
Edge Cases and Anomalies: When your tests require unconventional or incorrect data relationships—where you need to break the standard links between entities for testing purposes—opt for the seed{Model}
methods. These give you the flexibility to manually construct specific scenarios with granular control over data relationships.
Partial Data Extension: If you've already set up some basic data structures using seed{Model}
and want to extend them by adding related data entities, consider using populate{Model}
methods. These are ideal for incrementally building upon an existing setup.
By choosing the appropriate utility, you ensure that your tests are both effective and efficient, whether you're testing core functionalities or edge cases.
Even with the flexibility offered by seed
, populate
, and bootstrap
, there are unique test scenarios that these utilities might not cover completely. This is where "Seed Recipes" come into play.
Recipes are normal functions prefixed by prepare
that bootstraps/seeds multiple documents into the database for a given context. In contrast to the seeding utilities provided by this package, recipes may have a loose API and receive or return whatever fits the use case.
They are designed for particular test scenarios that don't fit the one-size-fits-all approach of general-purpose seeding utilities. For example, creating a team with a specific number of members, or a route with a specific number of stops, or create a team/user/route in one go.
We recommend organizing your Seed Recipes in a seed_recipes
folder within your project. Each module in this folder should contain one recipe function, prefixed by prepare
. All the recipes should then be re-exported by the index file of the seed_recipes
folder.
Here's a simple example:
// In seed_recipes/prepareDriverWithRoute.ts
/**
* Bootstraps a team with a driver member and assign them to the defined route.
*/
export async function prepareDriverWithRoute(/* ... */) {
// ...
}
// In seed_recipes/index.ts
export * from './prepareDriverWithRoute'
By adhering to these conventions and using Seed Recipes wisely, you can ensure that your Firestore tests are both effective and flexible, capable of handling both general use-cases and unique edge cases.
firebase-admin
exclusive utilitiesThe seeding utilities are designed to work in both the browser and node environments. However, when in the context of a node environment (firebase-admin
) any bootstrapped user will also have their Auth account created. This is not the case in the browser environment (firebase
), as the browser SDK doesn't allow for creating users directly.
The following utilities are only available in the firebase-admin
environment:
createIdTokenForEmailAndPassword
- Creates an id token for the given email and password.createIdTokenForUID
- Creates an id token for the given uid.seedAccount
- Seeds an account with the given data.findAccount
- Finds an account with the given email or phone number.Each utility allows for partial data input through a DeepPartial of the model. This enables you to specify only the data you need for a test, with any unspecified data falling back to defaults set in src/firebase/data-factories
.
For example, each seeded model may or may not receive an id
property. If no id
is provided, a random one will be generated.
await seedStops(collection, [
{ id: 'stop-1', name: 'Stop 1' }, // Stop 1 will have a known id
{ name: 'Stop 2' }, // Stop 2 will have a random id
])
While there are individual methods to seed users, members, depots, etc, you don't need to use them directly as explained in the when to use which section. Instead, you can use the populateTeam
or boostrapTeam
method to seed a team with all its related data.
For example, to seed a team with members and their respective users:
import {
seedTeam,
populateTeam,
bootstrapTeam,
} from '@getcircuit/firestore-test'
// Using both seedTeam and populateTeam
const teamSelectors1 = await seedTeam()
await populateTeam(teamSelectors1.teamRef, {
members: [
{ id: 'john', name: 'John Doe' },
{ id: 'jane', name: 'Jane Doe' },
],
})
// Using bootstrapTeam
const teamSelectors2 = await bootstrapTeam(
{},
{
members: [
{ id: 'john', name: 'John Doe' },
{ id: 'jane', name: 'Jane Doe' },
],
},
)
This will automatically create the users for the specified members, and link their DocumentReferences
accordingly.
We can then select these references with the help of the teamSelectors
object:
teamSelectors.membersRef() // => CollectionReference<Member>
teamSelectors.memberRef('john') // => DocumentReference<Member>
teamSelectors.userRef('john') // => DocumentReference<User>
In a testing environment where each test is expected to be isolated and seed its own data, the user
collection is shared among all tests. Importantly, there is no expectation to reset any Firestore data or collections between tests. This presents a risk of ID conflicts when multiple tests attempt to create a user with the same ID via their associated members.
To address this issue, the utilities handle IDs in a specific manner depending on the context:
Direct User Seeding: Using seedUser
or seedUsers
directly will not involve any automatic ID prefixing. What you specify is what you get.
Seeding via member
: When a user is created as part of member seeding through bootstrapTeam
or populateTeam
, the ID for the user is automatically prefixed with the ID of the team to which the member belongs. This ensures uniqueness across tests.
test('some test', async () => {
const teamSelectors = await bootstrapTeam(
{ id: 'team-1' },
{ members: [{ id: 'john', name: 'John Doe' }] },
)
// Member id can be `john`, as the member collection is isolated to the team
expect(teamSelectors.memberRef('john').id).toBe('john')
// User id is prefixed with the team id to avoid conflicts as the user collection is shared
expect(teamSelectors.userRef('john').id).toBe('team-1-john')
})
test('some other test', async () => {
const teamSelectors = await bootstrapTeam(
{ id: 'team-2' },
{ members: [{ id: 'john', name: 'John Doe' }] },
)
// Member id can be `john`, as the member collection is isolated to the team
expect(teamSelectors.memberRef('john').id).toBe('john')
// User id is prefixed with the team id to avoid conflicts as the user collection is shared
expect(teamSelectors.userRef('john').id).toBe('team-2-john')
})
If you need to reference this prefixed user ID, the returned teamSelectors
object provides a userId
method for convenience:
teamSelectors.userId('john') // => 'team-1-john'
This feature ensures that tests remain isolated in their data setup, thereby making the utilities robust and easy to use.
Creating cross-references between Firestore documents can be tricky, especially when the documents are being seeded simultaneously. For example, how can you link a member to a depot when both are just about to be created? This challenge arises often when using, for example, populateTeam
or bootstrapTeam
.
There are two main approaches to handle this:
populate
or bootstrap
You can use a deferred argument function to dynamically generate the seeding data. The function receives the selectors
as an argument, allowing you to reference previously created documents.
const teamSelectors = await bootstrapTeam({}, (selectors) => ({
depots: [{ id: 'depot-1' }],
members: [
{
id: 'john',
name: 'John Doe',
depot: selectors.depotRef('depot-1'), // Linking made easy
},
],
}))
For more granular control, you can use the seeding methods individually. This approach requires manually linking the references.
// First, seed the team
const teamSelectors = await seedTeam()
// Then, seed the depot
const { depotRef } = await seedDepot(teamSelectors.teamRef, {
id: 'depot-1',
name: 'Depot 1',
})
// Finally, bootstrap the member and link them to the depot
await bootstrapMember(teamSelectors.teamRef, {
id: 'john',
name: 'John Doe',
depot: depotRef,
})
By choosing the appropriate method, you can effectively manage the complexities of document cross-referencing in your Firestore tests.
export {
getTestFirestore,
getTestRefSelectors,
getUniqueTestRootRef,
waitForSnapshotListenersInSync,
fakeId,
fakeEmail,
fakePhoneNumber,
fakeShopifySessionId,
FIREBASE_TEST_PROJECT_ID,
} from '@getcircuit/firestore-test'
getTestFirestore()
returns a Firestore
instance that is connected to the emulator.
getTestRefSelectors()
returns a bag of ref selectors already attached to the test Firestore.
getUniqueTestRootRef()
returns a DocumentReference
that is unique to the current test. This is useful when you want to seed data that is specific to the current test without having to seed a whole team.
waitForSnapshotListenersInSync()
returns a promise that resolves when all active snapshot listeners are in sync with the remote firestore. This is useful when you want to guarantee that a test is waiting for a firestore document to be updated.
fakeId()
returns a random id that can be used to seed data.
fakeEmail()
returns a random email that can be used to seed data.
fakePhoneNumber()
returns a random phone number that can be used to seed data.
fakeShopifySessionId()
returns a random shopify session id that can be used to seed data.
FIREBASE_TEST_PROJECT_ID
is the id of the test project. It's _firebase_test_
currently.
waitForSnapshotListenersInSync()
is a function that returns a promise that resolves when all active snapshot listeners are in sync with the remote firestore. This is useful when you want to guarantee that a test is waiting for a firestore document to be updated.
Each test that connects to the firestore emulator is expected to be self-contained. This means that each test should seed its own data. Cleaning up is not expected nor necessary.
import { bootstrapTeam, bootstrapRoute } from '@getcircuit/firestore-test'
test('route is created', async () => {
// Bootstrap a team with all its related data, including members and users
const teamSelectors = await bootstrapTeam(
{},
{
members: [{ id: 'chris', name: 'Chris Doe' }],
},
)
// Bootstrap a route while linking it to the driver user and member documents
const routeSelectors = await bootstrapRoute(teamSelectors.teamRef, {
route: {
driver: teamSelectors.userRef('chris'),
member: teamSelectors.memberRef('chris'),
},
})
// Test assertions
expect('...').toEqual('...')
})
When your Firestore-related test doesn't require a complete team setup, you can achieve test isolation by using a unique Firestore root for each test. The getUniqueTestRootRef
function provides a convenient way to do this. This function returns a unique document reference pointing to test-suite/{randomId}
, effectively isolating each test's data.
By using this approach, you can prevent conflicts when running multiple tests that may otherwise interact with the same Firestore collection names.
import { getUniqueTestRootRef } from '@getcircuit/firestore-test'
test('test 1', async () => {
const rootRef = getUniqueTestRootRef()
const patches = rootRef.collection('patches')
// Perform operations on this patches collection, isolated from other tests
})
test('test 2', async () => {
const rootRef = getUniqueTestRootRef() // Different from the rootRef in the previous test
const patches = rootRef.collection('patches') // Despite having the same collection name, these are isolated instances
// Continue with test-specific operations on this isolated patches collection
})
By using getUniqueTestRootRef
, you ensure that each test has its own unique path in Firestore, allowing for conflict-free and independent test execution.
FAQs
<!-- @import "[TOC]" {cmd="toc" depthFrom=2 depthTo=4 orderedList=false} -->
The npm package @getcircuit/firestore-test receives a total of 0 weekly downloads. As such, @getcircuit/firestore-test popularity was classified as not popular.
We found that @getcircuit/firestore-test demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 19 open source maintainers 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 researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.