❌ Firebase trigger function random data + storage result testing
Hello
Version info
{
"name": "functions",
"main": "lib/src/index.js",
"scripts": {
. . .
"test": "mocha --timeout 20000 --reporter spec --require ts-node/register ./test/**/*.test.ts",
},
"engines": {
"node": "16"
},
"dependencies": {
"@firebase/rules-unit-testing": "^2.0.7",
"firebase-admin": "^11.10.1",
"firebase-functions": "^4.2.0"
},
"devDependencies": {
"@types/mocha": "10.0.1",
"@types/node": "20.4.8",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"eslint": "^8.9.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"firebase": "^9.23.0",
"firebase-functions-test": "3.1.0",
"mocha": "10.2.0",
"ts-node": "10.9.1",
"typescript": "^4.9.0"
},
"private": true
}
Test case
I have a trigger function that updates the storage. I simplified the function to this:
export const profileUpdate = onDocumentWritten(FIRESTORE.USERS, async (event) => {
logInfo(`✍ User updated - Updating profile users report. . .`);
logInfo(event?.data?.after.data());
logInfo(event?.data?.before.data());
admin.storage().bucket(STORAGE_BUCKET_NAME)
.file(STORAGE.USERS)
.save(JSON.stringify({ testing: "this does not make sense!" }));
return;
});
❌ Problem 1: In the console I get always the same random data that I don't know where is coming from instead of the data I provided. This is the result:
✅ before
[ '✍ User updated - Updating profile users report. . .' ]
[ { aString: 'foo', anObject: { a: 'qux', b: 'faz' }, aNumber: 7 } ] 👈 Random data
[ { anObject: { a: 'bar' }, aNumber: 7 } ] 👈 Random data
👨🏫 Exists: true
👨🏫 Downloading file . . .
{ testing: 'this does not make sense!' }
✔ tests a Cloud Firestore function (942ms)
✅ after
❌ Problem 2: I don't find the way to wait for the function to update the storage (either using firebase emulator preferably or a new "testing project" in Firebase)
Steps to reproduce
This is the code for the testing file:
import 'mocha';
import assert = require('assert');
import {
COLLECTIONS, PROJECT_ID, STORAGE, STORAGE_BUCKET_NAME,
} from "../../../utils/setup";
import {
normalUser
} from "../../../utils/users";
// that detects onDocumentWritten and then checks if the document exists or not
import admin = require("firebase-admin");
import { profileUpdate } from '../../../../src/ui/profile';
const firebaseFunctionEnv = require("firebase-functions-test")(
{
projectId: PROJECT_ID,
storage: STORAGE_BUCKET_NAME,
databaseURL: 'https://${PROJECT_ID}.firebaseio.com',
},
"./service-account.json"
);
describe.only('Storage testing for ${STORAGE.USERS}', () => {
after(() => {
firebaseFunctionEnv.cleanup();
});
// let db: RulesTestEnvironment;
before(async () => {
console.log("✅ before");
});
after(async () => {
console.log("✅ after");
});
it("tests a Cloud Firestore function", async () => {
// Create a fake event object
const mockedData = {
uid: normalUser.uid,
displayName: normalUser.displayName,
email: normalUser.email,
}
// Make a fake document snapshot to pass to the function
const before = firebaseFunctionEnv.firestore.makeDocumentSnapshot(
{},
"/" + COLLECTIONS.USERS + "/" + normalUser.uid
);
const after = firebaseFunctionEnv.firestore.makeDocumentSnapshot(
mockedData,
"/" + COLLECTIONS.USERS + "/" + normalUser.uid
);
// Make change
const change = firebaseFunctionEnv.makeChange(before, after);
const profileUpdateWrapped = firebaseFunctionEnv.wrap(profileUpdate);
await profileUpdateWrapped(change, { params: { userId: normalUser.uid } });
// Get the profile user list from the Storage emulator
try {
let fileRef = admin.storage().bucket(STORAGE_BUCKET_NAME).file(STORAGE.USERS);
let fileRefExists: any = await fileRef.exists();
fileRefExists = fileRefExists[0];
console.log("👨🏫 Exists: ", fileRefExists);
if (fileRefExists) {
assert.ok(true);
} else {
assert.fail("The file does not exist");
}
console.log("👨🏫 Downloading file . . .");
let fileContent = await fileRef.download();
fileContent = JSON.parse(fileContent.toString());
console.log(fileContent);
} catch (e: any) {
console.log(e);
assert.fail("The file was not modified/created");
}
assert.ok(true);
}).timeout(20000);
});
Expected behavior
I would expect to receive in my firebase function the data I have passed which is:
const mockedData = {
uid: normalUser.uid,
displayName: normalUser.displayName,
email: normalUser.email,
}
Actual behavior
I am getting this random data coming from nowhere when calling makeDocumentSnapshot, makeChange, wrap methods from firebase-function-testing:
[ { aString: 'foo', anObject: { a: 'qux', b: 'faz' }, aNumber: 7 } ]
[ { anObject: { a: 'bar' }, aNumber: 7 } ]
Right now I am using mocha for the testing and directly a firebase project instead of the emulator so I am running the test either with:
mocha --timeout 20000 --reporter spec --require ts-node/register ./test/**/*.test.ts
or this:
firebase emulators:exec --project audit-6b96e 'npm run test'
Thanks in advance for your help, Pedro Reyes.
😊
😬
@PedroReyes - I am expecting the same behaviour. Did you managed to solve this issue?
Hello @lpotapczuk ,
I gave up trying to use this firebase suite.
What I did as a solution:
- Run firebase emulator as usual
- Run the scripts for testing giving some time between writes to test that the final result you are expecting in the firestore or storage does ocurr indeed. I am using 5000 millseconds.
I will show here one of my "subtests" of a test suite for the user profile updates test so you can take it directly. Once you understand this one you can simply expand it to your needs.
// https://firebase.google.com/docs/firestore/security/test-rules-emulator
import "mocha";
import assert = require("assert");
import admin = require("firebase-admin");
import { initAdminApp } from "../../utils/setup";
import { createRandomFirestoreUser, wait } from "../../utils/utils";
import { FIRESTORE, STORAGE } from "../../../src/shared/utils/setup";
import { StorageMetadata } from "../../../src/shared/types/storage";
import { FirebaseUser } from "../../../src/shared/types/users";
initAdminApp();
/**
* 📘 This test mainly checks that firestore document timestamp is updated whenever
* an update happens in the storage document. This is very helpful to know when
* the storage document was updated for the last time and for updating in real time
* the frontend. We "monitor" the firestore document that matches the storage document
* and we can react to changes in real time in the frontend.
*/
describe(`📖 Firestore user updates`, () => {
let userForDeletion: FirebaseUser;
let userForUpdates: FirebaseUser;
const WAITING_TIME = 5000;
before(async () => {
// Create user in firestore to be able to trigger the function for deletion
userForDeletion = await createRandomFirestoreUser(admin);
// Create user in firestore to be able to trigger the function for updates
userForUpdates = await createRandomFirestoreUser(admin);
// Wait for trigger function to complete
await wait(WAITING_TIME);
});
it(`should create ${STORAGE.USERS_DOC_PATH} 🟩 if it doesn't exist`, async () => {
try {
// Get current date in firestore
(
await admin.firestore().doc(FIRESTORE.STORAGE(STORAGE.USERS_DOC_PATH)).get()
).data() as StorageMetadata;
assert.fail("Should not be any document");
} catch (e) {
assert.ok(true);
}
// Create random number
await createRandomFirestoreUser(admin);
// Wait for trigger function to complete
await wait(WAITING_TIME);
// Get current date in firestore
const metadataFileAfter: StorageMetadata = (
await admin.firestore().doc(FIRESTORE.STORAGE(STORAGE.USERS_DOC_PATH)).get()
).data() as StorageMetadata;
assert.ok(metadataFileAfter?.generation !== undefined);
});
});
As you can see I init the admin firebase with a function:
/**
* Initializes the admin app and sets environment variables for
* development environment.
*
* You will have to set this at the beginning of the test in case of having problems with
* environment credentials to setup the admin app.
*/
export async function initAdminApp() {
// Using admin SDK to connect to firestore emulator in local tests - https://github.com/firebase/firebase-admin-node/issues/776#issuecomment-1129048690
// Set this environment variables if we are in a development environment
process.env["FUNCTIONS_EMULATOR"] = "true";
process.env["FIRESTORE_EMULATOR_HOST"] = "localhost:8080";
process.env["FIREBASE_STORAGE_EMULATOR_HOST"] = "localhost:9199";
if (admin.apps.length === 0) {
logInfo("🤖 Initializing admin app 🔁");
admin.initializeApp({
projectId: `${PROJECT_ID}`,
storageBucket: `${STORAGE_BUCKET_NAME}`,
});
logInfo("🤖 Admin app initialized ✅");
} else {
logInfo("🤖 Admin app already initialized 🆗");
}
}
My "wait" time function is something simple:
/**
* The `wait` function is an asynchronous function that waits for a specified delay before resolving.
* @param [delay=1000] - The `delay` parameter is the amount of time in milliseconds that the function
* should wait before resolving the promise. By default, it is set to 1000 milliseconds (1 second).
*/
export async function wait(delay = 1000) {
await new Promise((resolve: any) => setTimeout(resolve, delay));
}
And here are the function for creating random users:
/**
* The function creates a random Firebase user object with a unique ID, display name, email, and
* default user role.
* @returns a new FirebaseUser object with randomly generated values for uid, displayName, email, and
* roles.
*/
export function createRandomFirebaseUser(): FirebaseUser {
const randomNumber: number = Math.floor(Math.random() * 1000000000000000);
// Create new FirebaseUser object
const newUser: FirebaseUser = {
uid: `${randomNumber}_Uid`,
displayName: `${randomNumber}_DisplayName`,
email: `${randomNumber}_Email`,
photoURL: `${randomNumber}_PhotoURL`,
};
return newUser;
}
/**
* The function creates a random user and saves it to a Firestore database.
* @returns a `FirebaseUser` object.
*/
export async function createRandomFirestoreUser(firebaseAdmin: any) {
const newUser: FirebaseUser = createRandomFirebaseUser();
// Create new user
if (newUser) {
await firebaseAdmin.firestore().doc(FIRESTORE.USERS(newUser.uid)).set(newUser);
}
return newUser;
}
This is my command script from package json to run the triggers in my CI/CD for example:
- run: npm ci && npm run build && firebase emulators:exec 'npm run test:triggers'
The command npm run test:triggers is a package.json script command, the next one:
"test:triggers": "node --experimental-vm-modules --dns-result-order=ipv4first node_modules/mocha/bin/_mocha --timeout 40000 --reporter spec --require ts-node/register ./test/triggers/**/*.test.ts",
I'd say that's all you need to make my code yours.
Best regards, Pedro Reyes.
@PedroReyes thank you!
Moment after I've posted the message, I've found the solution to our problem in another thread:
https://github.com/firebase/firebase-functions-test/issues/205
Basically, the solution was to modify:
await wrapped(change);
to
await wrapped({ data: change, params: {...} });
@lpotapczuk thank you!
I'll give it a try as soon as I can ✌️