blob: 501075c073ef5d527701d0bce55e2b8b2b20f9cf [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "protocol_core.h"
#include <memory>
#include "cbor.h"
#include "maybe.h"
#include "status_test_support.h"
#include "test_platform.h"
namespace v8_crdtp {
// Test-only. Real-life bindings use UTF8/16 conversions as needed.
template <>
struct ProtocolTypeTraits<std::string> {
static bool Deserialize(DeserializerState* state, std::string* value) {
if (state->tokenizer()->TokenTag() == cbor::CBORTokenTag::STRING8) {
auto cbor_span = state->tokenizer()->GetString8();
value->assign(reinterpret_cast<const char*>(cbor_span.data()),
cbor_span.size());
return true;
}
state->RegisterError(Error::BINDINGS_STRING8_VALUE_EXPECTED);
return false;
}
static void Serialize(const std::string& value, std::vector<uint8_t>* bytes) {
cbor::EncodeString8(
span<uint8_t>(reinterpret_cast<const uint8_t*>(value.data()),
value.size()),
bytes);
}
};
namespace {
using ::testing::Eq;
template <typename TResult, typename TArg>
std::unique_ptr<TResult> RoundtripToType(const TArg& obj) {
std::vector<uint8_t> bytes;
obj.AppendSerialized(&bytes);
StatusOr<std::unique_ptr<TResult>> result =
TResult::ReadFrom(std::move(bytes));
return std::move(result).value();
}
template <typename T>
std::unique_ptr<T> Roundtrip(const T& obj) {
return RoundtripToType<T, T>(obj);
}
// These TestTypeFOO classes below would normally be generated
// by the protocol generator.
class TestTypeBasic : public ProtocolObject<TestTypeBasic> {
public:
TestTypeBasic() = default;
const std::string& GetValue() const { return value_; }
void SetValue(std::string value) { value_ = std::move(value); }
private:
DECLARE_SERIALIZATION_SUPPORT();
std::string value_;
};
// clang-format off
V8_CRDTP_BEGIN_DESERIALIZER(TestTypeBasic)
V8_CRDTP_DESERIALIZE_FIELD("value", value_)
V8_CRDTP_END_DESERIALIZER()
V8_CRDTP_BEGIN_SERIALIZER(TestTypeBasic)
V8_CRDTP_SERIALIZE_FIELD("value", value_);
V8_CRDTP_END_SERIALIZER();
// clang-format on
TEST(ProtocolCoreTest, Basic) {
TestTypeBasic obj1;
obj1.SetValue("foo");
auto obj2 = Roundtrip(obj1);
ASSERT_THAT(obj2, Not(testing::IsNull()));
EXPECT_THAT(obj2->GetValue(), Eq("foo"));
}
TEST(ProtocolCoreTest, FailedToDeserializeTestTypeBasic) {
std::vector<uint8_t> garbage = {'g', 'a', 'r', 'b', 'a', 'g', 'e'};
StatusOr<std::unique_ptr<TestTypeBasic>> result =
TestTypeBasic::ReadFrom(std::move(garbage));
EXPECT_THAT(result.status(), StatusIs(Error::CBOR_INVALID_STRING8, 0));
}
class TestTypeBasicDouble : public ProtocolObject<TestTypeBasicDouble> {
public:
TestTypeBasicDouble() = default;
double GetValue() const { return value_; }
void SetValue(double value) { value_ = value; }
private:
DECLARE_SERIALIZATION_SUPPORT();
double value_;
};
// clang-format off
V8_CRDTP_BEGIN_DESERIALIZER(TestTypeBasicDouble)
V8_CRDTP_DESERIALIZE_FIELD("value", value_)
V8_CRDTP_END_DESERIALIZER()
V8_CRDTP_BEGIN_SERIALIZER(TestTypeBasicDouble)
V8_CRDTP_SERIALIZE_FIELD("value", value_);
V8_CRDTP_END_SERIALIZER();
// clang-format on
TEST(TestBasicDouble, ParserAllowsAllowsDoubleEncodedAsInt) {
// We allow double's encoded as INT32, because this is what a roundtrip via
// JSON would produce.
std::vector<uint8_t> encoded;
crdtp::cbor::EnvelopeEncoder envelope;
envelope.EncodeStart(&encoded);
encoded.push_back(crdtp::cbor::EncodeIndefiniteLengthMapStart());
crdtp::cbor::EncodeString8(crdtp::SpanFrom("value"), &encoded);
crdtp::cbor::EncodeInt32(
42, &encoded); // It's a double field, but we encode an int.
encoded.push_back(crdtp::cbor::EncodeStop());
envelope.EncodeStop(&encoded);
auto obj = TestTypeBasicDouble::ReadFrom(encoded).value();
ASSERT_THAT(obj, Not(testing::IsNull()));
EXPECT_THAT(obj->GetValue(), Eq(42));
}
class TestTypeComposite : public ProtocolObject<TestTypeComposite> {
public:
bool GetBoolField() const { return bool_field_; }
void SetBoolField(bool value) { bool_field_ = value; }
int GetIntField() const { return int_field_; }
void SetIntField(int value) { int_field_ = value; }
double GetDoubleField() const { return double_field_; }
void SetDoubleField(double value) { double_field_ = value; }
const std::string& GetStrField() const { return str_field_; }
void SetStrField(std::string value) { str_field_ = std::move(value); }
const TestTypeBasic* GetTestTypeBasicField() {
return test_type1_field_.get();
}
void SetTestTypeBasicField(std::unique_ptr<TestTypeBasic> value) {
test_type1_field_ = std::move(value);
}
private:
DECLARE_SERIALIZATION_SUPPORT();
bool bool_field_ = false;
int int_field_ = 0;
double double_field_ = 0.0;
std::string str_field_;
std::unique_ptr<TestTypeBasic> test_type1_field_;
};
// clang-format off
V8_CRDTP_BEGIN_DESERIALIZER(TestTypeComposite)
V8_CRDTP_DESERIALIZE_FIELD("bool_field", bool_field_),
V8_CRDTP_DESERIALIZE_FIELD("double_field", double_field_),
V8_CRDTP_DESERIALIZE_FIELD("int_field", int_field_),
V8_CRDTP_DESERIALIZE_FIELD("str_field", str_field_),
V8_CRDTP_DESERIALIZE_FIELD("test_type1_field", test_type1_field_),
V8_CRDTP_END_DESERIALIZER()
V8_CRDTP_BEGIN_SERIALIZER(TestTypeComposite)
V8_CRDTP_SERIALIZE_FIELD("bool_field", bool_field_),
V8_CRDTP_SERIALIZE_FIELD("double_field", double_field_),
V8_CRDTP_SERIALIZE_FIELD("int_field", int_field_),
V8_CRDTP_SERIALIZE_FIELD("str_field", str_field_),
V8_CRDTP_SERIALIZE_FIELD("test_type1_field", test_type1_field_),
V8_CRDTP_END_SERIALIZER();
// clang-format on
TEST(ProtocolCoreTest, Composite) {
TestTypeComposite obj1;
obj1.SetBoolField(true);
obj1.SetIntField(42);
obj1.SetDoubleField(2.718281828);
obj1.SetStrField("bar");
auto val1 = std::make_unique<TestTypeBasic>();
val1->SetValue("bazzzz");
obj1.SetTestTypeBasicField(std::move(val1));
auto obj2 = Roundtrip(obj1);
ASSERT_THAT(obj2, Not(testing::IsNull()));
EXPECT_THAT(obj2->GetBoolField(), Eq(true));
EXPECT_THAT(obj2->GetIntField(), Eq(42));
EXPECT_THAT(obj2->GetDoubleField(), Eq(2.718281828));
EXPECT_THAT(obj2->GetStrField(), Eq("bar"));
EXPECT_THAT(obj2->GetTestTypeBasicField()->GetValue(), Eq("bazzzz"));
}
class CompositeParsingTest : public testing::Test {
public:
CompositeParsingTest() {
TestTypeComposite top;
top.SetIntField(42);
top.SetBoolField(true);
top.SetIntField(42);
top.SetDoubleField(2.718281828);
top.SetStrField("junk");
auto child = std::make_unique<TestTypeBasic>();
child->SetValue("child_value");
top.SetTestTypeBasicField(std::move(child));
// Let's establish that |serialized_| is a properly serialized
// representation of |top|, by checking that it deserializes ok.
top.AppendSerialized(&serialized_);
TestTypeComposite::ReadFrom(serialized_).value();
}
protected:
std::vector<uint8_t> serialized_;
};
TEST_F(CompositeParsingTest, DecodingFailure_CBORTokenizer) {
// Mutates |serialized_| so that it won't parse correctly. In this case,
// we're changing a string value so that it's invalid, making CBORTokenizer
// unhappy.
size_t position =
std::string(reinterpret_cast<const char*>(serialized_.data()),
serialized_.size())
.find("child_value");
EXPECT_GT(position, 0ul);
// We override the byte just before so that it's still a string
// (3 << 5), but the length is encoded in the bytes that follows.
// So, we override that with 0xff (255), which exceeds the length
// of the message and thereby makes the string8 invalid.
--position;
serialized_[position] = 3 << 5 | // major type: STRING
25; // length in encoded in byte that follows.
serialized_[position + 1] = 0xff; // length
auto result = TestTypeComposite::ReadFrom(serialized_);
EXPECT_THAT(result.status(), StatusIs(Error::CBOR_INVALID_STRING8, position));
}
TEST_F(CompositeParsingTest, DecodingFailure_MandatoryFieldMissingShallow) {
// We're changing the string key "int_field" to something else ("lnt_field"),
// so that the mandatory field value won't be found. Unknown fields are
// ignored for compatibility, so that's why this simple technique works here.
size_t position =
std::string(reinterpret_cast<const char*>(serialized_.data()),
serialized_.size())
.find("int_field");
serialized_[position] = 'l'; // Change 'i' to 'l'.
// serialized_.size() - 1 is the STOP character for the entire message,
size_t expected_error_pos = serialized_.size() - 1;
auto result = TestTypeComposite::ReadFrom(serialized_);
EXPECT_THAT(result.status(), StatusIs(Error::BINDINGS_MANDATORY_FIELD_MISSING,
expected_error_pos));
}
TEST_F(CompositeParsingTest, DecodingFailure_MandatoryFieldMissingNested) {
// We're changing the string key "value" to something else ("falue"), so that
// the mandatory field value in TestTypeBasic in the child won't be found.
size_t position =
std::string(reinterpret_cast<const char*>(serialized_.data()),
serialized_.size())
.find("value");
serialized_[position] = 'f'; // Change 'v' to 'f'.
// serialized_.size() - 1 is the STOP character for the enclosing message,
// and serialized_.size() - 2 is the STOP character for TestTypeBasic.
size_t expected_error_pos = serialized_.size() - 2;
auto result = TestTypeComposite::ReadFrom(serialized_);
EXPECT_THAT(result.status(), StatusIs(Error::BINDINGS_MANDATORY_FIELD_MISSING,
expected_error_pos));
}
TEST_F(CompositeParsingTest, DecodingFailure_BoolValueExpected) {
// We're changing the bool value (true) to null; we do this by looking
// for bool_field, and searching from there for TRUE; both TRUE and null
// are just one byte in the serialized buffer, so this swap is convenient.
std::string serialized_view(reinterpret_cast<const char*>(serialized_.data()),
serialized_.size());
size_t position = serialized_view.find("bool_field");
for (; position < serialized_.size(); ++position) {
if (serialized_[position] == crdtp::cbor::EncodeTrue()) {
serialized_[position] = crdtp::cbor::EncodeNull();
break;
}
}
auto result = TestTypeComposite::ReadFrom(serialized_);
EXPECT_THAT(result.status(),
StatusIs(Error::BINDINGS_BOOL_VALUE_EXPECTED, position));
}
class TestTypeArrays : public ProtocolObject<TestTypeArrays> {
public:
const std::vector<int>* GetIntArray() const { return &int_array_; }
void SetIntArray(std::vector<int> value) { int_array_ = std::move(value); }
const std::vector<double>* GetDoubleArray() const { return &double_array_; }
void SetDoubleArray(std::vector<double> value) {
double_array_ = std::move(value);
}
const std::vector<std::string>* GetStrArray() const { return &str_array_; }
void SetStrArray(std::vector<std::string> value) {
str_array_ = std::move(value);
}
const std::vector<std::unique_ptr<TestTypeBasic>>* GetTestTypeBasicArray()
const {
return &test_type_basic_array_;
}
void SetTestTypeBasicArray(
std::vector<std::unique_ptr<TestTypeBasic>> value) {
test_type_basic_array_ = std::move(value);
}
private:
DECLARE_SERIALIZATION_SUPPORT();
std::vector<int> int_array_;
std::vector<double> double_array_;
std::vector<std::string> str_array_;
std::vector<std::unique_ptr<TestTypeBasic>> test_type_basic_array_;
};
// clang-format off
V8_CRDTP_BEGIN_DESERIALIZER(TestTypeArrays)
V8_CRDTP_DESERIALIZE_FIELD("int_array", int_array_),
V8_CRDTP_DESERIALIZE_FIELD("str_array", str_array_),
V8_CRDTP_DESERIALIZE_FIELD("test_type_basic_array", test_type_basic_array_),
V8_CRDTP_END_DESERIALIZER()
V8_CRDTP_BEGIN_SERIALIZER(TestTypeArrays)
V8_CRDTP_SERIALIZE_FIELD("int_array", int_array_),
V8_CRDTP_SERIALIZE_FIELD("str_array", str_array_),
V8_CRDTP_SERIALIZE_FIELD("test_type_basic_array", test_type_basic_array_),
V8_CRDTP_END_SERIALIZER();
// clang-format on
TEST_F(CompositeParsingTest, Arrays) {
TestTypeArrays obj1;
obj1.SetIntArray(std::vector<int>{1, 3, 5, 7});
std::vector<std::string> strs;
strs.emplace_back("foo");
strs.emplace_back(std::string("bar"));
obj1.SetStrArray(std::move(strs));
auto val1 = std::make_unique<TestTypeBasic>();
val1->SetValue("bazzzz");
std::vector<std::unique_ptr<TestTypeBasic>> vec1;
vec1.emplace_back(std::move(val1));
obj1.SetTestTypeBasicArray(std::move(vec1));
auto obj2 = Roundtrip(obj1);
ASSERT_THAT(obj2, Not(testing::IsNull()));
EXPECT_THAT(*obj2->GetIntArray(), testing::ElementsAre(1, 3, 5, 7));
EXPECT_THAT(*obj2->GetStrArray(), testing::ElementsAre("foo", "bar"));
EXPECT_THAT(obj2->GetDoubleArray()->size(), Eq(0ul));
EXPECT_THAT(obj2->GetTestTypeBasicArray()->size(), Eq(1ul));
EXPECT_THAT(obj2->GetTestTypeBasicArray()->front()->GetValue(), Eq("bazzzz"));
}
class TestTypeOptional : public ProtocolObject<TestTypeOptional> {
public:
TestTypeOptional() = default;
bool HasIntField() const { return int_field_.isJust(); }
int GetIntField() const { return int_field_.fromJust(); }
void SetIntField(int value) { int_field_ = value; }
bool HasStrField() { return str_field_.isJust(); }
const std::string& GetStrField() const { return str_field_.fromJust(); }
void SetStrField(std::string value) { str_field_ = std::move(value); }
bool HasTestTypeBasicField() { return test_type_basic_field_.isJust(); }
const TestTypeBasic* GetTestTypeBasicField() const {
return test_type_basic_field_.isJust() ? test_type_basic_field_.fromJust()
: nullptr;
}
void SetTestTypeBasicField(std::unique_ptr<TestTypeBasic> value) {
test_type_basic_field_ = std::move(value);
}
private:
DECLARE_SERIALIZATION_SUPPORT();
Maybe<int> int_field_;
Maybe<std::string> str_field_;
Maybe<TestTypeBasic> test_type_basic_field_;
};
// clang-format off
V8_CRDTP_BEGIN_DESERIALIZER(TestTypeOptional)
V8_CRDTP_DESERIALIZE_FIELD_OPT("int_field", int_field_),
V8_CRDTP_DESERIALIZE_FIELD_OPT("str_field", str_field_),
V8_CRDTP_DESERIALIZE_FIELD_OPT("test_type_basic_field", test_type_basic_field_),
V8_CRDTP_END_DESERIALIZER()
V8_CRDTP_BEGIN_SERIALIZER(TestTypeOptional)
V8_CRDTP_SERIALIZE_FIELD("int_field", int_field_),
V8_CRDTP_SERIALIZE_FIELD("str_field", str_field_),
V8_CRDTP_SERIALIZE_FIELD("test_type_basic_field", test_type_basic_field_),
V8_CRDTP_END_SERIALIZER();
// clang-format on
TEST(ProtocolCoreTest, OptionalAbsent) {
TestTypeOptional obj1;
auto obj2 = Roundtrip(obj1);
ASSERT_THAT(obj2, Not(testing::IsNull()));
EXPECT_THAT(obj2->HasIntField(), Eq(false));
EXPECT_THAT(obj2->HasStrField(), Eq(false));
EXPECT_THAT(obj2->HasTestTypeBasicField(), Eq(false));
}
TEST(ProtocolCoreTest, OptionalPresent) {
TestTypeOptional obj1;
obj1.SetIntField(42);
obj1.SetStrField("foo");
auto val1 = std::make_unique<TestTypeBasic>();
val1->SetValue("bar");
obj1.SetTestTypeBasicField(std::move(val1));
auto obj2 = Roundtrip(obj1);
ASSERT_THAT(obj2, Not(testing::IsNull()));
EXPECT_THAT(obj2->HasIntField(), Eq(true));
EXPECT_THAT(obj2->GetIntField(), Eq(42));
EXPECT_THAT(obj2->HasStrField(), Eq(true));
EXPECT_THAT(obj2->GetStrField(), Eq("foo"));
EXPECT_THAT(obj2->HasTestTypeBasicField(), Eq(true));
EXPECT_THAT(obj2->GetTestTypeBasicField()->GetValue(), Eq("bar"));
}
class TestTypeLazy : public ProtocolObject<TestTypeLazy> {
public:
TestTypeLazy() = default;
const std::string& GetStrField() const { return str_field_; }
void SetStrField(std::string value) { str_field_ = std::move(value); }
const DeferredMessage* deferred_test_type1_field() const {
return test_type1_field_.get();
}
private:
DECLARE_SERIALIZATION_SUPPORT();
std::string str_field_;
std::unique_ptr<DeferredMessage> test_type1_field_;
};
// clang-format off
V8_CRDTP_BEGIN_DESERIALIZER(TestTypeLazy)
V8_CRDTP_DESERIALIZE_FIELD("str_field", str_field_),
V8_CRDTP_DESERIALIZE_FIELD_OPT("test_type1_field", test_type1_field_),
V8_CRDTP_END_DESERIALIZER()
V8_CRDTP_BEGIN_SERIALIZER(TestTypeLazy)
V8_CRDTP_SERIALIZE_FIELD("str_field", str_field_),
V8_CRDTP_SERIALIZE_FIELD("test_type1_field", test_type1_field_),
V8_CRDTP_END_SERIALIZER();
// clang-format on
TEST(ProtocolCoreTest, TestDeferredMessage) {
TestTypeComposite obj1;
obj1.SetStrField("bar");
auto val1 = std::make_unique<TestTypeBasic>();
val1->SetValue("bazzzz");
obj1.SetTestTypeBasicField(std::move(val1));
auto obj2 = RoundtripToType<TestTypeLazy>(obj1);
EXPECT_THAT(obj2->GetStrField(), Eq("bar"));
TestTypeBasic basic_val;
auto deserializer = obj2->deferred_test_type1_field()->MakeDeserializer();
EXPECT_THAT(TestTypeBasic::Deserialize(&deserializer, &basic_val), Eq(true));
EXPECT_THAT(basic_val.GetValue(), Eq("bazzzz"));
StatusOr<std::unique_ptr<TestTypeBasic>> maybe_parsed =
TestTypeBasic::ReadFrom(*obj2->deferred_test_type1_field());
ASSERT_THAT(maybe_parsed.status(), StatusIsOk());
ASSERT_THAT((*maybe_parsed), Not(testing::IsNull()));
ASSERT_EQ((*maybe_parsed)->GetValue(), "bazzzz");
}
} // namespace
} // namespace v8_crdtp