firestore-vitest-mock
Version:
Vitest helper for mocking Google Cloud Firestore
375 lines (339 loc) • 13.8 kB
text/typescript
import { beforeEach, describe, expect, vi, test } from "vitest";
import {
CollectionReference,
DocumentReference,
FakeFirestore,
mockCollection,
mockDoc,
Timestamp,
} from "../mocks/firestore";
describe("Queries", () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
});
const db = (simulateQueryFilters = false): FakeFirestore =>
new FakeFirestore(
{
characters: [
{
id: "homer",
name: "Homer",
occupation: "technician",
address: { street: "742 Evergreen Terrace" },
// May 12, 1956. Conveniently, a negative number
birthdate: {
seconds: -430444800,
nanoseconds: 0,
},
// Test a pre-constructed Timestamp
timestamp: new Timestamp(123, 456),
},
{ id: "krusty", name: "Krusty", occupation: "clown" },
{
id: "bob",
name: "Bob",
occupation: "insurance agent",
_collections: {
family: [
{ id: "violet", name: "Violet", relation: "daughter" },
{ id: "dash", name: "Dash", relation: "son" },
{ id: "jackjack", name: "Jackjack", relation: "son" },
{ id: "helen", name: "Helen", relation: "wife" },
],
},
},
],
checkEmpty: [
{
id: "emptyDocument",
_collections: {
validChildren: {
family: [
{ id: "1", name: "One" },
{ id: "2", name: "Two" },
{ id: "3", name: "Three" },
],
},
},
},
],
},
{ simulateQueryFilters }
);
describe("Single records versus queries", () => {
test("it can fetch a single record", async () => {
expect.assertions(6);
const record = await db().collection("characters").doc("krusty").get();
expect(mockCollection).toHaveBeenCalledWith("characters");
expect(mockDoc).toHaveBeenCalledWith("krusty");
expect(record.exists).toBe(true);
expect(record.id).toBe("krusty");
const data = record.data();
expect(data).toHaveProperty("name", "Krusty");
expect(data).toHaveProperty("occupation", "clown");
});
test("it flags records do not exist", async () => {
expect.assertions(4);
const record = await db().collection("animals").doc("monkey").get();
expect(mockCollection).toHaveBeenCalledWith("animals");
expect(mockDoc).toHaveBeenCalledWith("monkey");
expect(record.id).toBe("monkey");
expect(record.exists).toBe(false);
});
test("it can fetch a single record with a promise", async () => {
await db()
.collection("characters")
.doc("homer")
.get()
.then((record) => {
expect(record.exists).toBe(true);
expect(record.id).toBe("homer");
expect(mockCollection).toHaveBeenCalledWith("characters");
const data = record.data();
expect(record).toHaveProperty("exists", true);
expect(data).toBeDefined();
expect(data).toHaveProperty("name", "Homer");
expect(data).toHaveProperty("occupation", "technician");
expect(record.get("name")).toEqual("Homer");
expect(record.get("address.street")).toEqual("742 Evergreen Terrace");
expect(record.get("address.street.doesntExist")).toBeNull();
});
});
test("it can fetch a single record with a promise without a specified collection", async () => {
await db()
.doc("characters/homer")
.get()
.then((record) => {
expect(record.exists).toBe(true);
expect(record.id).toBe("homer");
expect(mockCollection).not.toHaveBeenCalled();
const data = record.data();
expect(record).toHaveProperty("exists", true);
expect(data).toBeDefined();
expect(data).toHaveProperty("name", "Homer");
expect(data).toHaveProperty("occupation", "technician");
});
});
test("it can fetch multiple records and returns documents", async () => {
const records = await db()
.collection("characters")
.where("name", "==", "Homer")
.get();
expect(records.empty).toBe(false);
expect(records).toHaveProperty("docs", expect.any(Array));
const doc = records.docs[0];
expect(doc).toHaveProperty("id", "homer");
expect(doc).toHaveProperty("exists", true);
const data = doc.data();
expect(data).toBeDefined();
expect(data).toHaveProperty("name", "Homer");
});
test("it throws an error if the collection path ends at a document", () => {
expect(() => db().collection("")).toThrow(Error);
expect(db().collection("foo")).toBeInstanceOf(CollectionReference);
expect(() => db().collection("foo/bar")).toThrow(Error);
expect(db().collection("foo/bar/baz")).toBeInstanceOf(
CollectionReference
);
});
test("it throws an error if the document path ends at a collection", () => {
expect(() => db().doc("")).toThrow(Error);
expect(() => db().doc("characters")).toThrow(Error);
expect(db().doc("characters/bob")).toBeInstanceOf(DocumentReference);
expect(() => db().doc("characters/bob/family")).toThrow(Error);
});
test("it can fetch nonexistent documents from a root collection", async () => {
const nope = await db().doc("characters/joe").get();
expect(nope).toHaveProperty("exists", false);
expect(nope).toHaveProperty("id", "joe");
expect(nope).toHaveProperty("ref");
expect(nope.ref).toHaveProperty("path", "characters/joe");
});
test("it can fetch nonexistent documents from extant subcollections", async () => {
const nope = await db().doc("characters/bob/family/thing3").get();
expect(nope).toHaveProperty("exists", false);
expect(nope).toHaveProperty("id", "thing3");
expect(nope).toHaveProperty("ref");
expect(nope.ref).toHaveProperty("path", "characters/bob/family/thing3");
});
test("it can fetch nonexistent documents from nonexistent subcollections", async () => {
const nope = await db().doc("characters/sam/family/phil").get();
expect(nope).toHaveProperty("exists", false);
expect(nope).toHaveProperty("id", "phil");
expect(nope).toHaveProperty("ref");
expect(nope.ref).toHaveProperty("path", "characters/sam/family/phil");
});
test("it can fetch nonexistent documents from nonexistent root collections", async () => {
const nope = await db().doc("foo/bar/baz/bin").get();
expect(nope).toHaveProperty("exists", false);
expect(nope).toHaveProperty("id", "bin");
expect(nope).toHaveProperty("ref");
expect(nope.ref).toHaveProperty("path", "foo/bar/baz/bin");
});
test("it flags when a collection is empty", async () => {
expect.assertions(1);
const records = await db()
.collection("animals")
.where("type", "==", "mammal")
.get();
expect(records).toHaveProperty("empty", true);
});
test.each`
simulateQueryFilters | expectedSize
${true} | ${1}
${false} | ${3}
`(
"it can fetch multiple records as a promise",
async ({ simulateQueryFilters, expectedSize }) => {
await db(simulateQueryFilters)
.collection("characters")
.where("name", "==", "Homer")
.get()
.then((records) => {
expect(records).toHaveProperty("empty", false);
expect(records).toHaveProperty("docs", expect.any(Array));
expect(records).toHaveProperty("size", expectedSize);
expect(records.docs[0]).toHaveProperty("id", "homer");
expect(records.docs[0]).toHaveProperty("exists", true);
expect(records.docs[0].data()).toHaveProperty("name", "Homer");
});
}
);
test("it can return all root records", async () => {
expect.assertions(4);
const firstRecord = db().collection("characters").doc("homer");
const secondRecord = db().collection("characters").doc("krusty");
const records = await db().getAll(firstRecord, secondRecord);
expect(records.length).toBe(2);
expect(records[0]).toHaveProperty("id", "homer");
expect(records[0]).toHaveProperty("exists", true);
expect(records[0].data()).toHaveProperty("name", "Homer");
});
test("it does not fetch subcollections unless we tell it to", async () => {
expect.assertions(4);
const record = await db().collection("characters").doc("bob").get();
expect(record.exists).toBe(true);
expect(record.id).toBe("bob");
expect(record.data()).toHaveProperty("name", "Bob");
expect(record.data()).not.toHaveProperty("_collections");
});
test("it can fetch records from subcollections", async () => {
expect.assertions(8);
const family = db()
.collection("characters")
.doc("bob")
.collection("family");
expect(family.path).toBe("characters/bob/family");
const allFamilyMembers = await family.get();
expect(allFamilyMembers.docs.length).toBe(4);
expect(allFamilyMembers.forEach).toBeTruthy();
const ref = family.doc("violet");
expect(ref).toHaveProperty("path", "characters/bob/family/violet");
const record = await ref.get();
expect(record).toHaveProperty("exists", true);
expect(record).toHaveProperty("id", "violet");
expect(record).toHaveProperty("data");
expect(record.data()).toHaveProperty("name", "Violet");
});
test.each`
simulateQueryFilters | expectedSize
${true} | ${2}
${false} | ${4}
`(
"it can fetch records from subcollections with query parameters",
async ({ simulateQueryFilters, expectedSize }) => {
const family = db(simulateQueryFilters)
.collection("characters")
.doc("bob")
.collection("family")
.where("relation", "==", "son"); // should return only sons
expect(family).toHaveProperty("path", "characters/bob/family");
const docs = await family.get();
expect(docs).toHaveProperty("size", expectedSize);
}
);
});
describe("Multiple records versus queries", () => {
test("it fetches all records from a root collection", async () => {
expect.assertions(4);
const characters = await db().collection("characters").get();
expect(characters).toHaveProperty("empty", false);
expect(characters).toHaveProperty("size", 3);
expect(Array.isArray(characters.docs)).toBe(true);
expect(characters.forEach).toBeTruthy();
});
test("it fetches no records from nonexistent collection", async () => {
expect.assertions(4);
const nope = await db().collection("foo").get();
expect(nope).toHaveProperty("empty", true);
expect(nope).toHaveProperty("size", 0);
expect(Array.isArray(nope.docs)).toBe(true);
expect(nope.forEach).toBeTruthy();
});
test("it fetches all records from subcollection", async () => {
expect.assertions(4);
const familyRef = db()
.collection("characters")
.doc("bob")
.collection("family");
const family = await familyRef.get();
expect(family).toHaveProperty("empty", false);
expect(family).toHaveProperty("size", 4);
expect(Array.isArray(family.docs)).toBe(true);
expect(family.forEach).toBeTruthy();
});
test("it fetches no records from nonexistent subcollection", async () => {
expect.assertions(4);
const nope = await db()
.collection("characters")
.doc("bob")
.collection("not-here")
.get();
expect(nope).toHaveProperty("empty", true);
expect(nope).toHaveProperty("size", 0);
expect(Array.isArray(nope.docs)).toBe(true);
expect(nope.forEach).toBeTruthy();
});
test("it fetches no records from nonexistent root collection", async () => {
expect.assertions(4);
const nope = await db()
.collection("foo")
.doc("bar")
.collection("baz")
.get();
expect(nope).toHaveProperty("empty", true);
expect(nope).toHaveProperty("size", 0);
expect(Array.isArray(nope.docs)).toBe(true);
expect(nope.forEach).toBeTruthy();
});
});
test("it returns all results from listDocuments", async () => {
const [emptyDoc] = await db().collection("checkEmpty").listDocuments();
expect(emptyDoc).toBeDefined();
const data = await emptyDoc.get();
expect(data.exists).toBeTruthy();
// Contains no data
expect(Object.keys(data.data())).toHaveLength(0);
});
test("New documents with random ID", async () => {
expect.assertions(2);
// See https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference#doc
// "If no path is specified, an automatically-generated unique ID will be used for the returned DocumentReference."
const col = db().collection("foo");
const newDoc = col.doc();
const otherIds = col._records().map((doc) => doc.id);
expect(otherIds).not.toContainEqual(newDoc.id);
expect(newDoc).toHaveProperty("path", `foo/${newDoc.id}`);
});
test("it properly converts timestamps", async () => {
await db()
.doc("characters/homer")
.get()
.then((record) => {
expect(record.id).toEqual("homer");
const data = record.data();
expect(typeof data.birthdate.toDate).toEqual("function");
});
});
});