Files
wu-huiandGitHub 4703c99e12 merge off pipeline into main (#9929)
* chore(ci): make all firestore integration tests optional

* fix: align expressions and pipelines, address review comments, update yarn.lock, and fix package versions

* chore: update yarn.lock after merge

* fix: restore firestore package version and devDependencies to 4.15.0
2026-06-05 10:28:06 -04:00

471 lines
14 KiB
TypeScript

/**
* @license
* Copyright 2017 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 { newQueryForPath, Query } from '../../../src/core/query';
import { DocumentKey } from '../../../src/model/document_key';
import { JsonObject } from '../../../src/model/object_value';
import { LimitType } from '../../../src/protos/firestore_bundle_proto';
import { toVersion } from '../../../src/remote/serializer';
import {
doc,
filter,
query,
TestSnapshotVersion,
version,
wrapObject
} from '../../util/helpers';
import {
JSON_SERIALIZER,
TEST_DATABASE_ID
} from '../local/persistence_test_helpers';
import { TestBundleBuilder } from '../util/bundle_data';
import { describeSpec, specTest } from './describe_spec';
import { client, spec } from './spec_builder';
interface TestBundleDocument {
key: DocumentKey;
readTime: TestSnapshotVersion;
createTime?: TestSnapshotVersion;
updateTime?: TestSnapshotVersion;
content?: JsonObject<unknown>;
}
interface TestBundledQuery {
name: string;
readTime: TestSnapshotVersion;
query: Query;
limitType?: LimitType;
}
export function bundleWithDocumentAndQuery(
testDoc: TestBundleDocument,
testQuery?: TestBundledQuery
): string {
const builder = new TestBundleBuilder(TEST_DATABASE_ID);
if (testQuery) {
builder.addNamedQuery(
testQuery.name,
toVersion(JSON_SERIALIZER, version(testQuery.readTime)),
testQuery.query
);
}
builder.addDocumentMetadata(
testDoc.key,
toVersion(JSON_SERIALIZER, version(testDoc.readTime)),
!!testDoc.createTime
);
if (testDoc.createTime) {
builder.addDocument(
testDoc.key,
toVersion(JSON_SERIALIZER, version(testDoc.createTime)),
toVersion(JSON_SERIALIZER, version(testDoc.updateTime!)),
wrapObject(testDoc.content!).value.mapValue.fields!
);
}
return builder.build(
'test-bundle',
toVersion(JSON_SERIALIZER, version(testDoc.readTime))
);
}
describeSpec('Bundles:', [], () => {
specTest('Newer docs from bundles should overwrite cache', [], () => {
const query1 = query('collection');
const docA = doc('collection/a', 1000, { value: 'a' });
const docAChanged = doc('collection/a', 2999, { value: 'b' });
const bundleString = bundleWithDocumentAndQuery({
key: docA.key,
readTime: 3000,
createTime: 1999,
updateTime: 2999,
content: { value: 'b' }
});
return (
spec()
.userListens(query1)
.watchAcksFull(query1, 1000, docA)
.expectEvents(query1, { added: [docA] })
// TODO(b/160876443): This currently raises snapshots with
// `fromCache=false` if users already listen to some queries and bundles
// has newer version.
.loadBundle(bundleString)
.expectEvents(query1, { modified: [docAChanged] })
);
});
specTest(
'Newer deleted docs from bundles should delete cached docs',
[],
() => {
const query1 = query('collection');
const docA = doc('collection/a', 1000, { value: 'a' });
const bundleString = bundleWithDocumentAndQuery({
key: docA.key,
readTime: 3000
});
return spec()
.userListens(query1)
.watchAcksFull(query1, 1000, docA)
.expectEvents(query1, { added: [docA] })
.loadBundle(bundleString)
.expectEvents(query1, { removed: [docA] });
}
);
specTest('Older deleted docs from bundles should do nothing', [], () => {
const query1 = query('collection');
const docA = doc('collection/a', 1000, { value: 'a' });
const bundleString = bundleWithDocumentAndQuery({
key: docA.key,
readTime: 999
});
return (
spec()
.userListens(query1)
.watchAcksFull(query1, 1000, docA)
.expectEvents(query1, { added: [docA] })
// No events are expected here.
.loadBundle(bundleString)
);
});
specTest(
'Newer docs from bundles should raise snapshot only when Watch catches up with acknowledged writes',
[],
() => {
const query1 = query('collection');
const docA = doc('collection/a', 250, { value: 'a' });
const bundleBeforeMutationAck = bundleWithDocumentAndQuery({
key: docA.key,
readTime: 500,
createTime: 250,
updateTime: 500,
content: { value: 'b' }
});
const bundleAfterMutationAck = bundleWithDocumentAndQuery({
key: docA.key,
readTime: 1001,
createTime: 250,
updateTime: 1001,
content: { value: 'fromBundle' }
});
return (
spec()
// TODO(b/160878667): Figure out what happens when memory eager GC is on
// a bundle is loaded.
.ensureManualLruGC()
.userListens(query1)
.watchAcksFull(query1, 250, docA)
.expectEvents(query1, {
added: [doc('collection/a', 250, { value: 'a' })]
})
.userPatches('collection/a', { value: 'patched' })
.expectEvents(query1, {
modified: [
doc('collection/a', 250, {
value: 'patched'
}).setHasLocalMutations()
],
hasPendingWrites: true
})
.writeAcks('collection/a', 1000)
// loading bundleBeforeMutationAck will not raise snapshots, because its
// snapshot version is older than the acknowledged mutation.
.loadBundle(bundleBeforeMutationAck)
// loading bundleAfterMutationAck will raise a snapshot, because it is after
// the acknowledged mutation.
.loadBundle(bundleAfterMutationAck)
.expectEvents(query1, {
modified: [doc('collection/a', 1001, { value: 'fromBundle' })]
})
);
}
);
specTest(
'Newer docs from bundles should keep not raise snapshot if there are unacknowledged writes',
[],
() => {
const query1 = query('collection');
const docA = doc('collection/a', 250, { value: 'a' });
const bundleString = bundleWithDocumentAndQuery({
key: docA.key,
readTime: 1001,
createTime: 250,
updateTime: 1001,
content: { value: 'fromBundle' }
});
return (
spec()
.ensureManualLruGC()
.userListens(query1)
.watchAcksFull(query1, 250, docA)
.expectEvents(query1, {
added: [doc('collection/a', 250, { value: 'a' })]
})
.userPatches('collection/a', { value: 'patched' })
.expectEvents(query1, {
modified: [
doc('collection/a', 250, {
value: 'patched'
}).setHasLocalMutations()
],
hasPendingWrites: true
})
// Loading the bundle will not raise snapshots, because the
// mutation has not been acknowledged.
.loadBundle(bundleString)
);
}
);
specTest('Newer docs from bundles might lead to limbo doc', [], () => {
const query1 = query('collection');
const docA = doc('collection/a', 1000, { value: 'a' });
const bundleString1 = bundleWithDocumentAndQuery({
key: docA.key,
readTime: 500,
createTime: 250,
updateTime: 500,
content: { value: 'b' }
});
const limboQuery = newQueryForPath(docA.key.path);
return (
spec()
.ensureManualLruGC()
.userListens(query1)
.watchAcksFull(query1, 250)
// Backend tells is there is no such doc.
.expectEvents(query1, {})
// Bundle tells otherwise, leads to limbo.
.loadBundle(bundleString1)
.expectLimboDocs(docA.key)
.expectEvents(query1, {
added: [doc('collection/a', 500, { value: 'b' })],
fromCache: true
})
// .watchAcksFull(limboQuery, 1002, docA1)
.watchAcks(limboQuery)
.watchSends({ affects: [limboQuery] })
.watchCurrents(limboQuery, 'resume-token-1002')
.watchSnapshots(1002)
.expectLimboDocs()
.expectEvents(query1, {
removed: [doc('collection/a', 500, { value: 'b' })],
fromCache: false
})
);
});
specTest(
'Bundles query can be resumed from same query.',
['no-pipeline-conversion'],
() => {
const query1 = query('collection');
const docA = doc('collection/a', 100, { key: 'a' });
const bundleString1 = bundleWithDocumentAndQuery(
{
key: docA.key,
readTime: 500,
createTime: 250,
updateTime: 500,
content: { value: 'b' }
},
{ name: 'bundled-query', readTime: 400, query: query1 }
);
return spec()
.loadBundle(bundleString1)
.userListens(query1, { readTime: 400 })
.expectEvents(query1, {
added: [doc('collection/a', 500, { value: 'b' })],
fromCache: true
});
}
);
specTest(
'Bundles query can be loaded and resumed from different tabs',
['multi-client', 'no-pipeline-conversion'],
() => {
const query1 = query('collection');
const query2 = query('collection', filter('value', '==', 'c'));
const docA = doc('collection/a', 100, { value: 'a' });
const bundleString1 = bundleWithDocumentAndQuery(
{
key: docA.key,
readTime: 500,
createTime: 250,
updateTime: 500,
content: { value: 'b' }
},
{ name: 'bundled-query', readTime: 400, query: query1 }
);
const bundleString2 = bundleWithDocumentAndQuery(
{
key: docA.key,
readTime: 600,
createTime: 250,
updateTime: 550,
content: { value: 'c' }
},
{ name: 'bundled-query', readTime: 560, query: query2 }
);
return (
client(0)
.loadBundle(bundleString1)
// Read named query from loaded bundle by primary.
.client(1)
.userListens(query1, { readTime: 400 })
.expectEvents(query1, {
added: [doc('collection/a', 500, { value: 'b' })],
fromCache: true
})
// Loads a newer bundle.
.loadBundle(bundleString2)
.expectEvents(query1, {
modified: [doc('collection/a', 550, { value: 'c' })],
fromCache: true
})
// Read named query from loaded bundle by secondary.
.client(0)
.expectListen(query1, { readTime: 400 })
.expectActiveTargets({ query: query1, readTime: 400 })
.userListens(query2, { readTime: 560 })
.expectEvents(query2, {
added: [doc('collection/a', 550, { value: 'c' })],
fromCache: true
})
);
}
);
specTest(
'Load from secondary clients and observe from primary',
['multi-client'],
() => {
const query1 = query('collection');
const docA = doc('collection/a', 250, { value: 'a' });
const bundleString1 = bundleWithDocumentAndQuery({
key: docA.key,
readTime: 500,
createTime: 250,
updateTime: 500,
content: { value: 'b' }
});
return client(0)
.userListens(query1)
.watchAcksFull(query1, 250, docA)
.expectEvents(query1, {
added: [docA]
})
.client(1)
.loadBundle(bundleString1)
.client(0)
.expectEvents(query1, {
modified: [doc('collection/a', 500, { value: 'b' })]
});
}
);
specTest(
'Load and observe from same secondary client',
['multi-client'],
() => {
const query1 = query('collection');
const docA = doc('collection/a', 250, { value: 'a' });
const bundleString = bundleWithDocumentAndQuery({
key: docA.key,
readTime: 500,
createTime: 250,
updateTime: 500,
content: { value: 'b' }
});
return client(0)
.userListens(query1)
.watchAcksFull(query1, 250, docA)
.expectEvents(query1, {
added: [docA]
})
.client(1)
.userListens(query1)
.expectEvents(query1, {
added: [docA]
})
.loadBundle(bundleString)
.expectEvents(query1, {
modified: [doc('collection/a', 500, { value: 'b' })]
});
}
);
specTest(
'Load from primary client and observe from secondary',
['multi-client'],
() => {
const query1 = query('collection');
const docA = doc('collection/a', 250, { value: 'a' });
const bundleString1 = bundleWithDocumentAndQuery({
key: docA.key,
readTime: 500,
createTime: 250,
updateTime: 500,
content: { value: 'b' }
});
return client(0)
.userListens(query1)
.watchAcksFull(query1, 250, docA)
.expectEvents(query1, {
added: [docA]
})
.client(1)
.userListens(query1)
.expectEvents(query1, {
added: [docA]
})
.client(0)
.loadBundle(bundleString1)
.expectEvents(query1, {
modified: [doc('collection/a', 500, { value: 'b' })]
})
.client(1)
.expectEvents(query1, {
modified: [doc('collection/a', 500, { value: 'b' })]
});
}
);
});