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 | 434 |
1 files changed, 402 insertions, 32 deletions
diff --git a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc index 96125f7..a147309 100644 --- a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc +++ b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc @@ -33,18 +33,41 @@ #include <vector> #include "Firestore/Protos/cpp/google/firestore/v1beta1/document.pb.h" +#include "Firestore/Protos/cpp/google/firestore/v1beta1/firestore.pb.h" +#include "Firestore/core/include/firebase/firestore/firestore_errors.h" +#include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/timestamp_internal.h" #include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" #include "google/protobuf/stubs/common.h" #include "google/protobuf/util/message_differencer.h" #include "gtest/gtest.h" +using firebase::Timestamp; +using firebase::TimestampInternal; +using firebase::firestore::FirestoreErrorCode; +using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::Document; +using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldValue; +using firebase::firestore::model::MaybeDocument; +using firebase::firestore::model::NoDocument; using firebase::firestore::model::ObjectValue; +using firebase::firestore::model::SnapshotVersion; using firebase::firestore::remote::Serializer; +using firebase::firestore::testutil::Key; using firebase::firestore::util::Status; +using firebase::firestore::util::StatusOr; using google::protobuf::util::MessageDifferencer; +#define ASSERT_OK(status) ASSERT_TRUE(StatusOk(status)) +#define ASSERT_NOT_OK(status) ASSERT_FALSE(StatusOk(status)) +#define EXPECT_OK(status) EXPECT_TRUE(StatusOk(status)) +#define EXPECT_NOT_OK(status) EXPECT_FALSE(StatusOk(status)) + TEST(Serializer, CanLinkToNanopb) { // This test doesn't actually do anything interesting as far as actually using // nanopb is concerned but that it can run at all is proof that all the @@ -56,8 +79,11 @@ TEST(Serializer, CanLinkToNanopb) { // Fixture for running serializer tests. class SerializerTest : public ::testing::Test { public: - SerializerTest() : serializer(/*DatabaseId("p", "d")*/) { + SerializerTest() : serializer(kDatabaseId) { + msg_diff.ReportDifferencesToString(&message_differences); } + + const DatabaseId kDatabaseId{"p", "d"}; Serializer serializer; void ExpectRoundTrip(const FieldValue& model, @@ -74,22 +100,88 @@ class SerializerTest : public ::testing::Test { ExpectDeserializationRoundTrip(model, proto, type); } + void ExpectRoundTrip( + const DocumentKey& key, + const FieldValue& value, + const SnapshotVersion& update_time, + const google::firestore::v1beta1::BatchGetDocumentsResponse& proto) { + ExpectSerializationRoundTrip(key, value, update_time, proto); + ExpectDeserializationRoundTrip(key, value, update_time, proto); + } + + /** + * Checks the status. Don't use directly; use one of the relevant macros + * instead. eg: + * + * Status good_status = ...; + * ASSERT_OK(good_status); + * + * Status bad_status = ...; + * EXPECT_NOT_OK(bad_status); + */ + testing::AssertionResult StatusOk(const Status& status) { + if (!status.ok()) { + return testing::AssertionFailure() + << "Status should have been ok, but instead contained " + << status.ToString(); + } + return testing::AssertionSuccess(); + } + + template <typename T> + testing::AssertionResult StatusOk(const StatusOr<T>& status) { + return StatusOk(status.status()); + } + + /** + * Ensures that decoding fails with the given status. + * + * @param status the expected (failed) status. Only the code() is verified. + */ + void ExpectFailedStatusDuringDecode(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()); + } + google::firestore::v1beta1::Value ValueProto(nullptr_t) { - std::vector<uint8_t> bytes; - Status status = - serializer.EncodeFieldValue(FieldValue::NullValue(), &bytes); - EXPECT_TRUE(status.ok()); + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::NullValue()); google::firestore::v1beta1::Value proto; bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); return proto; } - google::firestore::v1beta1::Value ValueProto(bool b) { + std::vector<uint8_t> EncodeFieldValue(Serializer* serializer, + const FieldValue& fv) { + std::vector<uint8_t> bytes; + Status status = serializer->EncodeFieldValue(fv, &bytes); + EXPECT_OK(status); + return bytes; + } + + std::vector<uint8_t> EncodeDocument(Serializer* serializer, + const DocumentKey& key, + const FieldValue& value) { std::vector<uint8_t> bytes; Status status = - serializer.EncodeFieldValue(FieldValue::BooleanValue(b), &bytes); - EXPECT_TRUE(status.ok()); + serializer->EncodeDocument(key, value.object_value(), &bytes); + EXPECT_OK(status); + return bytes; + } + + void Mutate(uint8_t* byte, + uint8_t expected_initial_value, + uint8_t new_value) { + ASSERT_EQ(*byte, expected_initial_value); + *byte = new_value; + } + + google::firestore::v1beta1::Value ValueProto(bool b) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::BooleanValue(b)); google::firestore::v1beta1::Value proto; bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); @@ -97,10 +189,8 @@ class SerializerTest : public ::testing::Test { } google::firestore::v1beta1::Value ValueProto(int64_t i) { - std::vector<uint8_t> bytes; - Status status = - serializer.EncodeFieldValue(FieldValue::IntegerValue(i), &bytes); - EXPECT_TRUE(status.ok()); + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::IntegerValue(i)); google::firestore::v1beta1::Value proto; bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); @@ -112,10 +202,17 @@ class SerializerTest : public ::testing::Test { } google::firestore::v1beta1::Value ValueProto(const std::string& s) { - std::vector<uint8_t> bytes; - Status status = - serializer.EncodeFieldValue(FieldValue::StringValue(s), &bytes); - EXPECT_TRUE(status.ok()); + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::StringValue(s)); + google::firestore::v1beta1::Value proto; + bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); + EXPECT_TRUE(ok); + return proto; + } + + google::firestore::v1beta1::Value ValueProto(const Timestamp& ts) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::TimestampValue(ts)); google::firestore::v1beta1::Value proto; bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); @@ -128,13 +225,11 @@ class SerializerTest : public ::testing::Test { const google::firestore::v1beta1::Value& proto, FieldValue::Type type) { EXPECT_EQ(type, model.type()); - std::vector<uint8_t> bytes; - Status status = serializer.EncodeFieldValue(model, &bytes); - EXPECT_TRUE(status.ok()); + std::vector<uint8_t> bytes = EncodeFieldValue(&serializer, model); google::firestore::v1beta1::Value actual_proto; bool ok = actual_proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); - EXPECT_TRUE(MessageDifferencer::Equals(proto, actual_proto)); + EXPECT_TRUE(msg_diff.Compare(proto, actual_proto)) << message_differences; } void ExpectDeserializationRoundTrip( @@ -145,28 +240,87 @@ class SerializerTest : public ::testing::Test { std::vector<uint8_t> bytes(size); bool status = proto.SerializeToArray(bytes.data(), size); EXPECT_TRUE(status); - FieldValue actual_model = serializer.DecodeFieldValue(bytes); + StatusOr<FieldValue> actual_model_status = + serializer.DecodeFieldValue(bytes); + EXPECT_OK(actual_model_status); + FieldValue actual_model = actual_model_status.ValueOrDie(); EXPECT_EQ(type, actual_model.type()); EXPECT_EQ(model, actual_model); } -}; -// TODO(rsgowman): whoops! A previous commit performed approx s/Encodes/Writes/, -// but should not have done so here. Change it back in this file. + void ExpectSerializationRoundTrip( + const DocumentKey& key, + const FieldValue& value, + const SnapshotVersion& update_time, + const google::firestore::v1beta1::BatchGetDocumentsResponse& proto) { + std::vector<uint8_t> bytes = EncodeDocument(&serializer, key, value); + google::firestore::v1beta1::Document actual_proto; + bool ok = actual_proto.ParseFromArray(bytes.data(), bytes.size()); + EXPECT_TRUE(ok); + + // TODO(rsgowman): Right now, we only support Document (and don't support + // NoDocument). That should change in the next PR or so. + EXPECT_TRUE(proto.has_found()); + + // Slight weirdness: When we *encode* a document for sending it to the + // backend, we don't encode the update_time (or create_time). But when we + // *decode* a document, we *do* decode the update_time (though we still + // ignore the create_time). Therefore, we'll verify the update_time + // independently, and then strip it out before comparing the rest. + EXPECT_FALSE(actual_proto.has_create_time()); + EXPECT_EQ(update_time.timestamp().seconds(), + proto.found().update_time().seconds()); + EXPECT_EQ(update_time.timestamp().nanoseconds(), + proto.found().update_time().nanos()); + google::firestore::v1beta1::BatchGetDocumentsResponse proto_copy{proto}; + proto_copy.mutable_found()->clear_update_time(); + proto_copy.mutable_found()->clear_create_time(); + + EXPECT_TRUE(msg_diff.Compare(proto_copy.found(), actual_proto)) + << message_differences; + } + + void ExpectDeserializationRoundTrip( + const DocumentKey& key, + const FieldValue& value, + const SnapshotVersion& update_time, + const google::firestore::v1beta1::BatchGetDocumentsResponse& proto) { + size_t size = proto.ByteSizeLong(); + std::vector<uint8_t> bytes(size); + bool status = proto.SerializeToArray(bytes.data(), size); + EXPECT_TRUE(status); + 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(); + + // TODO(rsgowman): Right now, we only support Document (and don't support + // NoDocument). That should change in the next PR or so. + EXPECT_EQ(MaybeDocument::Type::Document, actual_model->type()); + Document* actual_doc_model = static_cast<Document*>(actual_model.get()); + EXPECT_EQ(key, actual_model->key()); + EXPECT_EQ(value, actual_doc_model->data()); + EXPECT_EQ(update_time, actual_model->version()); + } + + std::string message_differences; + MessageDifferencer msg_diff; +}; -TEST_F(SerializerTest, WritesNull) { +TEST_F(SerializerTest, EncodesNull) { FieldValue model = FieldValue::NullValue(); ExpectRoundTrip(model, ValueProto(nullptr), FieldValue::Type::Null); } -TEST_F(SerializerTest, WritesBool) { +TEST_F(SerializerTest, EncodesBool) { for (bool bool_value : {true, false}) { FieldValue model = FieldValue::BooleanValue(bool_value); ExpectRoundTrip(model, ValueProto(bool_value), FieldValue::Type::Boolean); } } -TEST_F(SerializerTest, WritesIntegers) { +TEST_F(SerializerTest, EncodesIntegers) { std::vector<int64_t> cases{0, 1, -1, @@ -181,7 +335,7 @@ TEST_F(SerializerTest, WritesIntegers) { } } -TEST_F(SerializerTest, WritesString) { +TEST_F(SerializerTest, EncodesString) { std::vector<std::string> cases{ "", "a", @@ -204,7 +358,24 @@ TEST_F(SerializerTest, WritesString) { } } -TEST_F(SerializerTest, WritesEmptyMap) { +TEST_F(SerializerTest, EncodesTimestamps) { + std::vector<Timestamp> cases{ + {}, // epoch + {1234, 0}, + {1234, 999999999}, + {-1234, 0}, + {-1234, 999999999}, + TimestampInternal::Max(), + TimestampInternal::Min(), + }; + + for (const Timestamp& ts_value : cases) { + FieldValue model = FieldValue::TimestampValue(ts_value); + ExpectRoundTrip(model, ValueProto(ts_value), FieldValue::Type::Timestamp); + } +} + +TEST_F(SerializerTest, EncodesEmptyMap) { FieldValue model = FieldValue::ObjectValueFromMap({}); google::firestore::v1beta1::Value proto; @@ -213,7 +384,7 @@ TEST_F(SerializerTest, WritesEmptyMap) { ExpectRoundTrip(model, proto, FieldValue::Type::Object); } -TEST_F(SerializerTest, WritesNestedObjects) { +TEST_F(SerializerTest, EncodesNestedObjects) { FieldValue model = FieldValue::ObjectValueFromMap({ {"b", FieldValue::TrueValue()}, // TODO(rsgowman): add doubles (once they're supported) @@ -258,7 +429,206 @@ TEST_F(SerializerTest, WritesNestedObjects) { ExpectRoundTrip(model, proto, FieldValue::Type::Object); } +TEST_F(SerializerTest, BadNullValue) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::NullValue()); + + // Alter the null value from 0 to 1. + Mutate(&bytes[1], /*expected_initial_value=*/0, /*new_value=*/1); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadBoolValue) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::BooleanValue(true)); + + // Alter the bool value from 1 to 2. (Value values are 0,1) + Mutate(&bytes[1], /*expected_initial_value=*/1, /*new_value=*/2); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadIntegerValue) { + // Encode 'maxint'. This should result in 9 0xff bytes, followed by a 1. + std::vector<uint8_t> bytes = EncodeFieldValue( + &serializer, + FieldValue::IntegerValue(std::numeric_limits<uint64_t>::max())); + ASSERT_EQ(11u, bytes.size()); + for (size_t i = 1; i < bytes.size() - 1; i++) { + ASSERT_EQ(0xff, bytes[i]); + } + + // make the number a bit bigger + Mutate(&bytes[10], /*expected_initial_value=*/1, /*new_value=*/0xff); + bytes.resize(12); + bytes[11] = 0x7f; + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadStringValue) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::StringValue("a")); + + // Claim that the string length is 5 instead of 1. (The first two bytes are + // used by the encoded tag.) + Mutate(&bytes[2], /*expected_initial_value=*/1, /*new_value=*/5); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadTimestampValue_TooLarge) { + std::vector<uint8_t> bytes = EncodeFieldValue( + &serializer, FieldValue::TimestampValue(TimestampInternal::Max())); + + // Add some time, which should push us above the maximum allowed timestamp. + Mutate(&bytes[4], 0x82, 0x83); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadTimestampValue_TooSmall) { + std::vector<uint8_t> bytes = EncodeFieldValue( + &serializer, FieldValue::TimestampValue(TimestampInternal::Min())); + + // Remove some time, which should push us below the minimum allowed timestamp. + Mutate(&bytes[4], 0x92, 0x91); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadTag) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::NullValue()); + + // The google::firestore::v1beta1::Value value_type oneof currently has tags + // up to 18. For this test, we'll pick a tag that's unlikely to be added in + // the near term but still fits within a uint8_t even when encoded. + // 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); +} + +TEST_F(SerializerTest, TagVarintWiretypeStringMismatch) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::BooleanValue(true)); + + // 0x0a represents a bool value encoded as a string. (We're using a + // boolean_value tag here, but any tag that would be represented by a varint + // would do.) + Mutate(&bytes[0], /*expected_initial_value=*/0x08, /*new_value=*/0x0a); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, TagStringWiretypeVarintMismatch) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::StringValue("foo")); + + // 0x88 represents a string value encoded as a varint. + Mutate(&bytes[0], /*expected_initial_value=*/0x8a, /*new_value=*/0x88); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, IncompleteFieldValue) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::NullValue()); + ASSERT_EQ(2u, bytes.size()); + + // Remove the (null) payload + ASSERT_EQ(0x00, bytes[1]); + bytes.pop_back(); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, IncompleteTag) { + std::vector<uint8_t> bytes; + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, EncodesKey) { + EXPECT_EQ("projects/p/databases/d/documents", serializer.EncodeKey(Key(""))); + EXPECT_EQ("projects/p/databases/d/documents/one/two/three/four", + serializer.EncodeKey(Key("one/two/three/four"))); +} + +TEST_F(SerializerTest, DecodesKey) { + EXPECT_EQ(Key(""), serializer.DecodeKey("projects/p/databases/d/documents")); + EXPECT_EQ(Key("one/two/three/four"), + serializer.DecodeKey( + "projects/p/databases/d/documents/one/two/three/four")); + // Same, but with a leading slash + EXPECT_EQ(Key("one/two/three/four"), + serializer.DecodeKey( + "/projects/p/databases/d/documents/one/two/three/four")); +} + +TEST_F(SerializerTest, BadKey) { + std::vector<std::string> bad_cases{ + "", // empty (and too short) + "projects/p", // too short + "projects/p/databases/d", // too short + "projects/p/databases/d/documents/odd_number_of_local_elements", + "projects_spelled_wrong/p/databases/d/documents", + "projects/p/databases_spelled_wrong/d/documents", + "projects/not_project_p/databases/d/documents", + "projects/p/databases/not_database_d/documents", + "projects/p/databases/d/not_documents", + }; + + for (const std::string& bad_key : bad_cases) { + EXPECT_ANY_THROW(serializer.DecodeKey(bad_key)); + } +} + +TEST_F(SerializerTest, EncodesEmptyDocument) { + DocumentKey key = DocumentKey::FromPathString("path/to/the/doc"); + FieldValue empty_value = FieldValue::ObjectValueFromMap({}); + SnapshotVersion update_time = SnapshotVersion{{1234, 5678}}; + + google::firestore::v1beta1::BatchGetDocumentsResponse proto; + google::firestore::v1beta1::Document* doc_proto = proto.mutable_found(); + doc_proto->set_name(serializer.EncodeKey(key)); + doc_proto->mutable_fields(); + + google::protobuf::Timestamp* update_time_proto = + doc_proto->mutable_update_time(); + update_time_proto->set_seconds(1234); + update_time_proto->set_nanos(5678); + + // Note that 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. + google::protobuf::Timestamp* create_time_proto = + doc_proto->mutable_create_time(); + create_time_proto->set_seconds(8765); + create_time_proto->set_nanos(4321); + + ExpectRoundTrip(key, empty_value, update_time, proto); +} + // TODO(rsgowman): Test [en|de]coding multiple protos into the same output // vector. - -// TODO(rsgowman): Death test for decoding invalid bytes. |