firestore-vitest-mock
Version:
Vitest helper for mocking Google Cloud Firestore
603 lines (553 loc) • 20.4 kB
text/typescript
import { describe, expect, test, beforeAll } from "vitest";
import {
mockCollection,
mockDoc,
mockGet,
mockWhere,
mockOffset,
Timestamp,
Query,
DocumentReference,
FieldPath,
} from "../mocks/firestore";
import { mockFirebase } from "..";
import { type FirebaseUser } from "../mocks/auth";
describe("Queries", () => {
mockFirebase(
{
database: {
animals: [
{
id: "monkey",
name: "monkey",
type: "mammal",
legCount: 2,
food: ["banana", "mango"],
foodCount: 1,
foodEaten: [500, 20],
createdAt: new Timestamp(1628939119, 0),
house: {
street: {
name: 'Jungle Street'
}
}
},
{
id: "elephant",
name: "elephant",
type: "mammal",
legCount: 4,
food: ["banana", "peanut"],
foodCount: 0,
foodEaten: [0, 500],
createdAt: new Timestamp(1628939129, 0),
},
{
id: "chicken",
name: "chicken",
type: "bird",
legCount: 2,
food: ["leaf", "nut", "ant"],
foodCount: 4,
foodEaten: [80, 20, 16],
createdAt: new Timestamp(1628939139, 0),
_collections: {
foodSchedule: [
{
id: "nut",
interval: "whenever",
},
{
id: "leaf",
interval: "hourly",
},
],
},
},
{
id: "ant",
name: "ant",
type: "insect",
legCount: 6,
food: ["leaf", "bread"],
foodCount: 2,
foodEaten: [80, 12],
createdAt: new Timestamp(1628939149, 0),
_collections: {
foodSchedule: [
{
id: "leaf",
interval: "daily",
},
{
id: "peanut",
interval: "weekly",
},
],
},
},
{
id: "worm",
name: "worm",
legCount: null,
},
{
id: "pogo-stick",
name: "pogo-stick",
food: false,
},
],
foodSchedule: [
{ id: "ants", interval: "daily" },
{ id: "cows", interval: "twice daily" },
],
nested: [
{
id: "collections",
_collections: {
have: [
{
id: "lots",
_collections: {
of: [
{
id: "applications",
_collections: {
foodSchedule: [
{
id: "layer4_a",
interval: "daily",
},
{
id: "layer4_b",
interval: "weekly",
},
],
},
},
],
},
},
],
},
},
],
},
currentUser: { uid: "homer-user" } as FirebaseUser,
},
{ simulateQueryFilters: true }
);
let firebase;
let db;
beforeAll(async () => {
firebase = await import("firebase");
firebase.initializeApp({
apiKey: "### FIREBASE API KEY ###",
authDomain: "### FIREBASE AUTH DOMAIN ###",
projectId: "### CLOUD FIRESTORE PROJECT ID ###",
});
db = firebase.firestore();
});
test("it can query a single document", async () => {
const monkey = await db.collection("animals").doc("monkey").get();
expect(monkey).toHaveProperty("exists", true);
expect(mockCollection).toHaveBeenCalledWith("animals");
expect(mockDoc).toHaveBeenCalledWith("monkey");
expect(mockGet).toHaveBeenCalled();
});
test("it can query null values", async () => {
const noLegs = await db
.collection("animals")
.where("legCount", "==", null)
.get();
expect(noLegs).toHaveProperty("size", 1);
const worm = noLegs.docs[0];
expect(worm).toBeDefined();
expect(worm).toHaveProperty("id", "worm");
});
test("it can query by document id", async () => {
const monkeys = await db.collection("animals").where(FieldPath.documentId(), "==", "monkey").get();
expect(monkeys).toHaveProperty("size", 1);
const monkey = monkeys.docs[0];
expect(monkey).toBeDefined();
expect(monkey).toHaveProperty("id", "monkey")
});
test("it can query false values", async () => {
const noFood = await db
.collection("animals")
.where("food", "==", false)
.get();
expect(noFood).toHaveProperty("size", 1);
const pogoStick = noFood.docs[0];
expect(pogoStick).toBeDefined();
expect(pogoStick).toHaveProperty("id", "pogo-stick");
});
test('it can query nested values', async () => {
const street = await db
.collection("animals")
.where("house.street.name", "==", "Jungle Street")
.get();
expect(street).toHaveProperty("size", 1);
const monkey = street.docs[0];
expect(monkey).toBeDefined();
expect(monkey).toHaveProperty("id", "monkey");
expect(monkey.data()).toHaveProperty("house.street.name", "Jungle Street");
})
test("it can query date values for equality", async () => {
const elephant = await db
.collection("animals")
.where("createdAt", "==", new Date(1628939129 * 1000))
.get();
expect(elephant).toHaveProperty("size", 1);
expect(elephant.docs[0].id).toEqual("elephant");
});
test("it can query date values for greater than condition", async () => {
const res = await db
.collection("animals")
.where("createdAt", ">", new Date(1628939129 * 1000))
.get();
expect(res).toHaveProperty("size", 2);
expect(res.docs[0].id).toEqual("chicken");
expect(res.docs[1].id).toEqual("ant");
});
test("it can query multiple documents", async () => {
expect.assertions(9);
const animals = await db
.collection("animals")
.where("type", "==", "mammal")
.get();
expect(animals).toHaveProperty("docs", expect.any(Array));
expect(mockCollection).toHaveBeenCalledWith("animals");
// Make sure that the filter behaves appropriately
expect(animals.docs.length).toBe(2);
// Make sure that forEach works properly
expect(animals).toHaveProperty("forEach", expect.any(Function));
animals.forEach((doc) => {
// this should run 4 times, as asserted by `expect.assertions` above
expect(doc).toHaveProperty("exists", true);
});
expect(mockWhere).toHaveBeenCalledWith("type", "==", "mammal");
expect(mockGet).toHaveBeenCalled();
expect(animals).toHaveProperty("size", 2); // Returns 2 of 4 documents
});
test("it can filter firestore equality queries in subcollections", async () => {
const antSchedule = await db
.collection("animals")
.doc("ant")
.collection("foodSchedule")
.where("interval", "==", "daily")
.get();
expect(mockCollection).toHaveBeenCalledWith("animals");
expect(mockCollection).toHaveBeenCalledWith("foodSchedule");
expect(mockWhere).toHaveBeenCalledWith("interval", "==", "daily");
expect(mockGet).toHaveBeenCalled();
expect(antSchedule).toHaveProperty("docs", expect.any(Array));
expect(antSchedule).toHaveProperty("size", 1); // Returns 1 of 2 documents
});
test("in a transaction, it can filter firestore equality queries in subcollections", async () => {
mockGet.mockReset();
const antSchedule = db
.collection("animals")
.doc("ant")
.collection("foodSchedule")
.where("interval", "==", "daily");
expect.assertions(6);
await db.runTransaction(async (transaction) => {
const scheduleItems = await transaction.get(antSchedule);
expect(mockCollection).toHaveBeenCalledWith("animals");
expect(mockCollection).toHaveBeenCalledWith("foodSchedule");
expect(mockWhere).toHaveBeenCalledWith("interval", "==", "daily");
expect(mockGet).not.toHaveBeenCalled();
expect(scheduleItems).toHaveProperty("docs", expect.any(Array));
expect(scheduleItems).toHaveProperty("size", 1); // Returns 1 of 2 documents
});
});
test("it can filter firestore comparison queries in subcollections", async () => {
const chickenSchedule = db
.collection("animals")
.doc("chicken")
.collection("foodSchedule")
.where("interval", "<=", "hourly"); // should have 1 result
const scheduleItems = await chickenSchedule.get();
expect(scheduleItems).toHaveProperty("docs", expect.any(Array));
expect(scheduleItems).toHaveProperty("size", 1); // Returns 1 document
expect(scheduleItems.docs[0]).toHaveProperty(
"ref",
expect.any(DocumentReference)
);
expect(scheduleItems.docs[0]).toHaveProperty("id", "leaf");
expect(scheduleItems.docs[0].data()).toHaveProperty("interval", "hourly");
expect(scheduleItems.docs[0].ref).toHaveProperty(
"path",
"animals/chicken/foodSchedule/leaf"
);
});
test("in a transaction, it can filter firestore comparison queries in subcollections", async () => {
const chickenSchedule = db
.collection("animals")
.doc("chicken")
.collection("foodSchedule")
.where("interval", "<=", "hourly"); // should have 1 result
expect.assertions(6);
await db.runTransaction(async (transaction) => {
const scheduleItems = await transaction.get(chickenSchedule);
expect(scheduleItems).toHaveProperty("docs", expect.any(Array));
expect(scheduleItems).toHaveProperty("size", 1); // Returns 1 document
expect(scheduleItems.docs[0]).toHaveProperty(
"ref",
expect.any(DocumentReference)
);
expect(scheduleItems.docs[0]).toHaveProperty("id", "leaf");
expect(scheduleItems.docs[0].data()).toHaveProperty("interval", "hourly");
expect(scheduleItems.docs[0].ref).toHaveProperty(
"path",
"animals/chicken/foodSchedule/leaf"
);
});
});
test("it can query collection groups", async () => {
const allSchedules = await db.collectionGroup("foodSchedule").get();
expect(allSchedules).toHaveProperty("size", 8); // Returns all 8
const paths = allSchedules.docs.map((doc) => doc.ref.path).sort();
const expectedPaths = [
"nested/collections/have/lots/of/applications/foodSchedule/layer4_a",
"nested/collections/have/lots/of/applications/foodSchedule/layer4_b",
"animals/ant/foodSchedule/leaf",
"animals/ant/foodSchedule/peanut",
"animals/chicken/foodSchedule/leaf",
"animals/chicken/foodSchedule/nut",
"foodSchedule/ants",
"foodSchedule/cows",
].sort();
expect(paths).toStrictEqual(expectedPaths);
});
test("it returns the same instance from query methods", () => {
const ref = db.collection("animals");
const notThisRef = db.collection("elsewise");
expect(ref.where("type", "==", "mammal")).toBe(ref);
expect(ref.where("type", "==", "mammal")).not.toBe(notThisRef);
expect(ref.limit(1)).toBe(ref);
expect(ref.limit(1)).not.toBe(notThisRef);
expect(ref.orderBy("type")).toBe(ref);
expect(ref.orderBy("type")).not.toBe(notThisRef);
expect(ref.startAfter(null)).toBe(ref);
expect(ref.startAfter(null)).not.toBe(notThisRef);
expect(ref.startAt(null)).toBe(ref);
expect(ref.startAt(null)).not.toBe(notThisRef);
});
test("it returns a Query from query methods", () => {
const ref = db.collection("animals");
expect(ref.where("type", "==", "mammal")).toBeInstanceOf(Query);
expect(ref.limit(1)).toBeInstanceOf(Query);
expect(ref.orderBy("type")).toBeInstanceOf(Query);
expect(ref.startAfter(null)).toBeInstanceOf(Query);
expect(ref.startAt(null)).toBeInstanceOf(Query);
});
test("it throws an error when comparing to null", () => {
expect(() =>
db.collection("animals").where("legCount", ">", null)
).toThrow();
expect(() =>
db.collection("animals").where("legCount", ">=", null)
).toThrow();
expect(() =>
db.collection("animals").where("legCount", "<", null)
).toThrow();
expect(() =>
db.collection("animals").where("legCount", "<=", null)
).toThrow();
expect(() =>
db.collection("animals").where("legCount", "array-contains", null)
).toThrow();
expect(() =>
db.collection("animals").where("legCount", "array-contains-any", null)
).toThrow();
expect(() =>
db.collection("animals").where("legCount", "in", null)
).toThrow();
expect(() =>
db.collection("animals").where("legCount", "not-in", null)
).toThrow();
});
test("it allows equality comparisons with null", () => {
expect(() =>
db.collection("animals").where("legCount", "==", null)
).not.toThrow();
expect(() =>
db.collection("animals").where("legCount", "!=", null)
).not.toThrow();
});
test("it permits mocking the results of a where clause", async () => {
expect.assertions(2);
const ref = db.collection("animals");
let result = await ref.where("type", "==", "mammal").get();
expect(result.docs.length).toBe(2);
// There's got to be a better way to mock like this, but at least it works.
mockWhere.mockReturnValueOnce({
async get() {
return await Promise.resolve({
docs: [
{ id: "monkey", name: "monkey", type: "mammal" },
{ id: "elephant", name: "elephant", type: "mammal" },
],
});
},
});
result = await ref.where("type", "==", "mammal").get();
expect(result.docs.length).toBe(2);
});
test("it can offset query", async () => {
const firstTwoMammals = await db
.collection("animals")
.where("type", "==", "mammal")
.offset(2)
.get();
expect(firstTwoMammals).toHaveProperty("docs", expect.any(Array));
expect(mockWhere).toHaveBeenCalledWith("type", "==", "mammal");
expect(mockCollection).toHaveBeenCalledWith("animals");
expect(mockGet).toHaveBeenCalled();
expect(mockOffset).toHaveBeenCalledWith(2);
});
describe("Query Operations", () => {
test.each`
comp | value | count
${"=="} | ${2} | ${2}
${"=="} | ${4} | ${1}
${"=="} | ${6} | ${1}
${"=="} | ${7} | ${0}
${"!="} | ${7} | ${5}
${"!="} | ${4} | ${4}
${">"} | ${1000} | ${0}
${">"} | ${1} | ${4}
${">"} | ${6} | ${0}
${">="} | ${1000} | ${0}
${">="} | ${6} | ${1}
${">="} | ${0} | ${4}
${"<"} | ${-10000} | ${0}
${"<"} | ${10000} | ${4}
${"<"} | ${2} | ${0}
${"<"} | ${6} | ${3}
${"<="} | ${-10000} | ${0}
${"<="} | ${10000} | ${4}
${"<="} | ${2} | ${2}
${"<="} | ${6} | ${4}
${"in"} | ${[6, 2]} | ${3}
${"not-in"} | ${[6, 2]} | ${2}
${"not-in"} | ${[4]} | ${4}
${"not-in"} | ${[7]} | ${5}
`(
// eslint-disable-next-line quotes
"it performs '$comp' queries on number values ($count doc(s) where legCount $comp $value)",
async ({ comp, value, count }) => {
const results = await db
.collection("animals")
.where("legCount", comp, value)
.get();
expect(results.size).toBe(count);
}
);
test.each`
comp | value | count
${"=="} | ${0} | ${1}
${"=="} | ${1} | ${1}
${"=="} | ${2} | ${1}
${"=="} | ${4} | ${1}
${"=="} | ${6} | ${0}
${">"} | ${-1} | ${4}
${">"} | ${0} | ${3}
${">"} | ${1} | ${2}
${">"} | ${4} | ${0}
${">="} | ${6} | ${0}
${">="} | ${4} | ${1}
${">="} | ${0} | ${4}
${"<"} | ${2} | ${2}
${"<"} | ${6} | ${4}
${"<="} | ${2} | ${3}
${"<="} | ${6} | ${4}
${"in"} | ${[2, 0]} | ${2}
${"not-in"} | ${[2, 0]} | ${2}
`(
// eslint-disable-next-line quotes
"it performs '$comp' queries on number values that may be zero ($count doc(s) where foodCount $comp $value)",
async ({ comp, value, count }) => {
const results = await db
.collection("animals")
.where("foodCount", comp, value)
.get();
expect(results.size).toBe(count);
}
);
test.each`
comp | value | count
${"=="} | ${"mammal"} | ${2}
${"=="} | ${"bird"} | ${1}
${"=="} | ${"fish"} | ${0}
${"!="} | ${"bird"} | ${3}
${"!="} | ${"fish"} | ${4}
${">"} | ${"insect"} | ${2}
${">"} | ${"z"} | ${0}
${">="} | ${"mammal"} | ${2}
${">="} | ${"insect"} | ${3}
${"<"} | ${"bird"} | ${0}
${"<"} | ${"mammal"} | ${2}
${"<="} | ${"mammal"} | ${4}
${"<="} | ${"bird"} | ${1}
${"<="} | ${"a"} | ${0}
${"in"} | ${["a", "bird", "mammal"]} | ${3}
${"not-in"} | ${["a", "bird", "mammal"]} | ${1}
`(
// eslint-disable-next-line quotes
"it performs '$comp' queries on string values ($count doc(s) where type $comp '$value')",
async ({ comp, value, count }) => {
const results = await db
.collection("animals")
.where("type", comp, value)
.get();
expect(results.size).toBe(count);
}
);
test.each`
comp | value | count
${"=="} | ${["banana", "mango"]} | ${1}
${"=="} | ${["mango", "banana"]} | ${0}
${"=="} | ${["banana", "peanut"]} | ${1}
${"!="} | ${["banana", "peanut"]} | ${4}
${"array-contains"} | ${"banana"} | ${2}
${"array-contains"} | ${"leaf"} | ${2}
${"array-contains"} | ${"bread"} | ${1}
${"array-contains-any"} | ${["banana", "mango", "peanut"]} | ${2}
`(
// eslint-disable-next-line quotes
"it performs '$comp' queries on array values ($count doc(s) where food $comp '$value')",
async ({ comp, value, count }) => {
const results = await db
.collection("animals")
.where("food", comp, value)
.get();
expect(results.size).toBe(count);
}
);
test.each`
comp | value | count
${"=="} | ${[500, 20]} | ${1}
${"=="} | ${[20, 500]} | ${0}
${"=="} | ${[0, 500]} | ${1}
${"!="} | ${[20, 500]} | ${4}
${"array-contains"} | ${500} | ${2}
${"array-contains"} | ${80} | ${2}
${"array-contains"} | ${12} | ${1}
${"array-contains"} | ${0} | ${1}
${"array-contains-any"} | ${[0, 11, 500]} | ${2}
`(
// eslint-disable-next-line quotes
"it performs '$comp' queries on array values that may be zero ($count doc(s) where foodEaten $comp '$value')",
async ({ comp, value, count }) => {
const results = await db
.collection("animals")
.where("foodEaten", comp, value)
.get();
expect(results.size).toBe(count);
}
);
});
});