Skip to content

Commit

Permalink
test: write tests for firebase functions
Browse files Browse the repository at this point in the history
  • Loading branch information
czabaj committed Sep 19, 2024
1 parent 2c89135 commit 003493d
Show file tree
Hide file tree
Showing 9 changed files with 2,066 additions and 260 deletions.
9 changes: 4 additions & 5 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
{
"npm.packageManager": "yarn",
"testing.automaticallyOpenPeekView": "never",
"vitest.enable": true,
"vitest.include": [
"**/*._test.bs.js",
"**/*.{test,spec,_test.bs}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"
]
"jest.enable": true,
"jest.jestCommandLine": "yarn test",
"jest.rootPath": "functions",
"jest.runMode": "on-demand"
}
3 changes: 2 additions & 1 deletion firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint",
"npm --prefix \"$RESOURCE_DIR\" run build"
]
],
"runtime": "nodejs20"
}
],
"hosting": {
Expand Down
4 changes: 4 additions & 0 deletions functions/loadEnv.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* eslint-disable @typescript-eslint/no-require-imports */
const dotenv = require("dotenv");

dotenv.config({ path: "../.env.local" });
26 changes: 22 additions & 4 deletions functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,36 @@
"firebase-functions": "^6.0.0"
},
"devDependencies": {
"@types/jest": "^29.5.13",
"@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.5.0",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-import": "^2.30.0",
"firebase-functions-test": "^3.3.0",
"mocha": "^10.7.3",
"jest": "^29.7.0",
"parcel": "^2.12.0",
"ts-jest": "^29.2.5",
"typescript": "^5.6.2"
},
"engines": {
"node": "18"
"jest": {
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"setupFilesAfterEnv": [
"<rootDir>/loadEnv.cjs"
],
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$"
},
"main": "lib/index.js",
"name": "functions",
Expand All @@ -30,7 +47,8 @@
"serve": "npm run build && firebase emulators:start --only functions",
"shell": "npm run build && firebase functions:shell",
"start": "npm run shell",
"test": "mocha --reporter spec"
"test": "FIRESTORE_EMULATOR_HOST=\"127.0.0.1:9090\" jest",
"test:emulators": "firebase emulators:exec \"jest --watchAll\""
},
"source": "src/index.ts"
}
6 changes: 6 additions & 0 deletions functions/src/__tests__/offline.test.ts.off
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import "jest";

import * as admin from "firebase-admin";
import functions from "firebase-functions-test";

const testEnv = functions();
223 changes: 223 additions & 0 deletions functions/src/__tests__/online.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import * as path from "node:path";

import "jest";

import functions from "firebase-functions-test";
import * as admin from "firebase-admin";
import {
getFirestore,
Timestamp,
CollectionReference,
} from "firebase-admin/firestore";

import * as myFunctions from "../index";
import { UserRole } from "../../../src/backend/UserRoles";

const testEnv = functions(
{
projectId: process.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: process.env.VITE_FIREBASE_STORAGE_BUCKET,
},
path.join(__dirname, "../../../../beerbook2-da255-1c582faf4499.json")
);

afterAll(() => {
testEnv.cleanup();
});

// Make a fake document snapshot to pass to the function
const mock = testEnv.firestore.makeDocumentSnapshot(
{
text: "hello world",
},
"/lowercase/foo"
);

describe(`deletePlaceSubcollection`, () => {
const addPlace = async (opts: { placeId: string; withKegs: boolean }) => {
const db = getFirestore();
const placeCollection = db.collection("places");
const placeDoc = placeCollection.doc(opts.placeId);
await placeDoc.set({
createdAt: Timestamp.now(),
name: "Test Place",
});
const result = {
kegsCollection: undefined as CollectionReference<any> | undefined,
personsCollection: placeDoc.collection("persons"),
personsIndexCollection: placeDoc.collection("personsIndex"),
placeDoc,
};
const promises = [
result.personsCollection.add({
createdAt: Timestamp.now(),
}),
result.personsIndexCollection
.doc(`1`)
.set({ all: { tester: ["Tester"] } }),
];
if (opts.withKegs) {
result.kegsCollection = placeDoc.collection("kegs");
promises.push(
result.kegsCollection.add({
beer: "Test Beer",
createdAt: Timestamp.now(),
})
);
}
await Promise.all(promises);
return result;
};

it(`should delete place sub-collestion on placeDelete, new place withou kegs collection`, async () => {
const { personsCollection, personsIndexCollection, placeDoc } =
await addPlace({
placeId: `test_deleteSubCollection_noKegs`,
withKegs: false,
});
const wrapped = testEnv.wrap(myFunctions.deletePlaceSubcollection);
await wrapped({ params: { placeId: placeDoc.id } });
expect((await personsCollection.get()).empty).toBe(true);
expect((await personsIndexCollection.get()).empty).toBe(true);
});

it(`should delete place sub-collestion on placeDelete, including kegs`, async () => {
const { kegsCollection, placeDoc } = await addPlace({
placeId: `test_deleteSubCollection_withKegs`,
withKegs: true,
});
const wrapped = testEnv.wrap(myFunctions.deletePlaceSubcollection);
await wrapped({ params: { placeId: placeDoc.id } });
expect((await kegsCollection!.get()).empty).toBe(true);
});
});

describe(`truncateUserInDb`, () => {
const addUserPlace = async (opts: {
placeId: string;
userUid: string;
userRole: UserRole;
moreUsers?: Array<{ uid: string; role?: UserRole }>;
}) => {
const db = getFirestore();
const placeCollection = db.collection("places");
const placeDoc = placeCollection.doc(opts.placeId);
const docData = {
createdAt: Timestamp.now(),
name: "Test Place",
users: {
[opts.userUid]: opts.userRole,
...opts.moreUsers?.reduce((acc, u) => {
if (u.role) {
acc[u.uid] = u.role;
}
return acc;
}, {} as Record<string, UserRole>),
},
};
await placeDoc.set(docData);
const personsIndexCollection = placeDoc.collection("personsIndex");
const personsIndexDocData = {
[opts.userUid]: [opts.userUid, Timestamp.now(), 0, opts.userUid],
...opts.moreUsers?.reduce((acc, u) => {
acc[u.uid] = [u.uid, Timestamp.now(), 0, u.uid];
return acc;
}, {} as Record<string, [string, Timestamp, number, string]>),
};
const personsIndexDoc = personsIndexCollection.doc(`1`);
await personsIndexDoc.set({
all: personsIndexDocData,
});

return { personsIndexDoc, placeDoc };
};

it(`should delete the place when it has only connected user`, async () => {
const placeId = `test_truncateUserInDb_ownerOnly`;
const userUid = `owner_only`;
const { placeDoc } = await addUserPlace({
placeId,
userUid,
userRole: UserRole.owner,
});
const wrapped = testEnv.wrap(myFunctions.truncateUserInDb);
await wrapped({ uid: userUid });
expect((await placeDoc.get()).exists).toBe(false);
});

it(`should transfer the ownerhip to the next highest rank`, async () => {
const placeId = `test_truncateUserInDb_transferOwnership`;
const userUid = `owner`;
const { placeDoc } = await addUserPlace({
placeId,
userUid,
userRole: UserRole.owner,
moreUsers: [
{ uid: `user_staff`, role: UserRole.staff },
{ uid: `user_no_role` },
{ uid: `user_admin`, role: UserRole.admin },
],
});
const wrapped = testEnv.wrap(myFunctions.truncateUserInDb);
await wrapped({ uid: userUid });
const placeData = (await placeDoc.get()).data();
expect(placeData!.users[`user_admin`]).toBe(UserRole.owner);
});

it(`should delete relationship to the user from the place when non-owner`, async () => {
const placeId = `test_truncateUserInDb_destroyRelationship`;
const userUid = `owner`;
const { personsIndexDoc, placeDoc } = await addUserPlace({
placeId,
userUid,
userRole: UserRole.staff,
moreUsers: [
{ uid: `user_owner`, role: UserRole.owner },
{ uid: `user_no_role` },
],
});
const wrapped = testEnv.wrap(myFunctions.truncateUserInDb);
await wrapped({ uid: userUid });
const placeData = (await placeDoc.get()).data();
expect(placeData!.users[userUid]).toBeUndefined();
// the personsIndex should contain the user, but with null at 3th position (userId)
expect((await personsIndexDoc.get()).data()!.all[userUid][3]).toBe(null);
});

it(`should handle all places where user parcitipates`, async () => {
const userUid = `mixed`;
const placeOwnerOnly = `test_truncateUserInDb_manyPlacesOwnerOnly`;
const { placeDoc: placeDocOwnerOnly } = await addUserPlace({
placeId: placeOwnerOnly,
userUid,
userRole: UserRole.owner,
});
const placeStaff = `test_truncateUserInDb_manyPlacesStaff`;
const { placeDoc: placeDocStaff } = await addUserPlace({
placeId: placeStaff,
userUid,
userRole: UserRole.staff,
moreUsers: [
{ uid: `user_owner`, role: UserRole.owner },
{ uid: `user_no_role` },
],
});
const placeAdmin = `test_truncateUserInDb_manyPlacesAdmin`;
const { placeDoc: placeDocAdmin } = await addUserPlace({
placeId: placeAdmin,
userUid,
userRole: UserRole.admin,
moreUsers: [
{ uid: `user_owner`, role: UserRole.owner },
{ uid: `user_no_role` },
],
});
const wrapped = testEnv.wrap(myFunctions.truncateUserInDb);
await wrapped({ uid: userUid });
expect((await placeDocOwnerOnly.get()).exists).toBe(false);
expect((await placeDocStaff.get()).exists).toBe(true);
expect((await placeDocStaff.get()).data()!.users[userUid]).toBe(undefined);
expect((await placeDocAdmin.get()).exists).toBe(true);
expect((await placeDocAdmin.get()).data()!.users[userUid]).toBe(undefined);
});
});
Loading

0 comments on commit 003493d

Please sign in to comment.