UNPKG

firestore-vitest

Version:

Vitest helper for mocking Google Cloud Firestore

516 lines (474 loc) 19.1 kB
import { mockCollection, mockDoc, mockGet, mockWhere, mockOffset, FakeFirestore, } from '../mocks/firestore.js'; import { mockFirebase } from '../index.js'; describe('Queries', () => { mockFirebase( { database: { animals: [ { id: 'monkey', name: 'monkey', type: 'mammal', legCount: 2, food: ['banana', 'mango'], foodCount: 1, foodEaten: [500, 20], createdAt: new FakeFirestore.Timestamp(1628939119, 0), }, { id: 'elephant', name: 'elephant', type: 'mammal', legCount: 4, food: ['banana', 'peanut'], foodCount: 0, foodEaten: [0, 500], createdAt: new FakeFirestore.Timestamp(1628939129, 0), }, { id: 'chicken', name: 'chicken', type: 'bird', legCount: 2, food: ['leaf', 'nut', 'ant'], foodCount: 4, foodEaten: [80, 20, 16], createdAt: new FakeFirestore.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 FakeFirestore.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' }, }, { simulateQueryFilters: true }, ); let db; beforeAll(async () => { const 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 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 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).toBe('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).toBe('chicken'); expect(res.docs[1].id).toBe('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(FakeFirestore.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(FakeFirestore.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(FakeFirestore.Query); expect(ref.limit(1)).toBeInstanceOf(FakeFirestore.Query); expect(ref.orderBy('type')).toBeInstanceOf(FakeFirestore.Query); expect(ref.startAfter(null)).toBeInstanceOf(FakeFirestore.Query); expect(ref.startAt(null)).toBeInstanceOf(FakeFirestore.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({ get() { return 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); }, ); }); });