/* * 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 "Firestore/Source/Model/FSTMutation.h" #import #import #import #include #import "Firestore/Source/API/FIRFieldValue+Internal.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_mask.h" #include "Firestore/core/src/firebase/firestore/model/field_transform.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/model/transform_operations.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::ArrayTransform; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldMask; using firebase::firestore::model::FieldPath; using firebase::firestore::model::FieldTransform; using firebase::firestore::model::Precondition; using firebase::firestore::model::TransformOperation; @interface FSTMutationTests : XCTestCase @end @implementation FSTMutationTests { FIRTimestamp *_timestamp; } - (void)setUp { _timestamp = [FIRTimestamp timestamp]; } - (void)testAppliesSetsToDocuments { NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"}; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, FSTDocumentStateSynced); FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"bar" : @"bar-value"}); FSTMaybeDocument *setDoc = [set applyToLocalDocument:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; NSDictionary *expectedData = @{@"bar" : @"bar-value"}; XCTAssertEqualObjects( setDoc, FSTTestDoc("collection/key", 0, expectedData, FSTDocumentStateLocalMutations)); } - (void)testAppliesPatchesToDocuments { NSDictionary *docData = @{@"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value"}; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, FSTDocumentStateSynced); FSTMutation *patch = FSTTestPatchMutation("collection/key", @{@"foo.bar" : @"new-bar-value"}, {}); FSTMaybeDocument *patchedDoc = [patch applyToLocalDocument:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; NSDictionary *expectedData = @{@"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value"}; XCTAssertEqualObjects( patchedDoc, FSTTestDoc("collection/key", 0, expectedData, FSTDocumentStateLocalMutations)); } - (void)testDeletesValuesFromTheFieldMask { NSDictionary *docData = @{@"foo" : @{@"bar" : @"bar-value", @"baz" : @"baz-value"}}; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, FSTDocumentStateSynced); DocumentKey key = testutil::Key("collection/key"); FSTMutation *patch = [[FSTPatchMutation alloc] initWithKey:key fieldMask:{testutil::Field("foo.bar")} value:[FSTObjectValue objectValue] precondition:Precondition::None()]; FSTMaybeDocument *patchedDoc = [patch applyToLocalDocument:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; NSDictionary *expectedData = @{@"foo" : @{@"baz" : @"baz-value"}}; XCTAssertEqualObjects( patchedDoc, FSTTestDoc("collection/key", 0, expectedData, FSTDocumentStateLocalMutations)); } - (void)testPatchesPrimitiveValue { NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"}; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, FSTDocumentStateSynced); FSTMutation *patch = FSTTestPatchMutation("collection/key", @{@"foo.bar" : @"new-bar-value"}, {}); FSTMaybeDocument *patchedDoc = [patch applyToLocalDocument:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; NSDictionary *expectedData = @{@"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value"}; XCTAssertEqualObjects( patchedDoc, FSTTestDoc("collection/key", 0, expectedData, FSTDocumentStateLocalMutations)); } - (void)testPatchingDeletedDocumentsDoesNothing { FSTMaybeDocument *baseDoc = FSTTestDeletedDoc("collection/key", 0, NO); FSTMutation *patch = FSTTestPatchMutation("collection/key", @{@"foo" : @"bar"}, {}); FSTMaybeDocument *patchedDoc = [patch applyToLocalDocument:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; XCTAssertEqualObjects(patchedDoc, baseDoc); } - (void)testAppliesLocalServerTimestampTransformToDocuments { NSDictionary *docData = @{@"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value"}; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, FSTDocumentStateSynced); FSTMutation *transform = FSTTestTransformMutation( @"collection/key", @{@"foo.bar" : [FIRFieldValue fieldValueForServerTimestamp]}); FSTMaybeDocument *transformedDoc = [transform applyToLocalDocument:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; // Server timestamps aren't parsed, so we manually insert it. FSTObjectValue *expectedData = FSTTestObjectValue(@{@"foo" : @{@"bar" : @""}, @"baz" : @"baz-value"}); expectedData = [expectedData objectBySettingValue:[FSTServerTimestampValue serverTimestampValueWithLocalWriteTime:_timestamp previousValue:nil] forPath:testutil::Field("foo.bar")]; FSTDocument *expectedDoc = [FSTDocument documentWithData:expectedData key:FSTTestDocKey(@"collection/key") version:testutil::Version(0) state:FSTDocumentStateLocalMutations]; XCTAssertEqualObjects(transformedDoc, expectedDoc); } // NOTE: This is more a test of FSTUserDataConverter code than FSTMutation code but we don't have // unit tests for it currently. We could consider removing this test once we have integration tests. - (void)testCreateArrayUnionTransform { FSTTransformMutation *transform = FSTTestTransformMutation(@"collection/key", @{ @"foo" : [FIRFieldValue fieldValueForArrayUnion:@[ @"tag" ]], @"bar.baz" : [FIRFieldValue fieldValueForArrayUnion:@[ @YES, @{@"nested" : @{@"a" : @[ @1, @2 ]}} ]] }); XCTAssertEqual(transform.fieldTransforms.size(), 2); const FieldTransform &first = transform.fieldTransforms[0]; XCTAssertEqual(first.path(), FieldPath({"foo"})); { std::vector expectedElements{FSTTestFieldValue(@"tag")}; ArrayTransform expected(TransformOperation::Type::ArrayUnion, expectedElements); XCTAssertEqual(static_cast(first.transformation()), expected); } const FieldTransform &second = transform.fieldTransforms[1]; XCTAssertEqual(second.path(), FieldPath({"bar", "baz"})); { std::vector expectedElements { FSTTestFieldValue(@YES), FSTTestFieldValue(@{@"nested" : @{@"a" : @[ @1, @2 ]}}) }; ArrayTransform expected(TransformOperation::Type::ArrayUnion, expectedElements); XCTAssertEqual(static_cast(second.transformation()), expected); } } // NOTE: This is more a test of FSTUserDataConverter code than FSTMutation code but we don't have // unit tests for it currently. We could consider removing this test once we have integration tests. - (void)testCreateArrayRemoveTransform { FSTTransformMutation *transform = FSTTestTransformMutation(@"collection/key", @{ @"foo" : [FIRFieldValue fieldValueForArrayRemove:@[ @"tag" ]], }); XCTAssertEqual(transform.fieldTransforms.size(), 1); const FieldTransform &first = transform.fieldTransforms[0]; XCTAssertEqual(first.path(), FieldPath({"foo"})); { std::vector expectedElements{FSTTestFieldValue(@"tag")}; const ArrayTransform expected(TransformOperation::Type::ArrayRemove, expectedElements); XCTAssertEqual(static_cast(first.transformation()), expected); } } - (void)testAppliesLocalArrayUnionTransformToMissingField { auto baseDoc = @{}; auto transform = @{@"missing" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @2 ]]}; auto expected = @{@"missing" : @[ @1, @2 ]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformToNonArrayField { auto baseDoc = @{@"non-array" : @42}; auto transform = @{@"non-array" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @2 ]]}; auto expected = @{@"non-array" : @[ @1, @2 ]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformWithNonExistingElements { auto baseDoc = @{@"array" : @[ @1, @3 ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @2, @4 ]]}; auto expected = @{@"array" : @[ @1, @3, @2, @4 ]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformWithExistingElements { auto baseDoc = @{@"array" : @[ @1, @3 ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @3 ]]}; auto expected = @{@"array" : @[ @1, @3 ]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformWithDuplicateExistingElements { // Duplicate entries in your existing array should be preserved. auto baseDoc = @{@"array" : @[ @1, @2, @2, @3 ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @2 ]]}; auto expected = @{@"array" : @[ @1, @2, @2, @3 ]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformWithDuplicateUnionElements { // Duplicate entries in your union array should only be added once. auto baseDoc = @{@"array" : @[ @1, @3 ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @2, @2 ]]}; auto expected = @{@"array" : @[ @1, @3, @2 ]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformWithNonPrimitiveElements { // Union nested object values (one existing, one not). auto baseDoc = @{@"array" : @[ @1, @{@"a" : @"b"} ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @{@"a" : @"b"}, @{@"c" : @"d"} ]]}; auto expected = @{@"array" : @[ @1, @{@"a" : @"b"}, @{@"c" : @"d"} ]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformWithPartiallyOverlappingElements { // Union objects that partially overlap an existing object. auto baseDoc = @{@"array" : @[ @1, @{@"a" : @"b", @"c" : @"d"} ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @{@"a" : @"b"}, @{@"c" : @"d"} ]]}; auto expected = @{@"array" : @[ @1, @{@"a" : @"b", @"c" : @"d"}, @{@"a" : @"b"}, @{@"c" : @"d"} ]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } - (void)testAppliesLocalArrayRemoveTransformToMissingField { auto baseDoc = @{}; auto transform = @{@"missing" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @2 ]]}; auto expected = @{@"missing" : @[]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } - (void)testAppliesLocalArrayRemoveTransformToNonArrayField { auto baseDoc = @{@"non-array" : @42}; auto transform = @{@"non-array" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @2 ]]}; auto expected = @{@"non-array" : @[]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } - (void)testAppliesLocalArrayRemoveTransformWithNonExistingElements { auto baseDoc = @{@"array" : @[ @1, @3 ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @2, @4 ]]}; auto expected = @{@"array" : @[ @1, @3 ]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } - (void)testAppliesLocalArrayRemoveTransformWithExistingElements { auto baseDoc = @{@"array" : @[ @1, @2, @3, @4 ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @3 ]]}; auto expected = @{@"array" : @[ @2, @4 ]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } - (void)testAppliesLocalArrayRemoveTransformWithNonPrimitiveElements { // Remove nested object values (one existing, one not). auto baseDoc = @{@"array" : @[ @1, @{@"a" : @"b"} ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @{@"a" : @"b"}, @{@"c" : @"d"} ]]}; auto expected = @{@"array" : @[ @1 ]}; [self transformBaseDoc:baseDoc with:transform expecting:expected]; } // Helper to test a particular transform scenario. - (void)transformBaseDoc:(NSDictionary *)baseData with:(NSDictionary *)transformData expecting:(NSDictionary *)expectedData { FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, baseData, FSTDocumentStateSynced); FSTMutation *transform = FSTTestTransformMutation(@"collection/key", transformData); FSTMaybeDocument *transformedDoc = [transform applyToLocalDocument:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; FSTDocument *expectedDoc = [FSTDocument documentWithData:FSTTestObjectValue(expectedData) key:FSTTestDocKey(@"collection/key") version:testutil::Version(0) state:FSTDocumentStateLocalMutations]; XCTAssertEqualObjects(transformedDoc, expectedDoc); } - (void)testAppliesServerAckedServerTimestampTransformToDocuments { NSDictionary *docData = @{@"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value"}; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, FSTDocumentStateSynced); FSTMutation *transform = FSTTestTransformMutation( @"collection/key", @{@"foo.bar" : [FIRFieldValue fieldValueForServerTimestamp]}); FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:testutil::Version(1) transformResults:@[ [FSTTimestampValue timestampValue:_timestamp] ]]; FSTMaybeDocument *transformedDoc = [transform applyToRemoteDocument:baseDoc mutationResult:mutationResult]; NSDictionary *expectedData = @{@"foo" : @{@"bar" : _timestamp.dateValue}, @"baz" : @"baz-value"}; XCTAssertEqualObjects(transformedDoc, FSTTestDoc("collection/key", 1, expectedData, FSTDocumentStateCommittedMutations)); } - (void)testAppliesServerAckedArrayTransformsToDocuments { NSDictionary *docData = @{@"array_1" : @[ @1, @2 ], @"array_2" : @[ @"a", @"b" ]}; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, FSTDocumentStateSynced); FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @{ @"array_1" : [FIRFieldValue fieldValueForArrayUnion:@[ @2, @3 ]], @"array_2" : [FIRFieldValue fieldValueForArrayRemove:@[ @"a", @"c" ]] }); // Server just sends null transform results for array operations. FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:testutil::Version(1) transformResults:@[ [FSTNullValue nullValue], [FSTNullValue nullValue] ]]; FSTMaybeDocument *transformedDoc = [transform applyToRemoteDocument:baseDoc mutationResult:mutationResult]; NSDictionary *expectedData = @{@"array_1" : @[ @1, @2, @3 ], @"array_2" : @[ @"b" ]}; XCTAssertEqualObjects(transformedDoc, FSTTestDoc("collection/key", 1, expectedData, FSTDocumentStateCommittedMutations)); } - (void)testDeleteDeletes { NSDictionary *docData = @{@"foo" : @"bar"}; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, FSTDocumentStateSynced); FSTMutation *mutation = FSTTestDeleteMutation(@"collection/key"); FSTMaybeDocument *result = [mutation applyToLocalDocument:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; XCTAssertEqualObjects(result, FSTTestDeletedDoc("collection/key", 0, NO)); } - (void)testSetWithMutationResult { NSDictionary *docData = @{@"foo" : @"bar"}; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, FSTDocumentStateSynced); FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"foo" : @"new-bar"}); FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:testutil::Version(4) transformResults:nil]; FSTMaybeDocument *setDoc = [set applyToRemoteDocument:baseDoc mutationResult:mutationResult]; NSDictionary *expectedData = @{@"foo" : @"new-bar"}; XCTAssertEqualObjects( setDoc, FSTTestDoc("collection/key", 4, expectedData, FSTDocumentStateCommittedMutations)); } - (void)testPatchWithMutationResult { NSDictionary *docData = @{@"foo" : @"bar"}; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, FSTDocumentStateSynced); FSTMutation *patch = FSTTestPatchMutation("collection/key", @{@"foo" : @"new-bar"}, {}); FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:testutil::Version(4) transformResults:nil]; FSTMaybeDocument *patchedDoc = [patch applyToRemoteDocument:baseDoc mutationResult:mutationResult]; NSDictionary *expectedData = @{@"foo" : @"new-bar"}; XCTAssertEqualObjects(patchedDoc, FSTTestDoc("collection/key", 4, expectedData, FSTDocumentStateCommittedMutations)); } #define ASSERT_VERSION_TRANSITION(mutation, base, result, expected) \ do { \ FSTMaybeDocument *actual = [mutation applyToRemoteDocument:base mutationResult:result]; \ XCTAssertEqualObjects(actual, expected); \ } while (0); /** * Tests the transition table documented in FSTMutation.h. */ - (void)testTransitions { FSTDocument *docV3 = FSTTestDoc("collection/key", 3, @{}, FSTDocumentStateSynced); FSTDeletedDocument *deletedV3 = FSTTestDeletedDoc("collection/key", 3, NO); FSTMutation *setMutation = FSTTestSetMutation(@"collection/key", @{}); FSTMutation *patchMutation = FSTTestPatchMutation("collection/key", @{}, {}); FSTMutation *transformMutation = FSTTestTransformMutation(@"collection/key", @{}); FSTMutation *deleteMutation = FSTTestDeleteMutation(@"collection/key"); FSTDeletedDocument *docV7Deleted = FSTTestDeletedDoc("collection/key", 7, YES); FSTDocument *docV7Committed = FSTTestDoc("collection/key", 7, @{}, FSTDocumentStateCommittedMutations); FSTUnknownDocument *docV7Unknown = FSTTestUnknownDoc("collection/key", 7); FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:testutil::Version(7) transformResults:nil]; FSTMutationResult *transformResult = [[FSTMutationResult alloc] initWithVersion:testutil::Version(7) transformResults:@[]]; ASSERT_VERSION_TRANSITION(setMutation, docV3, mutationResult, docV7Committed); ASSERT_VERSION_TRANSITION(setMutation, deletedV3, mutationResult, docV7Committed); ASSERT_VERSION_TRANSITION(setMutation, nil, mutationResult, docV7Committed); ASSERT_VERSION_TRANSITION(patchMutation, docV3, mutationResult, docV7Committed); ASSERT_VERSION_TRANSITION(patchMutation, deletedV3, mutationResult, docV7Unknown); ASSERT_VERSION_TRANSITION(patchMutation, nil, mutationResult, docV7Unknown); ASSERT_VERSION_TRANSITION(transformMutation, docV3, transformResult, docV7Committed); ASSERT_VERSION_TRANSITION(transformMutation, deletedV3, transformResult, docV7Unknown); ASSERT_VERSION_TRANSITION(transformMutation, nil, transformResult, docV7Unknown); ASSERT_VERSION_TRANSITION(deleteMutation, docV3, mutationResult, docV7Deleted); ASSERT_VERSION_TRANSITION(deleteMutation, deletedV3, mutationResult, docV7Deleted); ASSERT_VERSION_TRANSITION(deleteMutation, nil, mutationResult, docV7Deleted); } #undef ASSERT_TRANSITION @end