mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
826 lines
38 KiB
Plaintext
826 lines
38 KiB
Plaintext
/*
|
|
* Copyright 2017 Google
|
|
*
|
|
* 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 <XCTest/XCTest.h>
|
|
|
|
#include <memory>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#import "Firestore/Source/Core/FSTQuery.h"
|
|
#import "Firestore/Source/Local/FSTQueryData.h"
|
|
#import "Firestore/Source/Model/FSTDocument.h"
|
|
|
|
#include "Firestore/core/src/firebase/firestore/model/document_key.h"
|
|
#include "Firestore/core/src/firebase/firestore/model/types.h"
|
|
#include "Firestore/core/src/firebase/firestore/remote/existence_filter.h"
|
|
#include "Firestore/core/src/firebase/firestore/remote/remote_event.h"
|
|
#include "Firestore/core/src/firebase/firestore/remote/watch_change.h"
|
|
|
|
#import "Firestore/Example/Tests/Util/FSTHelpers.h"
|
|
|
|
#include "Firestore/core/test/firebase/firestore/testutil/testutil.h"
|
|
#include "absl/memory/memory.h"
|
|
|
|
namespace testutil = firebase::firestore::testutil;
|
|
using firebase::firestore::model::DocumentKey;
|
|
using firebase::firestore::model::DocumentKeySet;
|
|
using firebase::firestore::model::SnapshotVersion;
|
|
using firebase::firestore::model::TargetId;
|
|
using firebase::firestore::remote::DocumentWatchChange;
|
|
using firebase::firestore::remote::ExistenceFilter;
|
|
using firebase::firestore::remote::ExistenceFilterWatchChange;
|
|
using firebase::firestore::remote::RemoteEvent;
|
|
using firebase::firestore::remote::TargetChange;
|
|
using firebase::firestore::remote::TestTargetMetadataProvider;
|
|
using firebase::firestore::remote::WatchChange;
|
|
using firebase::firestore::remote::WatchChangeAggregator;
|
|
using firebase::firestore::remote::WatchTargetChange;
|
|
using firebase::firestore::remote::WatchTargetChangeState;
|
|
using firebase::firestore::testutil::VectorOfUniquePtrs;
|
|
using firebase::firestore::util::MakeString;
|
|
using firebase::firestore::util::Status;
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
namespace {
|
|
|
|
template <typename... Elems>
|
|
std::vector<std::unique_ptr<WatchChange>> Changes(Elems... elems) {
|
|
return VectorOfUniquePtrs<WatchChange>(std::move(elems)...);
|
|
}
|
|
|
|
// These helpers work around the fact that `make_unique` cannot deduce the
|
|
// desired type (`vector<TargetId>` in this case) from an initialization list
|
|
// (e.g., `{1,2}`).
|
|
std::unique_ptr<DocumentWatchChange> MakeDocChange(std::vector<TargetId> updated,
|
|
std::vector<TargetId> removed,
|
|
DocumentKey key,
|
|
FSTMaybeDocument *doc) {
|
|
return absl::make_unique<DocumentWatchChange>(std::move(updated), std::move(removed),
|
|
std::move(key), doc);
|
|
}
|
|
|
|
std::unique_ptr<WatchTargetChange> MakeTargetChange(WatchTargetChangeState state,
|
|
std::vector<TargetId> target_ids) {
|
|
return absl::make_unique<WatchTargetChange>(state, std::move(target_ids));
|
|
}
|
|
|
|
std::unique_ptr<WatchTargetChange> MakeTargetChange(WatchTargetChangeState state,
|
|
std::vector<TargetId> target_ids,
|
|
NSData *token) {
|
|
return absl::make_unique<WatchTargetChange>(state, std::move(target_ids), token);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
@interface FSTRemoteEventTests : XCTestCase
|
|
@end
|
|
|
|
@implementation FSTRemoteEventTests {
|
|
NSData *_resumeToken1;
|
|
TestTargetMetadataProvider _targetMetadataProvider;
|
|
std::unordered_map<TargetId, int> _noOutstandingResponses;
|
|
}
|
|
|
|
- (void)setUp {
|
|
_resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding];
|
|
}
|
|
|
|
/**
|
|
* Creates a map with query data for the provided target IDs. All targets are considered active
|
|
* and query a collection named "coll".
|
|
*/
|
|
- (std::unordered_map<TargetId, FSTQueryData *>)queryDataForTargets:
|
|
(std::initializer_list<TargetId>)targetIDs {
|
|
std::unordered_map<TargetId, FSTQueryData *> targets;
|
|
for (TargetId targetID : targetIDs) {
|
|
FSTQuery *query = FSTTestQuery("coll");
|
|
targets[targetID] = [[FSTQueryData alloc] initWithQuery:query
|
|
targetID:targetID
|
|
listenSequenceNumber:0
|
|
purpose:FSTQueryPurposeListen];
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
/**
|
|
* Creates a map with query data for the provided target IDs. All targets are marked as limbo
|
|
* queries for the document at "coll/limbo".
|
|
*/
|
|
- (std::unordered_map<TargetId, FSTQueryData *>)queryDataForLimboTargets:
|
|
(std::initializer_list<TargetId>)targetIDs {
|
|
std::unordered_map<TargetId, FSTQueryData *> targets;
|
|
for (TargetId targetID : targetIDs) {
|
|
FSTQuery *query = FSTTestQuery("coll/limbo");
|
|
targets[targetID] = [[FSTQueryData alloc] initWithQuery:query
|
|
targetID:targetID
|
|
listenSequenceNumber:0
|
|
purpose:FSTQueryPurposeLimboResolution];
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
/**
|
|
* Creates an aggregator initialized with the set of provided `WatchChange`s. Tests can add further
|
|
* changes via `HandleDocumentChange`, `HandleTargetChange` and `HandleExistenceFilterChange`.
|
|
*
|
|
* @param targetMap A map of query data for all active targets. The map must include an entry for
|
|
* every target referenced by any of the watch changes.
|
|
* @param outstandingResponses The number of outstanding ACKs a target has to receive before it is
|
|
* considered active, or `_noOutstandingResponses` if all targets are already active.
|
|
* @param existingKeys The set of documents that are considered synced with the test targets as
|
|
* part of a previous listen. To modify this set during test execution, invoke
|
|
* `_targetMetadataProvider.SetSyncedKeys()`.
|
|
* @param watchChanges The watch changes to apply before returning the aggregator. Supported
|
|
* changes are `DocumentWatchChange` and `WatchTargetChange`.
|
|
*/
|
|
- (WatchChangeAggregator)
|
|
aggregatorWithTargetMap:(const std::unordered_map<TargetId, FSTQueryData *> &)targetMap
|
|
outstandingResponses:(const std::unordered_map<TargetId, int> &)outstandingResponses
|
|
existingKeys:(DocumentKeySet)existingKeys
|
|
changes:(const std::vector<std::unique_ptr<WatchChange>> &)watchChanges {
|
|
WatchChangeAggregator aggregator{&_targetMetadataProvider};
|
|
|
|
std::vector<TargetId> targetIDs;
|
|
for (const auto &kv : targetMap) {
|
|
TargetId targetID = kv.first;
|
|
FSTQueryData *queryData = kv.second;
|
|
|
|
targetIDs.push_back(targetID);
|
|
_targetMetadataProvider.SetSyncedKeys(existingKeys, queryData);
|
|
};
|
|
|
|
for (const auto &kv : outstandingResponses) {
|
|
TargetId targetID = kv.first;
|
|
int count = kv.second;
|
|
for (int i = 0; i < count; ++i) {
|
|
aggregator.RecordPendingTargetRequest(targetID);
|
|
}
|
|
}
|
|
|
|
for (const std::unique_ptr<WatchChange> &change : watchChanges) {
|
|
switch (change->type()) {
|
|
case WatchChange::Type::Document: {
|
|
aggregator.HandleDocumentChange(*static_cast<const DocumentWatchChange *>(change.get()));
|
|
break;
|
|
}
|
|
case WatchChange::Type::TargetChange: {
|
|
aggregator.HandleTargetChange(*static_cast<const WatchTargetChange *>(change.get()));
|
|
break;
|
|
}
|
|
default:
|
|
HARD_ASSERT("Encountered unexpected type of WatchChange");
|
|
}
|
|
}
|
|
|
|
aggregator.HandleTargetChange(
|
|
WatchTargetChange{WatchTargetChangeState::NoChange, targetIDs, _resumeToken1});
|
|
|
|
return aggregator;
|
|
}
|
|
|
|
/**
|
|
* Creates a single remote event that includes target changes for all provided `WatchChange`s.
|
|
*
|
|
* @param snapshotVersion The version at which to create the remote event. This corresponds to the
|
|
* snapshot version provided by the NO_CHANGE event.
|
|
* @param targetMap A map of query data for all active targets. The map must include an entry for
|
|
* every target referenced by any of the watch changes.
|
|
* @param outstandingResponses The number of outstanding ACKs a target has to receive before it is
|
|
* considered active, or `_noOutstandingResponses` if all targets are already active.
|
|
* @param existingKeys The set of documents that are considered synced with the test targets as
|
|
* part of a previous listen.
|
|
* @param watchChanges The watch changes to apply before creating the remote event. Supported
|
|
* changes are `DocumentWatchChange` and `WatchTargetChange`.
|
|
*/
|
|
- (RemoteEvent)
|
|
remoteEventAtSnapshotVersion:(FSTTestSnapshotVersion)snapshotVersion
|
|
targetMap:(std::unordered_map<TargetId, FSTQueryData *>)targetMap
|
|
outstandingResponses:(const std::unordered_map<TargetId, int> &)outstandingResponses
|
|
existingKeys:(DocumentKeySet)existingKeys
|
|
changes:(const std::vector<std::unique_ptr<WatchChange>> &)watchChanges {
|
|
WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap
|
|
outstandingResponses:outstandingResponses
|
|
existingKeys:existingKeys
|
|
changes:watchChanges];
|
|
return aggregator.CreateRemoteEvent(testutil::Version(snapshotVersion));
|
|
}
|
|
|
|
- (void)testWillAccumulateDocumentAddedAndRemovedEvents {
|
|
// The target map that contains an entry for every target in this test. If a target ID is
|
|
// omitted, the target is considered inactive and `TestTargetMetadataProvider` will fail on
|
|
// access.
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{
|
|
[self queryDataForTargets:{1, 2, 3, 4, 5, 6}]};
|
|
|
|
FSTDocument *existingDoc = FSTTestDoc("docs/1", 1, @{@"value" : @1}, FSTDocumentStateSynced);
|
|
auto change1 = MakeDocChange({1, 2, 3}, {4, 5, 6}, existingDoc.key, existingDoc);
|
|
|
|
FSTDocument *newDoc = FSTTestDoc("docs/2", 2, @{@"value" : @2}, FSTDocumentStateSynced);
|
|
auto change2 = MakeDocChange({1, 4}, {2, 6}, newDoc.key, newDoc);
|
|
|
|
// Create a remote event that includes both `change1` and `change2` as well as a NO_CHANGE event
|
|
// with the default resume token (`_resumeToken1`).
|
|
// As `existingDoc` is provided as an existing key, any updates to this document will be treated
|
|
// as modifications rather than adds.
|
|
RemoteEvent event =
|
|
[self remoteEventAtSnapshotVersion:3
|
|
targetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet{existingDoc.key}
|
|
changes:Changes(std::move(change1), std::move(change2))];
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
XCTAssertEqual(event.document_updates().size(), 2);
|
|
XCTAssertEqualObjects(event.document_updates().at(existingDoc.key), existingDoc);
|
|
XCTAssertEqualObjects(event.document_updates().at(newDoc.key), newDoc);
|
|
|
|
// 'change1' and 'change2' affect six different targets
|
|
XCTAssertEqual(event.target_changes().size(), 6);
|
|
|
|
TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{newDoc.key},
|
|
DocumentKeySet{existingDoc.key}, DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(1) == targetChange1);
|
|
|
|
TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{},
|
|
DocumentKeySet{existingDoc.key}, DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(2) == targetChange2);
|
|
|
|
TargetChange targetChange3{_resumeToken1, false, DocumentKeySet{},
|
|
DocumentKeySet{existingDoc.key}, DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(3) == targetChange3);
|
|
|
|
TargetChange targetChange4{_resumeToken1, false, DocumentKeySet{newDoc.key}, DocumentKeySet{},
|
|
DocumentKeySet{existingDoc.key}};
|
|
XCTAssertTrue(event.target_changes().at(4) == targetChange4);
|
|
|
|
TargetChange targetChange5{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{},
|
|
DocumentKeySet{existingDoc.key}};
|
|
XCTAssertTrue(event.target_changes().at(5) == targetChange5);
|
|
|
|
TargetChange targetChange6{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{},
|
|
DocumentKeySet{existingDoc.key}};
|
|
XCTAssertTrue(event.target_changes().at(6) == targetChange6);
|
|
}
|
|
|
|
- (void)testWillIgnoreEventsForPendingTargets {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1}]};
|
|
|
|
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"value" : @1}, FSTDocumentStateSynced);
|
|
auto change1 = MakeDocChange({1}, {}, doc1.key, doc1);
|
|
auto change2 = MakeTargetChange(WatchTargetChangeState::Removed, {1});
|
|
auto change3 = MakeTargetChange(WatchTargetChangeState::Added, {1});
|
|
FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{@"value" : @2}, FSTDocumentStateSynced);
|
|
auto change4 = MakeDocChange({1}, {}, doc2.key, doc2);
|
|
|
|
// We're waiting for the unwatch and watch ack
|
|
std::unordered_map<TargetId, int> outstandingResponses{{1, 2}};
|
|
|
|
RemoteEvent event =
|
|
[self remoteEventAtSnapshotVersion:3
|
|
targetMap:targetMap
|
|
outstandingResponses:outstandingResponses
|
|
existingKeys:DocumentKeySet {}
|
|
changes:Changes(std::move(change1), std::move(change2),
|
|
std::move(change3), std::move(change4))];
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
// doc1 is ignored because it was part of an inactive target, but doc2 is in the changes
|
|
// because it become active.
|
|
XCTAssertEqual(event.document_updates().size(), 1);
|
|
XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2);
|
|
|
|
XCTAssertEqual(event.target_changes().size(), 1);
|
|
}
|
|
|
|
- (void)testWillIgnoreEventsForRemovedTargets {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{}]};
|
|
|
|
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"value" : @1}, FSTDocumentStateSynced);
|
|
auto change1 = MakeDocChange({1}, {}, doc1.key, doc1);
|
|
auto change2 = MakeTargetChange(WatchTargetChangeState::Removed, {1});
|
|
|
|
// We're waiting for the unwatch ack
|
|
std::unordered_map<TargetId, int> outstandingResponses{{1, 1}};
|
|
|
|
RemoteEvent event =
|
|
[self remoteEventAtSnapshotVersion:3
|
|
targetMap:targetMap
|
|
outstandingResponses:outstandingResponses
|
|
existingKeys:DocumentKeySet {}
|
|
changes:Changes(std::move(change1), std::move(change2))];
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
// doc1 is ignored because it was part of an inactive target
|
|
XCTAssertEqual(event.document_updates().size(), 0);
|
|
|
|
// Target 1 is ignored because it was removed
|
|
XCTAssertEqual(event.target_changes().size(), 0);
|
|
}
|
|
|
|
- (void)testWillKeepResetMappingEvenWithUpdates {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1}]};
|
|
|
|
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"value" : @1}, FSTDocumentStateSynced);
|
|
auto change1 = MakeDocChange({1}, {}, doc1.key, doc1);
|
|
|
|
// Reset stream, ignoring doc1
|
|
auto change2 = MakeTargetChange(WatchTargetChangeState::Reset, {1});
|
|
|
|
// Add doc2, doc3
|
|
FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{@"value" : @2}, FSTDocumentStateSynced);
|
|
auto change3 = MakeDocChange({1}, {}, doc2.key, doc2);
|
|
|
|
FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{@"value" : @3}, FSTDocumentStateSynced);
|
|
auto change4 = MakeDocChange({1}, {}, doc3.key, doc3);
|
|
|
|
// Remove doc2 again, should not show up in reset mapping
|
|
auto change5 = MakeDocChange({}, {1}, doc2.key, doc2);
|
|
|
|
RemoteEvent event =
|
|
[self remoteEventAtSnapshotVersion:3
|
|
targetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet{doc1.key}
|
|
changes:Changes(std::move(change1), std::move(change2),
|
|
std::move(change3), std::move(change4),
|
|
std::move(change5))];
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
XCTAssertEqual(event.document_updates().size(), 3);
|
|
XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1);
|
|
XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2);
|
|
XCTAssertEqualObjects(event.document_updates().at(doc3.key), doc3);
|
|
|
|
XCTAssertEqual(event.target_changes().size(), 1);
|
|
|
|
// Only doc3 is part of the new mapping
|
|
TargetChange expectedChange{_resumeToken1, false, DocumentKeySet{doc3.key}, DocumentKeySet{},
|
|
DocumentKeySet{doc1.key}};
|
|
XCTAssertTrue(event.target_changes().at(1) == expectedChange);
|
|
}
|
|
|
|
- (void)testWillHandleSingleReset {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1}]};
|
|
|
|
// Reset target
|
|
WatchTargetChange change{WatchTargetChangeState::Reset, {1}};
|
|
|
|
WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet {}
|
|
changes:{}];
|
|
aggregator.HandleTargetChange(change);
|
|
|
|
RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3));
|
|
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
XCTAssertEqual(event.document_updates().size(), 0);
|
|
XCTAssertEqual(event.target_changes().size(), 1);
|
|
|
|
// Reset mapping is empty
|
|
TargetChange expectedChange{
|
|
[NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(1) == expectedChange);
|
|
}
|
|
|
|
- (void)testWillHandleTargetAddAndRemovalInSameBatch {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1, 2}]};
|
|
|
|
FSTDocument *doc1a = FSTTestDoc("docs/1", 1, @{@"value" : @1}, FSTDocumentStateSynced);
|
|
auto change1 = MakeDocChange({1}, {2}, doc1a.key, doc1a);
|
|
|
|
FSTDocument *doc1b = FSTTestDoc("docs/1", 1, @{@"value" : @2}, FSTDocumentStateSynced);
|
|
auto change2 = MakeDocChange({2}, {1}, doc1b.key, doc1b);
|
|
|
|
RemoteEvent event =
|
|
[self remoteEventAtSnapshotVersion:3
|
|
targetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet{doc1a.key}
|
|
changes:Changes(std::move(change1), std::move(change2))];
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
XCTAssertEqual(event.document_updates().size(), 1);
|
|
XCTAssertEqualObjects(event.document_updates().at(doc1b.key), doc1b);
|
|
|
|
XCTAssertEqual(event.target_changes().size(), 2);
|
|
|
|
TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{},
|
|
DocumentKeySet{doc1b.key}};
|
|
XCTAssertTrue(event.target_changes().at(1) == targetChange1);
|
|
|
|
TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{doc1b.key},
|
|
DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(2) == targetChange2);
|
|
}
|
|
|
|
- (void)testTargetCurrentChangeWillMarkTheTargetCurrent {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1}]};
|
|
|
|
auto change = MakeTargetChange(WatchTargetChangeState::Current, {1}, _resumeToken1);
|
|
|
|
RemoteEvent event = [self remoteEventAtSnapshotVersion:3
|
|
targetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet {}
|
|
changes:Changes(std::move(change))];
|
|
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
XCTAssertEqual(event.document_updates().size(), 0);
|
|
XCTAssertEqual(event.target_changes().size(), 1);
|
|
|
|
TargetChange targetChange1{_resumeToken1, true, DocumentKeySet{}, DocumentKeySet{},
|
|
DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(1) == targetChange1);
|
|
}
|
|
|
|
- (void)testTargetAddedChangeWillResetPreviousState {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1, 3}]};
|
|
|
|
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"value" : @1}, FSTDocumentStateSynced);
|
|
auto change1 = MakeDocChange({1, 3}, {2}, doc1.key, doc1);
|
|
auto change2 = MakeTargetChange(WatchTargetChangeState::Current, {1, 2, 3}, _resumeToken1);
|
|
auto change3 = MakeTargetChange(WatchTargetChangeState::Removed, {1});
|
|
auto change4 = MakeTargetChange(WatchTargetChangeState::Removed, {2});
|
|
auto change5 = MakeTargetChange(WatchTargetChangeState::Added, {1});
|
|
FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{@"value" : @2}, FSTDocumentStateSynced);
|
|
auto change6 = MakeDocChange({1}, {3}, doc2.key, doc2);
|
|
|
|
std::unordered_map<TargetId, int> outstandingResponses{{1, 2}, {2, 1}};
|
|
|
|
RemoteEvent event =
|
|
[self remoteEventAtSnapshotVersion:3
|
|
targetMap:targetMap
|
|
outstandingResponses:outstandingResponses
|
|
existingKeys:DocumentKeySet{doc2.key}
|
|
changes:Changes(std::move(change1), std::move(change2),
|
|
std::move(change3), std::move(change4),
|
|
std::move(change5), std::move(change6))];
|
|
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
XCTAssertEqual(event.document_updates().size(), 2);
|
|
XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1);
|
|
XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2);
|
|
|
|
// target 1 and 3 are affected (1 because of re-add), target 2 is not because of remove
|
|
XCTAssertEqual(event.target_changes().size(), 2);
|
|
|
|
// doc1 was before the remove, so it does not show up in the mapping.
|
|
// Current was before the remove.
|
|
TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{doc2.key},
|
|
DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(1) == targetChange1);
|
|
|
|
// Doc1 was before the remove
|
|
// Current was before the remove
|
|
TargetChange targetChange3{_resumeToken1, true, DocumentKeySet{doc1.key}, DocumentKeySet{},
|
|
DocumentKeySet{doc2.key}};
|
|
XCTAssertTrue(event.target_changes().at(3) == targetChange3);
|
|
}
|
|
|
|
- (void)testNoChangeWillStillMarkTheAffectedTargets {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1}]};
|
|
|
|
WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet {}
|
|
changes:{}];
|
|
|
|
WatchTargetChange change{WatchTargetChangeState::NoChange, {1}, _resumeToken1};
|
|
aggregator.HandleTargetChange(change);
|
|
|
|
RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3));
|
|
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
XCTAssertEqual(event.document_updates().size(), 0);
|
|
XCTAssertEqual(event.target_changes().size(), 1);
|
|
|
|
TargetChange targetChange{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{},
|
|
DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(1) == targetChange);
|
|
}
|
|
|
|
- (void)testExistenceFilterMismatchClearsTarget {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1, 2}]};
|
|
|
|
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"value" : @1}, FSTDocumentStateSynced);
|
|
auto change1 = MakeDocChange({1}, {}, doc1.key, doc1);
|
|
FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{@"value" : @2}, FSTDocumentStateSynced);
|
|
auto change2 = MakeDocChange({1}, {}, doc2.key, doc2);
|
|
auto change3 = MakeTargetChange(WatchTargetChangeState::Current, {1}, _resumeToken1);
|
|
|
|
WatchChangeAggregator aggregator = [self
|
|
aggregatorWithTargetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet{doc1.key, doc2.key}
|
|
changes:Changes(std::move(change1), std::move(change2), std::move(change3))];
|
|
|
|
RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3));
|
|
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
XCTAssertEqual(event.document_updates().size(), 2);
|
|
XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1);
|
|
XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2);
|
|
|
|
XCTAssertEqual(event.target_changes().size(), 2);
|
|
|
|
TargetChange targetChange1{_resumeToken1, true, DocumentKeySet{},
|
|
DocumentKeySet{doc1.key, doc2.key}, DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(1) == targetChange1);
|
|
|
|
TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{},
|
|
DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(2) == targetChange2);
|
|
|
|
// The existence filter mismatch will remove the document from target 1,
|
|
// but not synthesize a document delete.
|
|
ExistenceFilterWatchChange change4{ExistenceFilter{1}, 1};
|
|
aggregator.HandleExistenceFilter(change4);
|
|
|
|
event = aggregator.CreateRemoteEvent(testutil::Version(4));
|
|
|
|
TargetChange targetChange3{
|
|
[NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}};
|
|
XCTAssertTrue(event.target_changes().at(1) == targetChange3);
|
|
|
|
XCTAssertEqual(event.target_changes().size(), 1);
|
|
XCTAssertEqual(event.target_mismatches().size(), 1);
|
|
XCTAssertEqual(event.document_updates().size(), 0);
|
|
}
|
|
|
|
- (void)testExistenceFilterMismatchRemovesCurrentChanges {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1}]};
|
|
|
|
WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet {}
|
|
changes:{}];
|
|
|
|
WatchTargetChange markCurrent{WatchTargetChangeState::Current, {1}, _resumeToken1};
|
|
aggregator.HandleTargetChange(markCurrent);
|
|
|
|
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"value" : @1}, FSTDocumentStateSynced);
|
|
DocumentWatchChange addDoc{{1}, {}, doc1.key, doc1};
|
|
aggregator.HandleDocumentChange(addDoc);
|
|
|
|
// The existence filter mismatch will remove the document from target 1, but not synthesize a
|
|
// document delete.
|
|
ExistenceFilterWatchChange existenceFilter{ExistenceFilter{0}, 1};
|
|
aggregator.HandleExistenceFilter(existenceFilter);
|
|
|
|
RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3));
|
|
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
XCTAssertEqual(event.document_updates().size(), 1);
|
|
XCTAssertEqual(event.target_mismatches().size(), 1);
|
|
XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1);
|
|
|
|
XCTAssertEqual(event.target_changes().size(), 1);
|
|
|
|
TargetChange targetChange1{
|
|
[NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(1) == targetChange1);
|
|
}
|
|
|
|
- (void)testDocumentUpdate {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1}]};
|
|
|
|
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"value" : @1}, FSTDocumentStateSynced);
|
|
auto change1 = MakeDocChange({1}, {}, doc1.key, doc1);
|
|
FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{@"value" : @2}, FSTDocumentStateSynced);
|
|
auto change2 = MakeDocChange({1}, {}, doc2.key, doc2);
|
|
|
|
WatchChangeAggregator aggregator =
|
|
[self aggregatorWithTargetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet {}
|
|
changes:Changes(std::move(change1), std::move(change2))];
|
|
|
|
RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3));
|
|
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
XCTAssertEqual(event.document_updates().size(), 2);
|
|
XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1);
|
|
XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2);
|
|
|
|
_targetMetadataProvider.SetSyncedKeys(DocumentKeySet{doc1.key, doc2.key}, targetMap[1]);
|
|
|
|
FSTDeletedDocument *deletedDoc1 = [FSTDeletedDocument documentWithKey:doc1.key
|
|
version:testutil::Version(3)
|
|
hasCommittedMutations:NO];
|
|
DocumentWatchChange change3{{}, {1}, deletedDoc1.key, deletedDoc1};
|
|
aggregator.HandleDocumentChange(change3);
|
|
|
|
FSTDocument *updatedDoc2 = FSTTestDoc("docs/2", 3, @{@"value" : @2}, FSTDocumentStateSynced);
|
|
DocumentWatchChange change4{{1}, {}, updatedDoc2.key, updatedDoc2};
|
|
aggregator.HandleDocumentChange(change4);
|
|
|
|
FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{@"value" : @3}, FSTDocumentStateSynced);
|
|
DocumentWatchChange change5{{1}, {}, doc3.key, doc3};
|
|
aggregator.HandleDocumentChange(change5);
|
|
|
|
event = aggregator.CreateRemoteEvent(testutil::Version(3));
|
|
|
|
XCTAssertEqual(event.snapshot_version(), testutil::Version(3));
|
|
XCTAssertEqual(event.document_updates().size(), 3);
|
|
// doc1 is replaced
|
|
XCTAssertEqualObjects(event.document_updates().at(doc1.key), deletedDoc1);
|
|
// doc2 is updated
|
|
XCTAssertEqualObjects(event.document_updates().at(doc2.key), updatedDoc2);
|
|
// doc3 is new
|
|
XCTAssertEqualObjects(event.document_updates().at(doc3.key), doc3);
|
|
|
|
// Target is unchanged
|
|
XCTAssertEqual(event.target_changes().size(), 1);
|
|
|
|
TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{doc3.key},
|
|
DocumentKeySet{updatedDoc2.key}, DocumentKeySet{deletedDoc1.key}};
|
|
XCTAssertTrue(event.target_changes().at(1) == targetChange1);
|
|
}
|
|
|
|
- (void)testResumeTokensHandledPerTarget {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1, 2}]};
|
|
|
|
WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet {}
|
|
changes:{}];
|
|
|
|
WatchTargetChange change1{WatchTargetChangeState::Current, {1}, _resumeToken1};
|
|
aggregator.HandleTargetChange(change1);
|
|
|
|
NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding];
|
|
WatchTargetChange change2{WatchTargetChangeState::Current, {2}, resumeToken2};
|
|
aggregator.HandleTargetChange(change2);
|
|
|
|
RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3));
|
|
XCTAssertEqual(event.target_changes().size(), 2);
|
|
|
|
TargetChange targetChange1{_resumeToken1, true, DocumentKeySet{}, DocumentKeySet{},
|
|
DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(1) == targetChange1);
|
|
|
|
TargetChange targetChange2{resumeToken2, true, DocumentKeySet{}, DocumentKeySet{},
|
|
DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(2) == targetChange2);
|
|
}
|
|
|
|
- (void)testLastResumeTokenWins {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1, 2}]};
|
|
|
|
WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet {}
|
|
changes:{}];
|
|
|
|
WatchTargetChange change1{WatchTargetChangeState::Current, {1}, _resumeToken1};
|
|
aggregator.HandleTargetChange(change1);
|
|
|
|
NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding];
|
|
WatchTargetChange change2{WatchTargetChangeState::NoChange, {1}, resumeToken2};
|
|
aggregator.HandleTargetChange(change2);
|
|
|
|
NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding];
|
|
WatchTargetChange change3{WatchTargetChangeState::NoChange, {2}, resumeToken3};
|
|
aggregator.HandleTargetChange(change3);
|
|
|
|
RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3));
|
|
XCTAssertEqual(event.target_changes().size(), 2);
|
|
|
|
TargetChange targetChange1{resumeToken2, true, DocumentKeySet{}, DocumentKeySet{},
|
|
DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(1) == targetChange1);
|
|
|
|
TargetChange targetChange2{resumeToken3, false, DocumentKeySet{}, DocumentKeySet{},
|
|
DocumentKeySet{}};
|
|
XCTAssertTrue(event.target_changes().at(2) == targetChange2);
|
|
}
|
|
|
|
- (void)testSynthesizeDeletes {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForLimboTargets:{1}]};
|
|
DocumentKey limboKey = testutil::Key("coll/limbo");
|
|
|
|
auto resolveLimboTarget = MakeTargetChange(WatchTargetChangeState::Current, {1});
|
|
RemoteEvent event = [self remoteEventAtSnapshotVersion:3
|
|
targetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet {}
|
|
changes:Changes(std::move(resolveLimboTarget))];
|
|
|
|
FSTDeletedDocument *expected = [FSTDeletedDocument documentWithKey:limboKey
|
|
version:event.snapshot_version()
|
|
hasCommittedMutations:NO];
|
|
XCTAssertEqualObjects(event.document_updates().at(limboKey), expected);
|
|
XCTAssertTrue(event.limbo_document_changes().contains(limboKey));
|
|
}
|
|
|
|
- (void)testDoesntSynthesizeDeletesForWrongState {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{1}]};
|
|
|
|
auto wrongState = MakeTargetChange(WatchTargetChangeState::NoChange, {1});
|
|
|
|
RemoteEvent event = [self remoteEventAtSnapshotVersion:3
|
|
targetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet {}
|
|
changes:Changes(std::move(wrongState))];
|
|
|
|
XCTAssertEqual(event.document_updates().size(), 0);
|
|
XCTAssertEqual(event.limbo_document_changes().size(), 0);
|
|
}
|
|
|
|
- (void)testDoesntSynthesizeDeletesForExistingDoc {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForTargets:{3}]};
|
|
|
|
auto hasDocument = MakeTargetChange(WatchTargetChangeState::Current, {3});
|
|
|
|
RemoteEvent event =
|
|
[self remoteEventAtSnapshotVersion:3
|
|
targetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet{FSTTestDocKey(@"coll/limbo")}
|
|
changes:Changes(std::move(hasDocument))];
|
|
|
|
XCTAssertEqual(event.document_updates().size(), 0);
|
|
XCTAssertEqual(event.limbo_document_changes().size(), 0);
|
|
}
|
|
|
|
- (void)testSeparatesDocumentUpdates {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap{[self queryDataForLimboTargets:{1}]};
|
|
|
|
FSTDocument *newDoc = FSTTestDoc("docs/new", 1, @{@"key" : @"value"}, FSTDocumentStateSynced);
|
|
auto newDocChange = MakeDocChange({1}, {}, newDoc.key, newDoc);
|
|
|
|
FSTDocument *existingDoc =
|
|
FSTTestDoc("docs/existing", 1, @{@"some" : @"data"}, FSTDocumentStateSynced);
|
|
auto existingDocChange = MakeDocChange({1}, {}, existingDoc.key, existingDoc);
|
|
|
|
FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc("docs/deleted", 1, NO);
|
|
auto deletedDocChange = MakeDocChange({}, {1}, deletedDoc.key, deletedDoc);
|
|
|
|
FSTDeletedDocument *missingDoc = FSTTestDeletedDoc("docs/missing", 1, NO);
|
|
auto missingDocChange = MakeDocChange({}, {1}, missingDoc.key, missingDoc);
|
|
|
|
RemoteEvent event = [self
|
|
remoteEventAtSnapshotVersion:3
|
|
targetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet{existingDoc.key, deletedDoc.key}
|
|
changes:Changes(std::move(newDocChange), std::move(existingDocChange),
|
|
std::move(deletedDocChange),
|
|
std::move(missingDocChange))];
|
|
|
|
TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{newDoc.key},
|
|
DocumentKeySet{existingDoc.key}, DocumentKeySet{deletedDoc.key}};
|
|
|
|
XCTAssertTrue(event.target_changes().at(1) == targetChange2);
|
|
}
|
|
|
|
- (void)testTracksLimboDocuments {
|
|
std::unordered_map<TargetId, FSTQueryData *> targetMap = [self queryDataForTargets:{1}];
|
|
auto additionalTargets = [self queryDataForLimboTargets:{2}];
|
|
targetMap.insert(additionalTargets.begin(), additionalTargets.end());
|
|
|
|
// Add 3 docs: 1 is limbo and non-limbo, 2 is limbo-only, 3 is non-limbo
|
|
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"key" : @"value"}, FSTDocumentStateSynced);
|
|
FSTDocument *doc2 = FSTTestDoc("docs/2", 1, @{@"key" : @"value"}, FSTDocumentStateSynced);
|
|
FSTDocument *doc3 = FSTTestDoc("docs/3", 1, @{@"key" : @"value"}, FSTDocumentStateSynced);
|
|
|
|
// Target 2 is a limbo target
|
|
auto docChange1 = MakeDocChange({1, 2}, {}, doc1.key, doc1);
|
|
auto docChange2 = MakeDocChange({2}, {}, doc2.key, doc2);
|
|
auto docChange3 = MakeDocChange({1}, {}, doc3.key, doc3);
|
|
auto targetsChange = MakeTargetChange(WatchTargetChangeState::Current, {1, 2});
|
|
|
|
RemoteEvent event =
|
|
[self remoteEventAtSnapshotVersion:3
|
|
targetMap:targetMap
|
|
outstandingResponses:_noOutstandingResponses
|
|
existingKeys:DocumentKeySet {}
|
|
changes:Changes(std::move(docChange1), std::move(docChange2),
|
|
std::move(docChange3), std::move(targetsChange))];
|
|
|
|
DocumentKeySet limboDocChanges = event.limbo_document_changes();
|
|
// Doc1 is in both limbo and non-limbo targets, therefore not tracked as limbo
|
|
XCTAssertFalse(limboDocChanges.contains(doc1.key));
|
|
// Doc2 is only in the limbo target, so is tracked as a limbo document
|
|
XCTAssertTrue(limboDocChanges.contains(doc2.key));
|
|
// Doc3 is only in the non-limbo target, therefore not tracked as limbo
|
|
XCTAssertFalse(limboDocChanges.contains(doc3.key));
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|