Files
Josh SorefandGitHub b4e60981a5 Spelling (#8280)
Numerous fixes to spelling across the SDK.
2024-07-18 16:53:00 -04:00

1715 lines
56 KiB
TypeScript

/**
* @license
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as firestore from '@firebase/firestore-types';
import { Deferred } from '@firebase/util';
import { expect, use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { EventsAccumulator } from './util/events_accumulator';
import * as firebaseExport from './util/firebase_export';
import {
apiDescribe,
withTestCollection,
withTestDbsSettings,
withTestDb,
withTestDbs,
withTestDoc,
withTestDocAndInitialData
} from './util/helpers';
import { DEFAULT_SETTINGS, DEFAULT_PROJECT_ID } from './util/settings';
use(chaiAsPromised);
const newTestFirestore = firebaseExport.newTestFirestore;
const Timestamp = firebaseExport.Timestamp;
const FieldPath = firebaseExport.FieldPath;
const FieldValue = firebaseExport.FieldValue;
const DocumentReference = firebaseExport.DocumentReference;
const QueryDocumentSnapshot = firebaseExport.QueryDocumentSnapshot;
apiDescribe('Database', (persistence: boolean) => {
it('can set a document', () => {
return withTestDoc(persistence, docRef => {
return docRef.set({
desc: 'Stuff related to Firestore project...',
owner: {
name: 'Jonny',
title: 'scallywag'
}
});
});
});
it('doc() will auto generate an ID', () => {
return withTestDb(persistence, async db => {
const ref = db.collection('foo').doc();
// Auto IDs are 20 characters long
expect(ref.id.length).to.equal(20);
});
});
it('can delete a document', () => {
// TODO(#1865): This test fails with node:persistence against Prod
return withTestDoc(persistence, docRef => {
return docRef
.set({ foo: 'bar' })
.then(() => {
return docRef.get();
})
.then(doc => {
expect(doc.data()).to.deep.equal({ foo: 'bar' });
return docRef.delete();
})
.then(() => {
return docRef.get();
})
.then(doc => {
expect(doc.exists).to.equal(false);
});
});
});
it('can update existing document', () => {
return withTestDoc(persistence, doc => {
const initialData = {
desc: 'Description',
owner: { name: 'Jonny', email: '[email protected]' }
};
const updateData = {
desc: 'NewDescription',
'owner.email': '[email protected]'
};
const finalData = {
desc: 'NewDescription',
owner: { name: 'Jonny', email: '[email protected]' }
};
return doc
.set(initialData)
.then(() => doc.update(updateData))
.then(() => doc.get())
.then(docSnapshot => {
expect(docSnapshot.exists).to.be.ok;
expect(docSnapshot.data()).to.deep.equal(finalData);
});
});
});
it('can retrieve document that does not exist', () => {
return withTestDoc(persistence, doc => {
return doc.get().then(snapshot => {
expect(snapshot.exists).to.equal(false);
expect(snapshot.data()).to.equal(undefined);
expect(snapshot.get('foo')).to.equal(undefined);
});
});
});
// eslint-disable-next-line no-restricted-properties
(persistence ? it : it.skip)('can update an unknown document', () => {
return withTestDbs(persistence, 2, async ([reader, writer]) => {
const writerRef = writer.collection('collection').doc();
const readerRef = reader.collection('collection').doc(writerRef.id);
await writerRef.set({ a: 'a' });
await readerRef.update({ b: 'b' });
await writerRef
.get({ source: 'cache' })
.then(doc => expect(doc.exists).to.be.true);
await readerRef.get({ source: 'cache' }).then(
() => {
expect.fail('Expected cache miss');
},
err => expect(err.code).to.be.equal('unavailable')
);
await writerRef
.get()
.then(doc => expect(doc.data()).to.deep.equal({ a: 'a', b: 'b' }));
await readerRef
.get()
.then(doc => expect(doc.data()).to.deep.equal({ a: 'a', b: 'b' }));
});
});
it('can merge data with an existing document using set', () => {
return withTestDoc(persistence, doc => {
const initialData = {
desc: 'description',
'owner.data': { name: 'Jonny', email: '[email protected]' }
};
const mergeData = {
updated: true,
'owner.data': { name: 'Sebastian' }
};
const finalData = {
updated: true,
desc: 'description',
'owner.data': { name: 'Sebastian', email: '[email protected]' }
};
return doc
.set(initialData)
.then(() => doc.set(mergeData, { merge: true }))
.then(() => doc.get())
.then(docSnapshot => {
expect(docSnapshot.exists).to.be.ok;
expect(docSnapshot.data()).to.deep.equal(finalData);
});
});
});
it('can merge server timestamps', () => {
return withTestDoc(persistence, doc => {
const initialData = {
updated: false
};
const mergeData = {
time: FieldValue.serverTimestamp(),
nested: { time: FieldValue.serverTimestamp() }
};
return doc
.set(initialData)
.then(() => doc.set(mergeData, { merge: true }))
.then(() => doc.get())
.then(docSnapshot => {
expect(docSnapshot.exists).to.be.ok;
expect(docSnapshot.get('updated')).to.be.false;
expect(docSnapshot.get('time')).to.be.an.instanceof(Timestamp);
expect(docSnapshot.get('nested.time')).to.be.an.instanceof(Timestamp);
});
});
});
it('can merge empty object', async () => {
await withTestDoc(persistence, async doc => {
const accumulator = new EventsAccumulator<firestore.DocumentSnapshot>();
const unsubscribe = doc.onSnapshot(accumulator.storeEvent);
await accumulator
.awaitEvent()
.then(() => doc.set({}))
.then(() => accumulator.awaitEvent())
.then(docSnapshot => expect(docSnapshot.data()).to.be.deep.equal({}))
.then(() => doc.set({ a: {} }, { mergeFields: ['a'] }))
.then(() => accumulator.awaitEvent())
.then(docSnapshot =>
expect(docSnapshot.data()).to.be.deep.equal({ a: {} })
)
.then(() => doc.set({ b: {} }, { merge: true }))
.then(() => accumulator.awaitEvent())
.then(docSnapshot =>
expect(docSnapshot.data()).to.be.deep.equal({ a: {}, b: {} })
)
.then(() => doc.get({ source: 'server' }))
.then(docSnapshot => {
expect(docSnapshot.data()).to.be.deep.equal({ a: {}, b: {} });
});
unsubscribe();
});
});
it('update with empty object replaces all fields', () => {
return withTestDoc(persistence, async doc => {
await doc.set({ a: 'a' });
await doc.update('a', {});
const docSnapshot = await doc.get();
expect(docSnapshot.data()).to.be.deep.equal({ a: {} });
});
});
it('merge with empty object replaces all fields', () => {
return withTestDoc(persistence, async doc => {
await doc.set({ a: 'a' });
await doc.set({ 'a': {} }, { merge: true });
const docSnapshot = await doc.get();
expect(docSnapshot.data()).to.be.deep.equal({ a: {} });
});
});
it('can delete field using merge', () => {
return withTestDoc(persistence, doc => {
const initialData = {
untouched: true,
foo: 'bar',
nested: { untouched: true, foo: 'bar' }
};
const mergeData = {
foo: FieldValue.delete(),
nested: { foo: FieldValue.delete() }
};
const finalData = {
untouched: true,
nested: { untouched: true }
};
return doc
.set(initialData)
.then(() => doc.set(mergeData, { merge: true }))
.then(() => doc.get())
.then(docSnapshot => {
expect(docSnapshot.exists).to.be.ok;
expect(docSnapshot.data()).to.deep.equal(finalData);
});
});
});
it('can delete field using mergeFields', () => {
return withTestDoc(persistence, doc => {
const initialData = {
untouched: true,
foo: 'bar',
inner: { removed: true, foo: 'bar' },
nested: { untouched: true, foo: 'bar' }
};
const mergeData = {
foo: FieldValue.delete(),
inner: { foo: FieldValue.delete() },
nested: {
untouched: FieldValue.delete(),
foo: FieldValue.delete()
}
};
const finalData = {
untouched: true,
inner: {},
nested: { untouched: true }
};
return doc
.set(initialData)
.then(() =>
doc.set(mergeData, { mergeFields: ['foo', 'inner', 'nested.foo'] })
)
.then(() => doc.get())
.then(docSnapshot => {
expect(docSnapshot.exists).to.be.ok;
expect(docSnapshot.data()).to.deep.equal(finalData);
});
});
});
it('can set server timestamps using mergeFields', () => {
return withTestDoc(persistence, doc => {
const initialData = {
untouched: true,
foo: 'bar',
nested: { untouched: true, foo: 'bar' }
};
const mergeData = {
foo: FieldValue.serverTimestamp(),
inner: { foo: FieldValue.serverTimestamp() },
nested: { foo: FieldValue.serverTimestamp() }
};
return doc
.set(initialData)
.then(() =>
doc.set(mergeData, { mergeFields: ['foo', 'inner', 'nested.foo'] })
)
.then(() => doc.get())
.then(docSnapshot => {
expect(docSnapshot.exists).to.be.ok;
expect(docSnapshot.get('foo')).to.be.instanceof(Timestamp);
expect(docSnapshot.get('inner.foo')).to.be.instanceof(Timestamp);
expect(docSnapshot.get('nested.foo')).to.be.instanceof(Timestamp);
});
});
});
it('can replace an array by merging using set', () => {
return withTestDoc(persistence, doc => {
const initialData = {
untouched: true,
data: 'old',
topLevel: ['old', 'old'],
mapInArray: [{ data: 'old' }]
};
const mergeData = {
data: 'new',
topLevel: ['new'],
mapInArray: [{ data: 'new' }]
};
const finalData = {
untouched: true,
data: 'new',
topLevel: ['new'],
mapInArray: [{ data: 'new' }]
};
return doc
.set(initialData)
.then(() => doc.set(mergeData, { merge: true }))
.then(() => doc.get())
.then(docSnapshot => {
expect(docSnapshot.exists).to.be.ok;
expect(docSnapshot.data()).to.deep.equal(finalData);
});
});
});
it("can't specify a field mask for a missing field using set", () => {
return withTestDoc(persistence, async docRef => {
expect(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
docRef.set(
{ desc: 'NewDescription' },
{ mergeFields: ['desc', 'owner'] }
);
}).to.throw(
"Field 'owner' is specified in your field mask but missing from your input data."
);
});
});
it('can set a subset of fields using a field mask', () => {
const initialData = {
desc: 'Description',
owner: { name: 'Jonny', email: '[email protected]' }
};
const finalData = { desc: 'Description', owner: 'Sebastian' };
return withTestDocAndInitialData(persistence, initialData, async docRef => {
await docRef.set(
{ desc: 'NewDescription', owner: 'Sebastian' },
{ mergeFields: ['owner'] }
);
const result = await docRef.get();
expect(result.data()).to.deep.equal(finalData);
});
});
it("doesn't apply field delete outside of mask", () => {
const initialData = {
desc: 'Description',
owner: { name: 'Jonny', email: '[email protected]' }
};
const finalData = { desc: 'Description', owner: 'Sebastian' };
return withTestDocAndInitialData(persistence, initialData, async docRef => {
await docRef.set(
{ desc: FieldValue.delete(), owner: 'Sebastian' },
{ mergeFields: ['owner'] }
);
const result = await docRef.get();
expect(result.data()).to.deep.equal(finalData);
});
});
it("doesn't apply field transform outside of mask", () => {
const initialData = {
desc: 'Description',
owner: { name: 'Jonny', email: '[email protected]' }
};
const finalData = { desc: 'Description', owner: 'Sebastian' };
return withTestDocAndInitialData(persistence, initialData, async docRef => {
await docRef.set(
{
desc: FieldValue.serverTimestamp(),
owner: 'Sebastian'
},
{ mergeFields: ['owner'] }
);
const result = await docRef.get();
expect(result.data()).to.deep.equal(finalData);
});
});
it('can set an empty field mask', () => {
const initialData = {
desc: 'Description',
owner: { name: 'Jonny', email: '[email protected]' }
};
const finalData = initialData;
return withTestDocAndInitialData(persistence, initialData, async docRef => {
await docRef.set(
{ desc: 'NewDescription', owner: 'Sebastian' },
{ mergeFields: [] }
);
const result = await docRef.get();
expect(result.data()).to.deep.equal(finalData);
});
});
it('can specify fields multiple times in a field mask', () => {
const initialData = {
desc: 'Description',
owner: { name: 'Jonny', email: '[email protected]' }
};
const finalData = {
desc: 'Description',
owner: { name: 'Sebastian', email: '[email protected]' }
};
return withTestDocAndInitialData(persistence, initialData, async docRef => {
await docRef.set(
{
desc: 'NewDescription',
owner: { name: 'Sebastian', email: '[email protected]' }
},
{ mergeFields: ['owner.name', 'owner', 'owner'] }
);
const result = await docRef.get();
expect(result.data()).to.deep.equal(finalData);
});
});
it('cannot update nonexistent document', () => {
return withTestDoc(persistence, doc => {
return doc
.update({ owner: 'abc' })
.then(
() => Promise.reject('update should have failed.'),
err => {
expect(err.message).to.exist;
expect(err.code).to.equal('not-found');
}
)
.then(() => doc.get())
.then(docSnapshot => {
expect(docSnapshot.exists).to.equal(false);
});
});
});
it('can delete a field with an update', () => {
return withTestDoc(persistence, doc => {
const initialData = {
desc: 'Description',
owner: { name: 'Jonny', email: '[email protected]' }
};
const updateData = {
'owner.email': FieldValue.delete()
};
const finalData = {
desc: 'Description',
owner: { name: 'Jonny' }
};
return doc
.set(initialData)
.then(() => doc.update(updateData))
.then(() => doc.get())
.then(docSnapshot => {
expect(docSnapshot.exists).to.be.ok;
expect(docSnapshot.data()).to.deep.equal(finalData);
});
});
});
it('can update nested fields', () => {
return withTestDoc(persistence, doc => {
const initialData = {
desc: 'Description',
owner: { name: 'Jonny' },
'is.admin': false
};
const finalData = {
desc: 'Description',
owner: { name: 'Sebastian' },
'is.admin': true
};
return doc
.set(initialData)
.then(() =>
doc.update('owner.name', 'Sebastian', new FieldPath('is.admin'), true)
)
.then(() => doc.get())
.then(docSnapshot => {
expect(docSnapshot.exists).to.be.ok;
expect(docSnapshot.data()).to.deep.equal(finalData);
});
});
});
it('can specify updated field multiple times', () => {
return withTestDoc(persistence, doc => {
return doc
.set({})
.then(() => doc.update('field', 100, new FieldPath('field'), 200))
.then(() => doc.get())
.then(docSnap => {
expect(docSnap.data()).to.deep.equal({ field: 200 });
});
});
});
describe('documents: ', () => {
const invalidDocValues = [undefined, null, 0, 'foo', ['a'], new Date()];
for (const val of invalidDocValues) {
it('set/update should reject: ' + val, () => {
return withTestDoc(persistence, async doc => {
// Intentionally passing bad types.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => doc.set(val as any)).to.throw();
// Intentionally passing bad types.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => doc.update(val as any)).to.throw();
});
});
}
});
it('CollectionRef.add() resolves with resulting DocumentRef.', () => {
return withTestCollection(persistence, {}, coll => {
return coll
.add({ foo: 1 })
.then(docRef => docRef.get())
.then(docSnap => {
expect(docSnap.data()).to.deep.equal({ foo: 1 });
});
});
});
it('onSnapshotsInSync fires after listeners are in sync', () => {
const testDocs = {
a: { foo: 1 }
};
return withTestCollection(persistence, testDocs, async coll => {
let events: string[] = [];
const gotInitialSnapshot = new Deferred<void>();
const doc = coll.doc('a');
doc.onSnapshot(snap => {
events.push('doc');
gotInitialSnapshot.resolve();
});
await gotInitialSnapshot.promise;
events = [];
const done = new Deferred<void>();
doc.firestore.onSnapshotsInSync(() => {
events.push('snapshots-in-sync');
if (events.length === 3) {
// We should have an initial snapshots-in-sync event, then a snapshot
// event for set(), then another event to indicate we're in sync
// again.
expect(events).to.deep.equal([
'snapshots-in-sync',
'doc',
'snapshots-in-sync'
]);
done.resolve();
}
});
await doc.set({ foo: 3 });
await done.promise;
});
});
apiDescribe('Queries are validated client-side', (persistence: boolean) => {
// NOTE: Failure cases are validated in validation_test.ts
it('same inequality fields works', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() =>
coll.where('x', '>=', 32).where('x', '<=', 'cat')
).not.to.throw();
});
});
it('inequality and equality on different fields works', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() =>
coll.where('x', '>=', 32).where('y', '==', 'cat')
).not.to.throw();
});
});
it('inequality and array-contains on different fields works', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() =>
coll.where('x', '>=', 32).where('y', 'array-contains', 'cat')
).not.to.throw();
});
});
it('inequality and IN on different fields works', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() =>
coll.where('x', '>=', 32).where('y', 'in', [1, 2])
).not.to.throw();
});
});
it('inequality and array-contains-any on different fields works', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() =>
coll.where('x', '>=', 32).where('y', 'array-contains-any', [1, 2])
).not.to.throw();
});
});
it('inequality same as orderBy works.', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() => coll.where('x', '>', 32).orderBy('x')).not.to.throw();
expect(() => coll.orderBy('x').where('x', '>', 32)).not.to.throw();
});
});
it('!= same as orderBy works.', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() => coll.where('x', '!=', 32).orderBy('x')).not.to.throw();
expect(() => coll.orderBy('x').where('x', '!=', 32)).not.to.throw();
});
});
it('inequality same as first orderBy works.', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() =>
coll.where('x', '>', 32).orderBy('x').orderBy('y')
).not.to.throw();
expect(() =>
coll.orderBy('x').where('x', '>', 32).orderBy('y')
).not.to.throw();
});
});
it('!= same as first orderBy works.', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() =>
coll.where('x', '!=', 32).orderBy('x').orderBy('y')
).not.to.throw();
expect(() =>
coll.orderBy('x').where('x', '!=', 32).orderBy('y')
).not.to.throw();
});
});
it('equality different than orderBy works', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() => coll.orderBy('x').where('y', '==', 'cat')).not.to.throw();
});
});
it('array-contains different than orderBy works', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() =>
coll.orderBy('x').where('y', 'array-contains', 'cat')
).not.to.throw();
});
});
it('IN different than orderBy works', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() => coll.orderBy('x').where('y', 'in', [1, 2])).not.to.throw();
});
});
it('array-contains-any different than orderBy works', () => {
return withTestCollection(persistence, {}, async coll => {
expect(() =>
coll.orderBy('x').where('y', 'array-contains-any', [1, 2])
).not.to.throw();
});
});
});
it('DocumentSnapshot events for nonexistent document', () => {
return withTestCollection(persistence, {}, col => {
const doc = col.doc();
const storeEvent = new EventsAccumulator<firestore.DocumentSnapshot>();
doc.onSnapshot(storeEvent.storeEvent);
return storeEvent.awaitEvent().then(snap => {
expect(snap.exists).to.be.false;
expect(snap.data()).to.equal(undefined);
return storeEvent.assertNoAdditionalEvents();
});
});
});
it('DocumentSnapshot events for add data to document', () => {
return withTestCollection(persistence, {}, col => {
const doc = col.doc();
const storeEvent = new EventsAccumulator<firestore.DocumentSnapshot>();
doc.onSnapshot({ includeMetadataChanges: true }, storeEvent.storeEvent);
return storeEvent
.awaitEvent()
.then(snap => {
expect(snap.exists).to.be.false;
expect(snap.data()).to.equal(undefined);
})
.then(() => doc.set({ a: 1 }))
.then(() => storeEvent.awaitEvent())
.then(snap => {
expect(snap.exists).to.be.true;
expect(snap.data()).to.deep.equal({ a: 1 });
expect(snap.metadata.hasPendingWrites).to.be.true;
})
.then(() => storeEvent.awaitEvent())
.then(snap => {
expect(snap.exists).to.be.true;
expect(snap.data()).to.deep.equal({ a: 1 });
expect(snap.metadata.hasPendingWrites).to.be.false;
})
.then(() => storeEvent.assertNoAdditionalEvents());
});
});
it('DocumentSnapshot events for change data in document', () => {
const initialData = { a: 1 };
const changedData = { b: 2 };
return withTestCollection(persistence, { key1: initialData }, col => {
const doc = col.doc('key1');
const storeEvent = new EventsAccumulator<firestore.DocumentSnapshot>();
doc.onSnapshot({ includeMetadataChanges: true }, storeEvent.storeEvent);
return storeEvent
.awaitEvent()
.then(snap => {
expect(snap.data()).to.deep.equal(initialData);
expect(snap.metadata.hasPendingWrites).to.be.false;
})
.then(() => doc.set(changedData))
.then(() => storeEvent.awaitEvent())
.then(snap => {
expect(snap.data()).to.deep.equal(changedData);
expect(snap.metadata.hasPendingWrites).to.be.true;
})
.then(() => storeEvent.awaitEvent())
.then(snap => {
expect(snap.data()).to.deep.equal(changedData);
expect(snap.metadata.hasPendingWrites).to.be.false;
})
.then(() => storeEvent.assertNoAdditionalEvents());
});
});
it('DocumentSnapshot events for delete data in document', () => {
const initialData = { a: 1 };
return withTestCollection(persistence, { key1: initialData }, col => {
const doc = col.doc('key1');
const storeEvent = new EventsAccumulator<firestore.DocumentSnapshot>();
doc.onSnapshot({ includeMetadataChanges: true }, storeEvent.storeEvent);
return storeEvent
.awaitEvent()
.then(snap => {
expect(snap.exists).to.be.true;
expect(snap.data()).to.deep.equal(initialData);
expect(snap.metadata.hasPendingWrites).to.be.false;
})
.then(() => doc.delete())
.then(() => storeEvent.awaitEvent())
.then(snap => {
expect(snap.exists).to.be.false;
expect(snap.data()).to.equal(undefined);
expect(snap.metadata.hasPendingWrites).to.be.false;
})
.then(() => storeEvent.assertNoAdditionalEvents());
});
});
it('Listen can be called multiple times', () => {
return withTestCollection(persistence, {}, coll => {
const doc = coll.doc();
const deferred1 = new Deferred<void>();
const deferred2 = new Deferred<void>();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
doc.set({ foo: 'bar' }).then(() => {
doc.onSnapshot(snap => {
deferred1.resolve();
doc.onSnapshot(snap => {
deferred2.resolve();
});
});
});
return Promise.all([deferred1.promise, deferred2.promise]).then(() => {});
});
});
it('Metadata only changes are not fired when no options provided', () => {
return withTestDoc(persistence, docRef => {
const secondUpdateFound = new Deferred();
let count = 0;
const unlisten = docRef.onSnapshot(doc => {
if (doc) {
count++;
if (count === 1) {
expect(doc.data()).to.deep.equal({ a: 1 });
} else {
expect(doc.data()).to.deep.equal({ b: 1 });
secondUpdateFound.resolve();
}
}
});
// eslint-disable-next-line @typescript-eslint/no-floating-promises
docRef.set({ a: 1 }).then(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
docRef.set({ b: 1 });
});
return secondUpdateFound.promise.then(() => {
unlisten();
});
});
});
// TODO(mikelehen): We need a way to create a query that will pass
// client-side validation but fail remotely. May need to wait until we
// have security rules support or something?
// eslint-disable-next-line no-restricted-properties
describe('Listens are rejected remotely:', () => {
it('will reject listens', () => {
return withTestDb(persistence, async db => {
const deferred = new Deferred();
const queryForRejection = db.collection('a/__badpath__/b');
queryForRejection.onSnapshot(
() => {},
(err: Error) => {
expect(err.name).to.exist;
expect(err.message).to.exist;
deferred.resolve();
}
);
await deferred.promise;
});
});
it('will reject same listens twice in a row', () => {
return withTestDb(persistence, async db => {
const deferred = new Deferred();
const queryForRejection = db.collection('a/__badpath__/b');
queryForRejection.onSnapshot(
() => {},
(err: Error) => {
expect(err.name).to.exist;
expect(err.message).to.exist;
queryForRejection.onSnapshot(
() => {},
(err2: Error) => {
expect(err2.name).to.exist;
expect(err2.message).to.exist;
deferred.resolve();
}
);
}
);
await deferred.promise;
});
});
it('will reject gets', () => {
return withTestDb(persistence, async db => {
const queryForRejection = db.collection('a/__badpath__/b');
await queryForRejection.get().then(
() => {
expect.fail('Promise resolved even though error was expected.');
},
err => {
expect(err.name).to.exist;
expect(err.message).to.exist;
}
);
});
});
it('will reject gets twice in a row', () => {
return withTestDb(persistence, async db => {
const queryForRejection = db.collection('a/__badpath__/b');
return queryForRejection
.get()
.then(
() => {
expect.fail('Promise resolved even though error was expected.');
},
err => {
expect(err.name).to.exist;
expect(err.message).to.exist;
}
)
.then(() => queryForRejection.get())
.then(
() => {
expect.fail('Promise resolved even though error was expected.');
},
err => {
expect(err.name).to.exist;
expect(err.message).to.exist;
}
);
});
});
});
it('exposes "firestore" on document references.', () => {
return withTestDb(persistence, async db => {
expect(db.doc('foo/bar').firestore).to.equal(db);
});
});
it('exposes "firestore" on query references.', () => {
return withTestDb(persistence, async db => {
expect(db.collection('foo').limit(5).firestore).to.equal(db);
});
});
it('can compare DocumentReference instances with isEqual().', () => {
return withTestDb(persistence, firestore => {
return withTestDb(persistence, async otherFirestore => {
const docRef = firestore.doc('foo/bar');
expect(docRef.isEqual(firestore.doc('foo/bar'))).to.be.true;
expect(docRef.collection('baz').parent!.isEqual(docRef)).to.be.true;
expect(firestore.doc('foo/BAR').isEqual(docRef)).to.be.false;
expect(otherFirestore.doc('foo/bar').isEqual(docRef)).to.be.false;
});
});
});
it('can compare Query instances with isEqual().', () => {
return withTestDb(persistence, firestore => {
return withTestDb(persistence, async otherFirestore => {
const query = firestore
.collection('foo')
.orderBy('bar')
.where('baz', '==', 42);
const query2 = firestore
.collection('foo')
.orderBy('bar')
.where('baz', '==', 42);
expect(query.isEqual(query2)).to.be.true;
const query3 = firestore
.collection('foo')
.orderBy('BAR')
.where('baz', '==', 42);
expect(query.isEqual(query3)).to.be.false;
const query4 = otherFirestore
.collection('foo')
.orderBy('bar')
.where('baz', '==', 42);
expect(query4.isEqual(query)).to.be.false;
});
});
});
it('can traverse collections and documents.', () => {
return withTestDb(persistence, async db => {
const expected = 'a/b/c/d';
// doc path from root Firestore.
expect(db.doc('a/b/c/d').path).to.deep.equal(expected);
// collection path from root Firestore.
expect(db.collection('a/b/c').doc('d').path).to.deep.equal(expected);
// doc path from CollectionReference.
expect(db.collection('a').doc('b/c/d').path).to.deep.equal(expected);
// collection path from DocumentReference.
expect(db.doc('a/b').collection('c/d/e').path).to.deep.equal(
expected + '/e'
);
});
});
it('can traverse collection and document parents.', () => {
return withTestDb(persistence, async db => {
let collection = db.collection('a/b/c');
expect(collection.path).to.deep.equal('a/b/c');
const doc = collection.parent!;
expect(doc.path).to.deep.equal('a/b');
collection = doc.parent;
expect(collection.path).to.equal('a');
const nullDoc = collection.parent;
expect(nullDoc).to.equal(null);
});
});
it('can queue writes while offline', () => {
return withTestDoc(persistence, docRef => {
const firestore = docRef.firestore;
return firestore
.disableNetwork()
.then(() => {
return Promise.all([
docRef.set({ foo: 'bar' }),
firestore.enableNetwork()
]);
})
.then(() => docRef.get())
.then(doc => {
expect(doc.data()).to.deep.equal({ foo: 'bar' });
});
});
});
// eslint-disable-next-line no-restricted-properties
(persistence ? it : it.skip)('offline writes are sent after restart', () => {
return withTestDoc(persistence, async docRef => {
const firestore = docRef.firestore;
const app = firestore.app;
const name = app.name;
const options = app.options;
await firestore.disableNetwork();
// We are merely adding to the cache.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
docRef.set({ foo: 'bar' });
await app.delete();
const firestore2 = newTestFirestore(
options.projectId,
name,
DEFAULT_SETTINGS
);
await firestore2.enablePersistence();
await firestore2.waitForPendingWrites();
const doc = await firestore2.doc(docRef.path).get();
expect(doc.exists).to.be.true;
expect(doc.metadata.hasPendingWrites).to.be.false;
});
});
it('rejects subsequent method calls after terminate() is called', async () => {
return withTestDb(persistence, db => {
return db.INTERNAL.delete().then(() => {
expect(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
db.disableNetwork();
}).to.throw('The client has already been terminated.');
});
});
});
it('can call terminate() multiple times', async () => {
return withTestDb(persistence, async db => {
await db.terminate();
await db.terminate();
});
});
// eslint-disable-next-line no-restricted-properties
(persistence ? it : it.skip)(
'maintains persistence after restarting app',
async () => {
await withTestDoc(persistence, async docRef => {
await docRef.set({ foo: 'bar' });
const app = docRef.firestore.app;
const name = app.name;
const options = app.options;
await app.delete();
const firestore2 = newTestFirestore(options.projectId, name);
await firestore2.enablePersistence();
const docRef2 = firestore2.doc(docRef.path);
const docSnap2 = await docRef2.get({ source: 'cache' });
expect(docSnap2.exists).to.be.true;
});
}
);
// eslint-disable-next-line no-restricted-properties
(persistence ? it : it.skip)(
'can clear persistence if the client has been terminated',
async () => {
await withTestDoc(persistence, async docRef => {
const firestore = docRef.firestore;
await docRef.set({ foo: 'bar' });
const app = docRef.firestore.app;
const name = app.name;
const options = app.options;
await app.delete();
await firestore.clearPersistence();
const firestore2 = newTestFirestore(options.projectId, name);
await firestore2.enablePersistence();
const docRef2 = firestore2.doc(docRef.path);
await expect(
docRef2.get({ source: 'cache' })
).to.eventually.be.rejectedWith('Failed to get document from cache.');
});
}
);
// eslint-disable-next-line no-restricted-properties
(persistence ? it : it.skip)(
'can clear persistence if the client has not been initialized',
async () => {
await withTestDoc(persistence, async docRef => {
await docRef.set({ foo: 'bar' });
const app = docRef.firestore.app;
const name = app.name;
const options = app.options;
await app.delete();
const firestore2 = newTestFirestore(options.projectId, name);
await firestore2.clearPersistence();
await firestore2.enablePersistence();
const docRef2 = firestore2.doc(docRef.path);
await expect(
docRef2.get({ source: 'cache' })
).to.eventually.be.rejectedWith('Failed to get document from cache.');
});
}
);
// eslint-disable-next-line no-restricted-properties
(persistence ? it : it.skip)(
'cannot clear persistence if the client has been initialized',
async () => {
await withTestDoc(persistence, async docRef => {
const firestore = docRef.firestore;
const expectedError =
'Persistence can only be cleared before a Firestore instance is ' +
'initialized or after it is terminated.';
expect(() => firestore.clearPersistence()).to.throw(expectedError);
});
}
);
it('can get documents while offline', async () => {
await withTestDoc(persistence, async docRef => {
const firestore = docRef.firestore;
await firestore.disableNetwork();
await expect(docRef.get()).to.eventually.be.rejectedWith(
'Failed to get document because the client is offline.'
);
const writePromise = docRef.set({ foo: 'bar' });
const doc = await docRef.get();
expect(doc.metadata.fromCache).to.be.true;
await firestore.enableNetwork();
await writePromise;
const doc2 = await docRef.get();
expect(doc2.metadata.fromCache).to.be.false;
expect(doc2.data()).to.deep.equal({ foo: 'bar' });
});
});
it('can enable and disable networking', () => {
return withTestDb(persistence, async db => {
// There's not currently a way to check if networking is in fact disabled,
// so for now just test that the method is well-behaved and doesn't throw.
await db.enableNetwork();
await db.enableNetwork();
await db.disableNetwork();
await db.disableNetwork();
await db.enableNetwork();
});
});
it('can start a new instance after shut down', async () => {
return withTestDoc(persistence, async docRef => {
const firestore = docRef.firestore;
await firestore.terminate();
const newFirestore = newTestFirestore(
firestore.app.options.projectId,
firestore.app
);
expect(newFirestore).to.not.equal(firestore);
// New instance functions.
newFirestore.settings(DEFAULT_SETTINGS);
await newFirestore.doc(docRef.path).set({ foo: 'bar' });
const doc = await newFirestore.doc(docRef.path).get();
expect(doc.data()).to.deep.equal({ foo: 'bar' });
});
});
it('new operation after termination should throw', async () => {
await withTestDoc(persistence, async docRef => {
const firestore = docRef.firestore;
await firestore.terminate();
expect(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
firestore.doc(docRef.path).set({ foo: 'bar' });
}).to.throw('The client has already been terminated.');
});
});
it('calling terminate multiple times should proceed', async () => {
await withTestDoc(persistence, async docRef => {
const firestore = docRef.firestore;
await firestore.terminate();
await firestore.terminate();
expect(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
firestore.doc(docRef.path).set({ foo: 'bar' });
}).to.throw();
});
});
it('can unlisten queries after termination', async () => {
return withTestDoc(persistence, async docRef => {
const firestore = docRef.firestore;
const accumulator = new EventsAccumulator<firestore.DocumentSnapshot>();
const unsubscribe = docRef.onSnapshot(accumulator.storeEvent);
await accumulator.awaitEvent();
await firestore.terminate();
// This should proceed without error.
unsubscribe();
// Multiple calls should proceed as well.
unsubscribe();
});
});
it('can wait for pending writes', async () => {
await withTestDoc(persistence, async docRef => {
const firestore = docRef.firestore;
// Prevent pending writes receiving acknowledgement.
await firestore.disableNetwork();
const pendingWrites = docRef.set({ foo: 'bar' });
const awaitPendingWrites = firestore.waitForPendingWrites();
// pending writes can receive acknowledgements now.
await firestore.enableNetwork();
await pendingWrites;
await awaitPendingWrites;
});
});
it('waiting for pending writes resolves immediately when offline and no pending writes', async () => {
await withTestDoc(persistence, async docRef => {
const firestore = docRef.firestore;
// Prevent pending writes receiving acknowledgement.
await firestore.disableNetwork();
// `awaitsPendingWrites` is created when there is no pending writes, it will resolve
// immediately even if we are offline.
await firestore.waitForPendingWrites();
});
});
// PORTING NOTE: These tests are for FirestoreDataConverter support and apply
// only to web.
apiDescribe('withConverter() support', (persistence: boolean) => {
class Post {
constructor(
readonly title: string,
readonly author: string,
readonly ref: firestore.DocumentReference | null = null
) {}
byline(): string {
return this.title + ', by ' + this.author;
}
}
const postConverter = {
toFirestore(post: Post): firestore.DocumentData {
return { title: post.title, author: post.author };
},
fromFirestore(
snapshot: firestore.QueryDocumentSnapshot,
options: firestore.SnapshotOptions
): Post {
expect(snapshot).to.be.an.instanceof(QueryDocumentSnapshot);
const data = snapshot.data(options);
return new Post(data.title, data.author, snapshot.ref);
}
};
const postConverterMerge = {
toFirestore(
post: Partial<Post>,
options?: firestore.SetOptions
): firestore.DocumentData {
if (options && (options.merge || options.mergeFields)) {
expect(post).to.not.be.an.instanceof(Post);
} else {
expect(post).to.be.an.instanceof(Post);
}
const result: firestore.DocumentData = {};
if (post.title) {
result.title = post.title;
}
if (post.author) {
result.author = post.author;
}
return result;
},
fromFirestore(
snapshot: firestore.QueryDocumentSnapshot,
options: firestore.SnapshotOptions
): Post {
const data = snapshot.data();
return new Post(data.title, data.author, snapshot.ref);
}
};
it('for DocumentReference.withConverter()', () => {
return withTestDb(persistence, async db => {
const docRef = db
.collection('posts')
.doc()
.withConverter(postConverter);
await docRef.set(new Post('post', 'author'));
const postData = await docRef.get();
const post = postData.data();
expect(post).to.not.equal(undefined);
expect(post!.byline()).to.equal('post, by author');
});
});
it('for DocumentReference.withConverter(null) ', () => {
return withTestDb(persistence, async db => {
const docRef = db
.collection('posts')
.doc()
.withConverter(postConverter)
.withConverter(null);
expect(() => docRef.set(new Post('post', 'author'))).to.throw();
});
});
it('for CollectionReference.withConverter()', () => {
return withTestDb(persistence, async db => {
const coll = db.collection('posts').withConverter(postConverter);
const docRef = await coll.add(new Post('post', 'author'));
const postData = await docRef.get();
const post = postData.data();
expect(post).to.not.equal(undefined);
expect(post!.byline()).to.equal('post, by author');
});
});
it('for CollectionReference.withConverter(null)', () => {
return withTestDb(persistence, async db => {
const coll = db
.collection('posts')
.withConverter(postConverter)
.withConverter(null);
expect(() => coll.add(new Post('post', 'author'))).to.throw();
});
});
it('for Query.withConverter()', () => {
return withTestDb(persistence, async db => {
await db
.doc('postings/post1')
.set({ title: 'post1', author: 'author1' });
await db
.doc('postings/post2')
.set({ title: 'post2', author: 'author2' });
const posts = await db
.collectionGroup('postings')
.withConverter(postConverter)
.get();
expect(posts.size).to.equal(2);
expect(posts.docs[0].data()!.byline()).to.equal('post1, by author1');
});
});
it('for Query.withConverter(null)', () => {
return withTestDb(persistence, async db => {
await db
.doc('postings/post1')
.set({ title: 'post1', author: 'author1' });
const posts = await db
.collectionGroup('postings')
.withConverter(postConverter)
.withConverter(null)
.get();
expect(posts.docs[0].data()).to.not.be.an.instanceof(Post);
});
});
it('requires the correct converter for Partial usage', async () => {
return withTestDb(persistence, async db => {
const ref = db
.collection('posts')
.doc('some-post')
.withConverter(postConverter);
await ref.set(new Post('walnut', 'author'));
const batch = db.batch();
expect(() =>
batch.set(ref, { title: 'olive' }, { merge: true })
).to.throw(
'Function WriteBatch.set() called with invalid ' +
'data (via `toFirestore()`). Unsupported field value: undefined ' +
'(found in field author in document posts/some-post)'
);
});
});
it('WriteBatch.set() supports partials with merge', async () => {
return withTestDb(persistence, async db => {
const ref = db
.collection('posts')
.doc()
.withConverter(postConverterMerge);
await ref.set(new Post('walnut', 'author'));
const batch = db.batch();
batch.set(ref, { title: 'olive' }, { merge: true });
await batch.commit();
const doc = await ref.get();
expect(doc.get('title')).to.equal('olive');
expect(doc.get('author')).to.equal('author');
});
});
it('WriteBatch.set() supports partials with mergeFields', async () => {
return withTestDb(persistence, async db => {
const ref = db
.collection('posts')
.doc()
.withConverter(postConverterMerge);
await ref.set(new Post('walnut', 'author'));
const batch = db.batch();
batch.set(
ref,
{ title: 'olive', author: 'writer' },
{ mergeFields: ['title'] }
);
await batch.commit();
const doc = await ref.get();
expect(doc.get('title')).to.equal('olive');
expect(doc.get('author')).to.equal('author');
});
});
it('Transaction.set() supports partials with merge', async () => {
return withTestDb(persistence, async db => {
const ref = db
.collection('posts')
.doc()
.withConverter(postConverterMerge);
await ref.set(new Post('walnut', 'author'));
await db.runTransaction(async tx => {
tx.set(ref, { title: 'olive' }, { merge: true });
});
const doc = await ref.get();
expect(doc.get('title')).to.equal('olive');
expect(doc.get('author')).to.equal('author');
});
});
it('Transaction.set() supports partials with mergeFields', async () => {
return withTestDb(persistence, async db => {
const ref = db
.collection('posts')
.doc()
.withConverter(postConverterMerge);
await ref.set(new Post('walnut', 'author'));
await db.runTransaction(async tx => {
tx.set(
ref,
{ title: 'olive', author: 'person' },
{ mergeFields: ['title'] }
);
});
const doc = await ref.get();
expect(doc.get('title')).to.equal('olive');
expect(doc.get('author')).to.equal('author');
});
});
it('DocumentReference.set() supports partials with merge', async () => {
return withTestDb(persistence, async db => {
const ref = db
.collection('posts')
.doc()
.withConverter(postConverterMerge);
await ref.set(new Post('walnut', 'author'));
await ref.set({ title: 'olive' }, { merge: true });
const doc = await ref.get();
expect(doc.get('title')).to.equal('olive');
expect(doc.get('author')).to.equal('author');
});
});
it('DocumentReference.set() supports partials with mergeFields', async () => {
return withTestDb(persistence, async db => {
const ref = db
.collection('posts')
.doc()
.withConverter(postConverterMerge);
await ref.set(new Post('walnut', 'author'));
await ref.set(
{ title: 'olive', author: 'writer' },
{ mergeFields: ['title'] }
);
const doc = await ref.get();
expect(doc.get('title')).to.equal('olive');
expect(doc.get('author')).to.equal('author');
});
});
it('calls DocumentSnapshot.data() with specified SnapshotOptions', () => {
return withTestDb(persistence, async db => {
const docRef = db.doc('some/doc').withConverter({
toFirestore(post: Post): firestore.DocumentData {
return { title: post.title, author: post.author };
},
fromFirestore(
snapshot: firestore.QueryDocumentSnapshot,
options: firestore.SnapshotOptions
): Post {
// Check that options were passed in properly.
expect(options).to.deep.equal({ serverTimestamps: 'estimate' });
const data = snapshot.data(options);
return new Post(data.title, data.author, snapshot.ref);
}
});
await docRef.set(new Post('post', 'author'));
const postData = await docRef.get();
postData.data({ serverTimestamps: 'estimate' });
});
});
it('drops the converter when calling CollectionReference<T>.parent()', () => {
return withTestDb(persistence, async db => {
const postsCollection = db
.collection('users/user1/posts')
.withConverter(postConverter);
const usersCollection = postsCollection.parent;
expect(usersCollection!.isEqual(db.doc('users/user1'))).to.be.true;
});
});
it('checks converter when comparing with isEqual()', () => {
return withTestDb(persistence, async db => {
const postConverter2 = { ...postConverter };
const postsCollection = db
.collection('users/user1/posts')
.withConverter(postConverter);
const postsCollection2 = db
.collection('users/user1/posts')
.withConverter(postConverter2);
expect(postsCollection.isEqual(postsCollection2)).to.be.false;
const docRef = db.doc('some/doc').withConverter(postConverter);
const docRef2 = db.doc('some/doc').withConverter(postConverter2);
expect(docRef.isEqual(docRef2)).to.be.false;
});
});
it('Correct snapshot specified to fromFirestore() when registered with DocumentReference', () => {
return withTestDb(persistence, async db => {
const untypedDocRef = db.collection('/models').doc();
const docRef = untypedDocRef.withConverter(postConverter);
await docRef.set(new Post('post', 'author'));
const docSnapshot = await docRef.get();
const ref = docSnapshot.data()!.ref!;
expect(ref).to.be.an.instanceof(DocumentReference);
expect(untypedDocRef.isEqual(ref)).to.be.true;
});
});
it('Correct snapshot specified to fromFirestore() when registered with CollectionReference', () => {
return withTestDb(persistence, async db => {
const untypedCollection = db
.collection('/models')
.doc()
.collection('sub');
const collection = untypedCollection.withConverter(postConverter);
const docRef = collection.doc();
await docRef.set(new Post('post', 'author', docRef));
const querySnapshot = await collection.get();
expect(querySnapshot.size).to.equal(1);
const ref = querySnapshot.docs[0].data().ref!;
expect(ref).to.be.an.instanceof(DocumentReference);
const untypedDocRef = untypedCollection.doc(docRef.id);
expect(untypedDocRef.isEqual(ref)).to.be.true;
});
});
it('Correct snapshot specified to fromFirestore() when registered with Query', () => {
return withTestDb(persistence, async db => {
const untypedCollection = db.collection('/models');
const untypedDocRef = untypedCollection.doc();
const docRef = untypedDocRef.withConverter(postConverter);
await docRef.set(new Post('post', 'author', docRef));
const query = untypedCollection
.where(FieldPath.documentId(), '==', docRef.id)
.withConverter(postConverter);
const querySnapshot = await query.get();
expect(querySnapshot.size).to.equal(1);
const ref = querySnapshot.docs[0].data().ref!;
expect(ref).to.be.an.instanceof(DocumentReference);
expect(untypedDocRef.isEqual(ref)).to.be.true;
});
});
it('for DocumentReference.withConverter() that returns undefined', () => {
return withTestDb(persistence, async db => {
const docRef = db
.collection('posts')
.doc()
.withConverter({
toFirestore(post: Post): firestore.DocumentData {
return { title: post.title, author: post.author };
},
fromFirestore(
snapshot: firestore.QueryDocumentSnapshot,
options: firestore.SnapshotOptions
): Post | undefined {
return undefined;
}
});
await docRef.set(new Post('post', 'author'));
const postData = await docRef.get();
const post = postData.data();
expect(post).to.equal(undefined);
});
});
});
// TODO(b/196858864): This test regularly times out on CI.
// eslint-disable-next-line no-restricted-properties
it.skip('can set and get data with auto detect long polling enabled', () => {
const settings = {
...DEFAULT_SETTINGS,
experimentalAutoDetectLongPolling: true
};
return withTestDbsSettings(
persistence,
DEFAULT_PROJECT_ID,
settings,
1,
async ([db]) => {
const data = { name: 'Rafi', email: '[email protected]' };
const doc = await db.collection('users').doc();
return doc
.set(data)
.then(() => doc.get())
.then(snapshot => {
expect(snapshot.exists).to.be.ok;
expect(snapshot.data()).to.deep.equal(data);
});
}
);
});
it('app delete leads to instance termination', async () => {
await withTestDoc(persistence, async docRef => {
await docRef.set({ foo: 'bar' });
const app = docRef.firestore.app;
await app.delete();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((docRef.firestore as any)._delegate._terminated).to.be.true;
});
});
});