diff options
Diffstat (limited to 'Firestore/core/test/firebase/firestore/remote/serializer_test.cc')
-rw-r--r-- | Firestore/core/test/firebase/firestore/remote/serializer_test.cc | 241 |
1 files changed, 214 insertions, 27 deletions
diff --git a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc index ba9ea47..1125fb4 100644 --- a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc +++ b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc @@ -146,13 +146,21 @@ class SerializerTest : public ::testing::Test { * * @param status the expected (failed) status. Only the code() is verified. */ - void ExpectFailedStatusDuringDecode(Status status, - const std::vector<uint8_t>& bytes) { + void ExpectFailedStatusDuringFieldValueDecode( + Status status, const std::vector<uint8_t>& bytes) { StatusOr<FieldValue> bad_status = serializer.DecodeFieldValue(bytes); ASSERT_NOT_OK(bad_status); EXPECT_EQ(status.code(), bad_status.status().code()); } + void ExpectFailedStatusDuringMaybeDocumentDecode( + Status status, const std::vector<uint8_t>& bytes) { + StatusOr<std::unique_ptr<MaybeDocument>> bad_status = + serializer.DecodeMaybeDocument(bytes); + ASSERT_NOT_OK(bad_status); + EXPECT_EQ(status.code(), bad_status.status().code()); + } + v1beta1::Value ValueProto(nullptr_t) { std::vector<uint8_t> bytes = EncodeFieldValue(&serializer, FieldValue::NullValue()); @@ -230,20 +238,21 @@ class SerializerTest : public ::testing::Test { /** * Creates entries in the proto that we don't care about. * - * We ignore create time in our serializer. We never set it, and never read it - * (other than to throw it away). But the server could (and probably does) set - * it, so we need to be able to discard it properly. The ExpectRoundTrip deals - * with this asymmetry. + * We ignore certain fields in our serializer. We never set them, and never + * read them (other than to throw them away). But the server could (and + * probably does) set them, so we need to be able to discard them properly. + * The ExpectRoundTrip deals with this asymmetry. * * This method adds these ignored fields to the proto. */ void TouchIgnoredBatchGetDocumentsResponseFields( v1beta1::BatchGetDocumentsResponse* proto) { + proto->set_transaction("random bytes"); + // TODO(rsgowman): This method currently assumes that this is a 'found' // document. We (probably) will need to adjust this to work with NoDocuments // too. v1beta1::Document* doc_proto = proto->mutable_found(); - google::protobuf::Timestamp* create_time_proto = doc_proto->mutable_create_time(); create_time_proto->set_seconds(8765); @@ -465,6 +474,63 @@ TEST_F(SerializerTest, EncodesNestedObjects) { ExpectRoundTrip(model, proto, FieldValue::Type::Object); } +TEST_F(SerializerTest, EncodesFieldValuesWithRepeatedEntries) { + // Technically, serialized Value protos can contain multiple values. (The last + // one "wins".) However, well-behaved proto emitters (such as libprotobuf) + // won't generate that, so to test, we either need to use hand-crafted, raw + // bytes or use a proto message that's *almost* the same as the real one, such + // that when it's encoded, you can generate these repeated fields. (This is + // how libprotobuf tests itself.) + // + // Using libprotobuf for this purpose is mildly inconvenient for us, since we + // don't run protoc as part of the build process, so we'd need to either add + // these fake messages to our protos tree (Protos/testprotos?) and then check + // in the results (which isn't great when writing new tests). Fortunately, we + // have another alternative: nanopb. + // + // So we'll create a nanopb struct that *looks* like + // google_firestore_v1beta1_Value, and then populate and serialize it using + // the normal nanopb mechanisms. This should give us a wire-compatible Value + // message, but with multiple values set. + + // Copy of the real one (from the nanopb generated document.pb.h), but with + // only boolean_value and integer_value. + struct google_firestore_v1beta1_Value_Fake { + bool boolean_value; + int64_t integer_value; + }; + + // Copy of the real one (from the nanopb generated document.pb.c), but with + // only boolean_value and integer_value. + const pb_field_t google_firestore_v1beta1_Value_fields_Fake[3] = { + PB_FIELD(1, BOOL, SINGULAR, STATIC, FIRST, + google_firestore_v1beta1_Value_Fake, boolean_value, + boolean_value, 0), + PB_FIELD(2, INT64, SINGULAR, STATIC, OTHER, + google_firestore_v1beta1_Value_Fake, integer_value, + boolean_value, 0), + PB_LAST_FIELD, + }; + + // Craft the bytes. boolean_value has a smaller tag, so it'll get encoded + // first. Implying integer_value should "win". + google_firestore_v1beta1_Value_Fake crafty_value{false, int64_t{42}}; + std::vector<uint8_t> bytes(128); + pb_ostream_t stream = pb_ostream_from_buffer(bytes.data(), bytes.size()); + pb_encode(&stream, google_firestore_v1beta1_Value_fields_Fake, &crafty_value); + bytes.resize(stream.bytes_written); + + // Decode the bytes into the model + StatusOr<FieldValue> actual_model_status = serializer.DecodeFieldValue(bytes); + EXPECT_OK(actual_model_status); + FieldValue actual_model = actual_model_status.ValueOrDie(); + + // Ensure the decoded model is as expected. + FieldValue expected_model = FieldValue::IntegerValue(42); + EXPECT_EQ(FieldValue::Type::Integer, actual_model.type()); + EXPECT_EQ(expected_model, actual_model); +} + TEST_F(SerializerTest, BadNullValue) { std::vector<uint8_t> bytes = EncodeFieldValue(&serializer, FieldValue::NullValue()); @@ -472,7 +538,7 @@ TEST_F(SerializerTest, BadNullValue) { // Alter the null value from 0 to 1. Mutate(&bytes[1], /*expected_initial_value=*/0, /*new_value=*/1); - ExpectFailedStatusDuringDecode( + ExpectFailedStatusDuringFieldValueDecode( Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); } @@ -483,7 +549,7 @@ TEST_F(SerializerTest, BadBoolValue) { // Alter the bool value from 1 to 2. (Value values are 0,1) Mutate(&bytes[1], /*expected_initial_value=*/1, /*new_value=*/2); - ExpectFailedStatusDuringDecode( + ExpectFailedStatusDuringFieldValueDecode( Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); } @@ -502,7 +568,7 @@ TEST_F(SerializerTest, BadIntegerValue) { bytes.resize(12); bytes[11] = 0x7f; - ExpectFailedStatusDuringDecode( + ExpectFailedStatusDuringFieldValueDecode( Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); } @@ -514,7 +580,7 @@ TEST_F(SerializerTest, BadStringValue) { // used by the encoded tag.) Mutate(&bytes[2], /*expected_initial_value=*/1, /*new_value=*/5); - ExpectFailedStatusDuringDecode( + ExpectFailedStatusDuringFieldValueDecode( Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); } @@ -525,7 +591,7 @@ TEST_F(SerializerTest, BadTimestampValue_TooLarge) { // Add some time, which should push us above the maximum allowed timestamp. Mutate(&bytes[4], 0x82, 0x83); - ExpectFailedStatusDuringDecode( + ExpectFailedStatusDuringFieldValueDecode( Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); } @@ -536,11 +602,16 @@ TEST_F(SerializerTest, BadTimestampValue_TooSmall) { // Remove some time, which should push us below the minimum allowed timestamp. Mutate(&bytes[4], 0x92, 0x91); - ExpectFailedStatusDuringDecode( + ExpectFailedStatusDuringFieldValueDecode( Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); } -TEST_F(SerializerTest, BadTag) { +TEST_F(SerializerTest, BadFieldValueTagAndNoOtherTagPresent) { + // A bad tag should be ignored. But if there are *no* valid tags, then we + // don't know the type of the FieldValue. Although it might be reasonable to + // assume some sort of default type in this situation, we've decided to fail + // the deserialization process in this case instead. + std::vector<uint8_t> bytes = EncodeFieldValue(&serializer, FieldValue::NullValue()); @@ -550,15 +621,57 @@ TEST_F(SerializerTest, BadTag) { // Specifically 31. 0xf8 represents field number 31 encoded as a varint. Mutate(&bytes[0], /*expected_initial_value=*/0x58, /*new_value=*/0xf8); - // TODO(rsgowman): The behaviour is *temporarily* slightly different during - // development; this will cause a failed assertion rather than a failed - // status. Remove this EXPECT_ANY_THROW statement (and reenable the - // following commented out statement) once the corresponding assert has been - // removed from serializer.cc. - EXPECT_ANY_THROW(ExpectFailedStatusDuringDecode( - Status(FirestoreErrorCode::DataLoss, "ignored"), bytes)); - // ExpectFailedStatusDuringDecode( - // Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); + ExpectFailedStatusDuringFieldValueDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadFieldValueTagWithOtherValidTagsPresent) { + // A bad tag should be ignored, in which case, we should successfully + // deserialize the rest of the bytes as if it wasn't there. To craft these + // bytes, we'll use the same technique as + // EncodesFieldValuesWithRepeatedEntries (so go read the comments there + // first). + + // Copy of the real one (from the nanopb generated document.pb.h), but with + // only boolean_value and integer_value. + struct google_firestore_v1beta1_Value_Fake { + bool boolean_value; + int64_t integer_value; + }; + + // Copy of the real one (from the nanopb generated document.pb.c), but with + // only boolean_value and integer_value. Also modified such that integer_value + // now has an invalid tag (instead of 2). + const int invalid_tag = 31; + const pb_field_t google_firestore_v1beta1_Value_fields_Fake[3] = { + PB_FIELD(1, BOOL, SINGULAR, STATIC, FIRST, + google_firestore_v1beta1_Value_Fake, boolean_value, + boolean_value, 0), + PB_FIELD(invalid_tag, INT64, SINGULAR, STATIC, OTHER, + google_firestore_v1beta1_Value_Fake, integer_value, + boolean_value, 0), + PB_LAST_FIELD, + }; + + // Craft the bytes. boolean_value has a smaller tag, so it'll get encoded + // first, normally implying integer_value should "win". Except that + // integer_value isn't a valid tag, so it should be ignored here. + google_firestore_v1beta1_Value_Fake crafty_value{true, int64_t{42}}; + std::vector<uint8_t> bytes(128); + pb_ostream_t stream = pb_ostream_from_buffer(bytes.data(), bytes.size()); + pb_encode(&stream, google_firestore_v1beta1_Value_fields_Fake, &crafty_value); + bytes.resize(stream.bytes_written); + + // Decode the bytes into the model + StatusOr<FieldValue> actual_model_status = serializer.DecodeFieldValue(bytes); + Status s = actual_model_status.status(); + EXPECT_OK(actual_model_status); + FieldValue actual_model = actual_model_status.ValueOrDie(); + + // Ensure the decoded model is as expected. + FieldValue expected_model = FieldValue::BooleanValue(true); + EXPECT_EQ(FieldValue::Type::Boolean, actual_model.type()); + EXPECT_EQ(expected_model, actual_model); } TEST_F(SerializerTest, TagVarintWiretypeStringMismatch) { @@ -570,7 +683,7 @@ TEST_F(SerializerTest, TagVarintWiretypeStringMismatch) { // would do.) Mutate(&bytes[0], /*expected_initial_value=*/0x08, /*new_value=*/0x0a); - ExpectFailedStatusDuringDecode( + ExpectFailedStatusDuringFieldValueDecode( Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); } @@ -581,7 +694,7 @@ TEST_F(SerializerTest, TagStringWiretypeVarintMismatch) { // 0x88 represents a string value encoded as a varint. Mutate(&bytes[0], /*expected_initial_value=*/0x8a, /*new_value=*/0x88); - ExpectFailedStatusDuringDecode( + ExpectFailedStatusDuringFieldValueDecode( Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); } @@ -594,13 +707,13 @@ TEST_F(SerializerTest, IncompleteFieldValue) { ASSERT_EQ(0x00, bytes[1]); bytes.pop_back(); - ExpectFailedStatusDuringDecode( + ExpectFailedStatusDuringFieldValueDecode( Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); } TEST_F(SerializerTest, IncompleteTag) { std::vector<uint8_t> bytes; - ExpectFailedStatusDuringDecode( + ExpectFailedStatusDuringFieldValueDecode( Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); } @@ -694,6 +807,69 @@ TEST_F(SerializerTest, EncodesNonEmptyDocument) { ExpectRoundTrip(key, fields, update_time, proto); } +TEST_F(SerializerTest, + BadBatchGetDocumentsResponseTagWithOtherValidTagsPresent) { + // A bad tag should be ignored, in which case, we should successfully + // deserialize the rest of the bytes as if it wasn't there. To craft these + // bytes, we'll use the same technique as + // EncodesFieldValuesWithRepeatedEntries (so go read the comments there + // first). + + // Copy of the real one (from the nanopb generated firestore.pb.h), but with + // only "missing" (a field from the original proto) and an extra_field. + struct google_firestore_v1beta1_BatchGetDocumentsResponse_Fake { + pb_callback_t missing; + int64_t extra_field; + }; + + // Copy of the real one (from the nanopb generated firestore.pb.c), but with + // only missing and an extra_field. Also modified such that extra_field + // now has a tag of 31. + const int invalid_tag = 31; + const pb_field_t + google_firestore_v1beta1_BatchGetDocumentsResponse_fields_Fake[3] = { + PB_FIELD(2, STRING, SINGULAR, CALLBACK, FIRST, + google_firestore_v1beta1_BatchGetDocumentsResponse_Fake, + missing, missing, 0), + PB_FIELD(invalid_tag, INT64, SINGULAR, STATIC, OTHER, + google_firestore_v1beta1_BatchGetDocumentsResponse_Fake, + extra_field, missing, 0), + PB_LAST_FIELD, + }; + + const char* missing_value = "projects/p/databases/d/documents/one/two"; + google_firestore_v1beta1_BatchGetDocumentsResponse_Fake crafty_value; + crafty_value.missing.funcs.encode = + [](pb_ostream_t* stream, const pb_field_t* field, void* const* arg) { + const char* missing_value = static_cast<const char*>(*arg); + if (!pb_encode_tag_for_field(stream, field)) return false; + return pb_encode_string(stream, + reinterpret_cast<const uint8_t*>(missing_value), + strlen(missing_value)); + }; + crafty_value.missing.arg = const_cast<char*>(missing_value); + crafty_value.extra_field = 42; + + std::vector<uint8_t> bytes(128); + pb_ostream_t stream = pb_ostream_from_buffer(bytes.data(), bytes.size()); + pb_encode(&stream, + google_firestore_v1beta1_BatchGetDocumentsResponse_fields_Fake, + &crafty_value); + bytes.resize(stream.bytes_written); + + // Decode the bytes into the model + StatusOr<std::unique_ptr<MaybeDocument>> actual_model_status = + serializer.DecodeMaybeDocument(bytes); + EXPECT_OK(actual_model_status); + std::unique_ptr<MaybeDocument> actual_model = + std::move(actual_model_status).ValueOrDie(); + + // Ensure the decoded model is as expected. + NoDocument expected_model = + NoDocument(Key("one/two"), SnapshotVersion::None()); + EXPECT_EQ(expected_model, *actual_model.get()); +} + TEST_F(SerializerTest, DecodesNoDocument) { // We can't actually *encode* a NoDocument; the method exposed by the // serializer requires both the document key and contents (as an ObjectValue, @@ -713,5 +889,16 @@ TEST_F(SerializerTest, DecodesNoDocument) { ExpectNoDocumentDeserializationRoundTrip(key, read_time, proto); } +TEST_F(SerializerTest, DecodeMaybeDocWithoutFoundOrMissingSetShouldFail) { + v1beta1::BatchGetDocumentsResponse proto; + + std::vector<uint8_t> bytes(proto.ByteSizeLong()); + bool status = proto.SerializeToArray(bytes.data(), bytes.size()); + EXPECT_TRUE(status); + + ExpectFailedStatusDuringMaybeDocumentDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + // TODO(rsgowman): Test [en|de]coding multiple protos into the same output // vector. |