Skip to content

Commit

Permalink
add test for cleaning roots
Browse files Browse the repository at this point in the history
This also refactors out the initialization code from the 2 tests.
It took a while to figure out the right configuration so Jest would not pick up two versions of firebase-admin dependency since it also exists in the parent node_modules.
  • Loading branch information
scytacki committed Sep 11, 2024
1 parent a926f13 commit 2fd2247
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 39 deletions.
7 changes: 6 additions & 1 deletion functions-v2/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ module.exports = {
"^firebase-admin$": "<rootDir>/node_modules/firebase-admin",
"^firebase-admin/firestore$": "<rootDir>/node_modules/firebase-admin/lib/firestore",
"^firebase-admin/app$": "<rootDir>/node_modules/firebase-admin/lib/app",
}
"^firebase-admin/database$": "<rootDir>/node_modules/firebase-admin/lib/database",
},
// The tests can't be run in parallel because they are using a shared Firestore and
// Realtime database.
maxWorkers: 1,
};

// This is configured here because the clearFirebaseData function from
Expand All @@ -19,3 +23,4 @@ module.exports = {
// The port here should match the port that is set in the emulators
// section of firebase.json
process.env["FIRESTORE_EMULATOR_HOST"]="127.0.0.1:8088";
process.env["FIREBASE_DATABASE_EMULATOR_HOST"]="127.0.0.1:9000";
5 changes: 2 additions & 3 deletions functions-v2/src/at-midnight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ export const atMidnight = onSchedule("0 0 * * *", runAtMidnight);
export async function runAtMidnight() {
await cleanFirebaseRoots({
appMode: "qa",
// hoursAgo: 6,
hoursAgo: 90.7,
hoursAgo: 6,
logger,
dryRun: true,
dryRun: false,
});

// When cleanFirebaseRoots is called from a NodeJS script it is
Expand Down
97 changes: 76 additions & 21 deletions functions-v2/test/at-midnight.test.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,94 @@
import initializeFFT from "firebase-functions-test";
import {
clearFirestoreData,
} from "firebase-functions-test/lib/providers/firestore";
import {initializeApp} from "firebase-admin/app";
// import {getFirestore} from "firebase-admin/firestore";
import {getFirestore, Timestamp} from "firebase-admin/firestore";
import {getDatabase} from "firebase-admin/database";
import * as logger from "firebase-functions/logger";
import {initialize, projectConfig} from "./initialize";
import {runAtMidnight} from "../src/at-midnight";

process.env["FIRESTORE_EMULATOR_HOST"]="127.0.0.1:8088";
const projectConfig = {projectId: "demo-test"};
const fft = initializeFFT(projectConfig);
jest.mock("firebase-functions/logger");

// When the function is running in the cloud initializeApp is called by index.ts
// Here we are importing the function's module directly so we can call
// initializeApp ourselves. This is beneficial since initializeApp needs to
// be called after initializeFFT above.
initializeApp();
const {cleanup} = initialize();

// type CollectionRef = admin.firestore.CollectionReference<
// admin.firestore.DocumentData, admin.firestore.DocumentData
// >;
const HOUR = 1000 * 60 * 60;

async function writeFirestoreRoot(lastLaunchMillis = 0) {
const newRoot = getFirestore()
.collection("qa")
.doc();

await newRoot.set({
lastLaunchTime: Timestamp.fromMillis(lastLaunchMillis),
});

// Add some sub docs to make sure they are deleted
await newRoot.collection("users").doc().set({
uid: "test-user",
});

return newRoot;
}

async function writeDatabaseRoot(rootId: string) {
getDatabase().ref("qa").child(rootId).set({someField: "firebase realtime database"});
}

// In other tests we use firebase-functions-test to wrap the function.
// In this case it would look like:
// const wrapped = fft.wrap(atMidnight);
// However the wrapper doesn't support onSchedule:
// - The Typescript types don't allow it
// - at run time it doesn't pass the right event:
// https://github.com/firebase/firebase-functions-test/issues/210
// So instead the code is separated from the onSchedule and called directly.

describe("atMidnight", () => {
beforeEach(async () => {
await clearFirestoreData(projectConfig);
await getDatabase().ref().set(null);
});

test("clean up firestore roots", async () => {
// The wrapper doesn't support onSchedule. The Typescript types don't allow it
// and at run time it doesn't pass the right event:
// https://github.com/firebase/firebase-functions-test/issues/210
// const wrapped = fft.wrap(atMidnight);
test("clean up firestore roots with no database roots", async () => {
await writeFirestoreRoot();
await runAtMidnight();

const roots = await getFirestore().collection("qa").get();
expect(roots.size).toBe(0);
expect(logger.info)
.toHaveBeenCalledWith("Found 1 roots to delete");
});

test("clean up firestore root and database root", async () => {
const firestoreRoot = await writeFirestoreRoot();
await writeDatabaseRoot(firestoreRoot.id);

await runAtMidnight();

const fsRoots = await getFirestore().collection("qa").get();
expect(fsRoots.size).toBe(0);
const dbRoots = await getDatabase().ref("qa").get();
expect(dbRoots.val()).toEqual(null);
expect(logger.info)
.toHaveBeenCalledWith("Found 1 roots to delete");
});

test("only clean up firestore roots older than 6 hours", async () => {
await writeFirestoreRoot(Date.now() - HOUR);
await writeFirestoreRoot(Date.now() - 2*HOUR);
await writeFirestoreRoot(Date.now() - 5*HOUR);
await writeFirestoreRoot(Date.now() - 8*HOUR);
await writeFirestoreRoot(Date.now() - 24*HOUR);

await runAtMidnight();

const roots = await getFirestore().collection("qa").get();
expect(roots.size).toBe(3);
expect(logger.info)
.toHaveBeenCalledWith("Found 2 roots to delete");
});

afterAll(() => {
fft.cleanup();
afterAll(async () => {
await cleanup();
});
});
30 changes: 30 additions & 0 deletions functions-v2/test/initialize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {deleteApp, initializeApp} from "firebase-admin/app";
import initializeFFT from "firebase-functions-test";

export const projectConfig = {
projectId: "demo-test",
// This URL doesn't have to be valid, it just has to a non empty string
// The actual database host will be picked up from
// FIREBASE_DATABASE_EMULATOR_HOST
// This is defined in jest.config.js
databaseURL: "https://not-a-project.firebaseio.com",
};

export function initialize() {
const fft = initializeFFT(projectConfig);

// When the function is running in the cloud initializeApp is called by index.ts
// In our tests we import the function's module directly so we can call
// initializeApp ourselves. This is beneficial since initializeApp needs to
// be called after initializeFFT above.
const fbApp = initializeApp();

const cleanup = async () => {
fft.cleanup();
// Deleting the Firebase app is necessary for the Jest tests to exit when they
// are complete. FFT creates a testApp which it deletes in cleanup(), but
// we are not using this testApp.
await deleteApp(fbApp);
};
return {fft, fbApp, cleanup};
}
16 changes: 4 additions & 12 deletions functions-v2/test/on-user-doc-written.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
import initializeFFT from "firebase-functions-test";
import {
clearFirestoreData,
} from "firebase-functions-test/lib/providers/firestore";
import * as logger from "firebase-functions/logger";
import * as admin from "firebase-admin";
import {initialize, projectConfig} from "./initialize";
import {onUserDocWritten} from "../src/on-user-doc-written";

jest.mock("firebase-functions/logger");

process.env["FIRESTORE_EMULATOR_HOST"]="127.0.0.1:8088";
const projectConfig = {projectId: "demo-test"};
const fft = initializeFFT(projectConfig);

// When the function is running in the cloud initializeApp is called by index.ts
// Here we are importing the function's module directly so we can call
// initializeApp ourselves. This is beneficial since initializeApp needs to
// be called after initializeFFT above.
admin.initializeApp();
const {fft, cleanup} = initialize();

type CollectionRef = admin.firestore.CollectionReference<
admin.firestore.DocumentData, admin.firestore.DocumentData
Expand Down Expand Up @@ -288,7 +280,7 @@ describe("functions", () => {
});
});

afterAll(() => {
fft.cleanup();
afterAll(async () => {
await cleanup();
});
});
5 changes: 4 additions & 1 deletion functions-v2/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
"typeRoots": ["./node_modules/@types"],
"paths": {
// These are necessary so code imported from ../shared/ will use the same version of
// firebase-admin that the local code does.
// firebase-admin that the local code does. Technically only "firebase-admin/firestore"
// seems to be currently required, but it seems safer to alias all of the admin
// libraries the shared code might be using.
"firebase-admin": ["./node_modules/firebase-admin/lib"],
"firebase-admin/firestore": ["./node_modules/firebase-admin/lib/firestore"],
"firebase-admin/app": ["./node_modules/firebase-admin/lib/app"],
"firebase-admin/database": ["./node_modules/firebase-admin/lib/database"],
},

},
Expand Down
1 change: 0 additions & 1 deletion shared/clean-firebase-roots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export async function cleanFirebaseRoots(
// and try again later.
const databasePath = `/${appMode}/${root.id}`;
logger.info(`Deleting Realtime Database root: ${databasePath} ...`);
// TODO: what if the ref doesn't exist?
if (!dryRun) await getDatabase().ref(`/${appMode}/${root.id}`).remove();
logger.info(`Deleting Firestore root: ${root.ref.path} ...`);
if (!dryRun) await getFirestore().recursiveDelete(root.ref);
Expand Down

0 comments on commit 2fd2247

Please sign in to comment.