Skip to content

Add withFunctionsTriggersDisabled method to rules-unit-testing #3928

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Oct 30, 2020
5 changes: 5 additions & 0 deletions .changeset/lazy-elephants-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/rules-unit-testing': minor
---

Add withFunctionTriggersDisabled function to facilitate test setup
3 changes: 2 additions & 1 deletion packages/rules-unit-testing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ export {
initializeAdminApp,
initializeTestApp,
loadDatabaseRules,
loadFirestoreRules
loadFirestoreRules,
withFunctionTriggersDisabled
} from './src/api';
171 changes: 107 additions & 64 deletions packages/rules-unit-testing/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ const FIRESTORE_ADDRESS_ENV: string = 'FIRESTORE_EMULATOR_HOST';
/** The default address for the local Firestore emulator. */
const FIRESTORE_ADDRESS_DEFAULT: string = 'localhost:8080';

/** Environment variable to locate the Emulator Hub */
const HUB_HOST_ENV: string = 'FIREBASE_EMULATOR_HUB';
/** The default address for the Emulator hub */
const HUB_HOST_DEFAULT: string = 'localhost:4040';

/** The actual address for the database emulator */
let _databaseHost: string | undefined = undefined;

Expand Down Expand Up @@ -306,7 +311,7 @@ export type LoadDatabaseRulesOptions = {
databaseName: string;
rules: string;
};
export function loadDatabaseRules(
export async function loadDatabaseRules(
options: LoadDatabaseRulesOptions
): Promise<void> {
if (!options.databaseName) {
Expand All @@ -317,33 +322,25 @@ export function loadDatabaseRules(
throw Error('must provide rules to loadDatabaseRules');
}

return new Promise((resolve, reject) => {
request.put(
{
uri: `http://${getDatabaseHost()}/.settings/rules.json?ns=${
options.databaseName
}`,
headers: { Authorization: 'Bearer owner' },
body: options.rules
},
(err, resp, body) => {
if (err) {
reject(err);
} else if (resp.statusCode !== 200) {
reject(JSON.parse(body).error);
} else {
resolve();
}
}
);
const resp = await requestPromise({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also DRY-ed up a lot of the request boillerplate in this file.

method: 'PUT',
uri: `http://${getDatabaseHost()}/.settings/rules.json?ns=${
options.databaseName
}`,
headers: { Authorization: 'Bearer owner' },
body: options.rules
});

if (resp.statusCode !== 200) {
throw new Error(JSON.parse(resp.body.error));
}
}

export type LoadFirestoreRulesOptions = {
projectId: string;
rules: string;
};
export function loadFirestoreRules(
export async function loadFirestoreRules(
options: LoadFirestoreRulesOptions
): Promise<void> {
if (!options.projectId) {
Expand All @@ -354,64 +351,96 @@ export function loadFirestoreRules(
throw new Error('must provide rules to loadFirestoreRules');
}

return new Promise((resolve, reject) => {
request.put(
{
uri: `http://${getFirestoreHost()}/emulator/v1/projects/${
options.projectId
}:securityRules`,
body: JSON.stringify({
rules: {
files: [{ content: options.rules }]
}
})
},
(err, resp, body) => {
if (err) {
reject(err);
} else if (resp.statusCode !== 200) {
console.log('body', body);
reject(JSON.parse(body).error);
} else {
resolve();
}
const resp = await requestPromise({
method: 'PUT',
uri: `http://${getFirestoreHost()}/emulator/v1/projects/${
options.projectId
}:securityRules`,
body: JSON.stringify({
rules: {
files: [{ content: options.rules }]
}
);
})
});

if (resp.statusCode !== 200) {
throw new Error(JSON.parse(resp.body.error));
}
}

export type ClearFirestoreDataOptions = {
projectId: string;
};
export function clearFirestoreData(
export async function clearFirestoreData(
options: ClearFirestoreDataOptions
): Promise<void> {
if (!options.projectId) {
throw new Error('projectId not specified');
}

return new Promise((resolve, reject) => {
request.delete(
{
uri: `http://${getFirestoreHost()}/emulator/v1/projects/${
options.projectId
}/databases/(default)/documents`,
body: JSON.stringify({
database: `projects/${options.projectId}/databases/(default)`
})
},
(err, resp, body) => {
if (err) {
reject(err);
} else if (resp.statusCode !== 200) {
console.log('body', body);
reject(JSON.parse(body).error);
} else {
resolve();
}
}
const resp = await requestPromise({
method: 'DELETE',
uri: `http://${getFirestoreHost()}/emulator/v1/projects/${
options.projectId
}/databases/(default)/documents`,
body: JSON.stringify({
database: `projects/${options.projectId}/databases/(default)`
})
});

if (resp.statusCode !== 200) {
throw new Error(JSON.parse(resp.body.error));
}
}

/**
* Run a setup function with background Cloud Functions triggers disabled. This can be used to
* import data into the Realtime Database or Cloud Firestore emulator without triggering locally
* emulated Cloud Functions.
*
* This method only works with Firebase CLI version {TODO} or higher.
*
* @param fn an function which returns a promise.
*/
export async function withFunctionTriggersDisabled<TResult>(
fn: () => TResult | Promise<TResult>
): Promise<TResult> {
// TODO: Find the hub
let hubHost = process.env[HUB_HOST_ENV];
if (!hubHost) {
console.warn(
`${HUB_HOST_ENV} is not set, assuming the Emulator hub is running at ${HUB_HOST_DEFAULT}`
);
hubHost = HUB_HOST_DEFAULT;
}

// Disable background triggers
const disableRes = await requestPromise({
method: 'PUT',
uri: `http://${hubHost}/functions/disableBackgroundTriggers/`
});
if (disableRes.statusCode !== 200) {
throw new Error(
`HTTP Error ${disableRes.statusCode} when disabling functions triggers, are you using the latest version of the Firebase CLI?`
);
}

// Run the user's function
const result = await fn();

// Re-enable background triggers
const enableRes = await requestPromise({
method: 'PUT',
uri: `http://${hubHost}/functions/enableBackgroundTriggers/`
});
if (enableRes.statusCode !== 200) {
throw new Error(
`HTTP Error ${enableRes.statusCode} when enabling functions triggers, are you using the latest version of the Firebase CLI?`
);
}

// Return the user's function result
return result;
}

export function assertFails(pr: Promise<any>): any {
Expand Down Expand Up @@ -440,3 +469,17 @@ export function assertFails(pr: Promise<any>): any {
export function assertSucceeds(pr: Promise<any>): any {
return pr;
}

function requestPromise(
options: request.CoreOptions & request.UriOptions
): Promise<{ statusCode: number; body: any }> {
return new Promise((resolve, reject) => {
request(options, (err, resp, body) => {
if (err) {
reject(err);
} else {
resolve({ statusCode: resp.statusCode, body });
}
});
});
}
8 changes: 8 additions & 0 deletions packages/rules-unit-testing/test/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,4 +318,12 @@ describe('Testing Module Tests', function () {
it('there is a way to get firestore timestamps', function () {
expect(firebase.firestore.FieldValue.serverTimestamp()).not.to.be.null;
});

it('disabling function triggers does not throw, returns value', function () {
const res = firebase.withFunctionTriggersDisabled(() => {
return new Promise((res) => res(1234));
});

expect(res).to.eq(1234);
});
});