diff --git a/test/BUILD b/test/BUILD index 657b77f..3c32892 100644 --- a/test/BUILD +++ b/test/BUILD @@ -114,6 +114,7 @@ proto_library( name = "bookstore_proto", testonly = 1, srcs = ["bookstore.proto"], + deps = ["@com_google_protobuf//:wrappers_proto"], ) cc_proto_library( diff --git a/test/bookstore.proto b/test/bookstore.proto index ce73ff1..b7dd8be 100644 --- a/test/bookstore.proto +++ b/test/bookstore.proto @@ -18,6 +18,7 @@ // Test proto for transcoding syntax = "proto3"; package google.grpc.transcoding; +import "google/protobuf/wrappers.proto"; message Biography { int64 year_born = 1; int64 year_died = 2; @@ -73,3 +74,41 @@ message DeleteBookRequest { int64 shelf = 1; int64 book = 2; } +message ScalarBoolMessage { + bool b = 1; +} +message BoolValueMessage { + google.protobuf.BoolValue b = 1; +} +message StringValueMessage { + google.protobuf.StringValue s = 1; +} +message BytesValueMessage { + google.protobuf.BytesValue b = 1; +} +message Int32ValueMessage { + google.protobuf.Int32Value i = 1; +} +message Int64ValueMessage { + google.protobuf.Int64Value i = 1; +} +message UInt32ValueMessage { + google.protobuf.UInt32Value u = 1; +} +message UInt64ValueMessage { + google.protobuf.UInt64Value u = 1; +} +message FloatValueMessage { + google.protobuf.FloatValue f = 1; +} +message DoubleValueMessage { + google.protobuf.DoubleValue d = 1; +} +message FieldNamingMessage { + string snake_case_field = 1; + string custom_field = 2 [json_name = "myCustomName"]; +} +message FloatDoubleMessage { + float f = 1; + double d = 2; +} diff --git a/test/json_request_translator_test.cc b/test/json_request_translator_test.cc index f3d9a91..4bb43db 100644 --- a/test/json_request_translator_test.cc +++ b/test/json_request_translator_test.cc @@ -794,6 +794,283 @@ TEST_F(JsonRequestTranslatorTest, StreamingErrorNotAnArray) { absl::StatusCode::kInvalidArgument)); } +// Scalar bool fields accept string values. +// The proto JSON spec requires bool fields to only accept JSON true/false +// literals, but the transcoder also accepts string values like "true", "yes", +// "false", "no" (case-insensitive). +TEST_F(JsonRequestTranslatorTest, ScalarBoolAcceptsStringTrue) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("ScalarBoolMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"b": "true"})", "b: true"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, ScalarBoolAcceptsStringFalse) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("ScalarBoolMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"b": "false"})", "b: false"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, ScalarBoolAcceptsStringYes) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("ScalarBoolMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"b": "yes"})", "b: true"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, ScalarBoolAcceptsStringNo) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("ScalarBoolMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"b": "no"})", "b: false"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, ScalarBoolAcceptsStringCaseInsensitive) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("ScalarBoolMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"b": "TrUe"})", "b: true"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, ScalarBoolAcceptsStringNOUpperCase) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("ScalarBoolMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"b": "NO"})", "b: false"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +// Wrapper types accept the object/message form {"value": ...}. +// The proto JSON spec requires wrapper types (e.g. google.protobuf.BoolValue) +// to be represented as their underlying scalar, not as {"value": }. + +TEST_F(JsonRequestTranslatorTest, BoolValueAcceptsObjectFormTrue) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("BoolValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"b": {"value": true}})", "b { value: true }"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, BoolValueAcceptsObjectFormFalse) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("BoolValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"b": {"value": false}})", "b { value: false }"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, StringValueAcceptsObjectForm) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("StringValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"s": {"value": "oh no"}})", R"(s { value: "oh no" })"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, BytesValueAcceptsObjectForm) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("BytesValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"b": {"value": "dGVzdA=="}})", R"(b { value: "test" })"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, Int32ValueAcceptsObjectForm) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("Int32ValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"i": {"value": 42}})", "i { value: 42 }"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, Int64ValueAcceptsObjectForm) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("Int64ValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"i": {"value": "42"}})", "i { value: 42 }"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, UInt32ValueAcceptsObjectForm) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("UInt32ValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"u": {"value": 42}})", "u { value: 42 }"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, UInt64ValueAcceptsObjectForm) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("UInt64ValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"u": {"value": "42"}})", "u { value: 42 }"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, FloatValueAcceptsObjectForm) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("FloatValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"f": {"value": 3.14}})", "f { value: 3.14 }"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, DoubleValueAcceptsObjectForm) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("DoubleValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"d": {"value": 2.718}})", "d { value: 2.718 }"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +// Behaviors 1+2 combined: wrapper types in object form also accept string +// bool values. +TEST_F(JsonRequestTranslatorTest, BoolValueObjectFormAcceptsStringYes) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("BoolValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"b": {"value": "yes"}})", "b { value: true }"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +TEST_F(JsonRequestTranslatorTest, BoolValueObjectFormAcceptsStringNo) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("BoolValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"b": {"value": "no"}})", "b { value: false }"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +// Extra keys alongside "value" are silently ignored. +TEST_F(JsonRequestTranslatorTest, BoolValueObjectFormExtraKeysIgnored) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("BoolValueMessage"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"b": {"value": true, "extra": "ignored"}})", + "b { value: true }"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +// When a wrapper type field receives an object with NO "value" key, the +// transcoder correctly rejects it with INVALID_ARGUMENT. +TEST_F(JsonRequestTranslatorTest, BoolValueRejectsArbitraryObject) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("BoolValueMessage"); + Build(); + AddChunk(R"({"b": {"foo": "bar", "baz": 123}})"); + Finish(); + EXPECT_TRUE(Tester().ExpectNone()); + EXPECT_TRUE( + Tester().ExpectStatusEq(absl::StatusCode::kInvalidArgument)); +} + +TEST_F(JsonRequestTranslatorTest, StringValueRejectsArbitraryObject) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("StringValueMessage"); + Build(); + AddChunk(R"({"s": {"name": "test", "count": 42}})"); + Finish(); + EXPECT_TRUE(Tester().ExpectNone()); + EXPECT_TRUE( + Tester().ExpectStatusEq(absl::StatusCode::kInvalidArgument)); +} + +// Body presence detection with variable bindings (query parameters). +// When a JSON body and a query parameter target the same field, the weaver's +// CollisionCheck determines which value wins. CollisionCheck compares the raw +// JSON key name against the binding's proto field name (Field::name()). This +// means collision is only detected when the body uses the proto field name, not +// the json_name. + +// Proto field name in body → collision detected → body wins. +TEST_F(JsonRequestTranslatorTest, BodyProtoNameWinsOverBinding) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("FieldNamingMessage"); + AddVariableBinding("snake_case_field", "query"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"snake_case_field": "body"})", + R"(snake_case_field: "body")"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +// json_name (camelCase) in body → collision NOT detected → binding is weaved +// after body → query param silently overrides body value. +TEST_F(JsonRequestTranslatorTest, BodyJsonNameOverriddenByBinding) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("FieldNamingMessage"); + AddVariableBinding("snake_case_field", "query"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"snakeCaseField": "body"})", + R"(snake_case_field: "query")"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +// Custom json_name in body → collision NOT detected → binding overrides. +TEST_F(JsonRequestTranslatorTest, BodyCustomJsonNameOverriddenByBinding) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("FieldNamingMessage"); + AddVariableBinding("custom_field", "query"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"myCustomName": "body"})", + R"(custom_field: "query")"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +// Empty string body value using proto name → collision detected → body wins +// (default value is still considered present). +TEST_F(JsonRequestTranslatorTest, BodyEmptyStringWinsOverBinding) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("FieldNamingMessage"); + AddVariableBinding("snake_case_field", "query"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"snake_case_field": ""})", + R"(snake_case_field: "")"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + +// Null body value → RenderNull does not call CollisionCheck → binding survives +// and is weaved → query param wins. +TEST_F(JsonRequestTranslatorTest, BodyNullAllowsBindingToWin) { + LoadService("bookstore_service.pb.txt"); + SetMessageType("FieldNamingMessage"); + AddVariableBinding("snake_case_field", "query"); + TranslationTestCase tc(false); + tc.AddMessage(R"({"snake_case_field": null})", + R"(snake_case_field: "query")"); + tc.Build(); + EXPECT_TRUE((RunTest(1, 1.0, &tc))); +} + } // namespace } // namespace testing } // namespace transcoding diff --git a/test/response_to_json_translator_test.cc b/test/response_to_json_translator_test.cc index 2b58f1f..74f8454 100644 --- a/test/response_to_json_translator_test.cc +++ b/test/response_to_json_translator_test.cc @@ -1110,6 +1110,73 @@ TEST_F(ResponseToJsonTranslatorTest, IncompleteFrame) { "Incomplete gRPC frame expected size: 5 actual size: 1"); } +// Float and double fields set to integer values are serialized as integers in +// JSON (e.g. 100, not 100.0). This uses direct string matching because +// ExpectJsonObjectEq normalizes numbers and would treat 100 and 100.0 as equal. +TEST_F(ResponseToJsonTranslatorTest, FloatIntegerValueNoDecimalPoint) { + ::google::api::Service service; + ASSERT_TRUE( + transcoding::testing::LoadService("bookstore_service.pb.txt", &service)); + TypeHelper type_helper(service.types(), service.enums()); + + TestZeroCopyInputStream input_stream; + ResponseToJsonTranslator translator( + type_helper.Resolver(), "type.googleapis.com/FloatDoubleMessage", false, + &input_stream); + + input_stream.AddChunk( + GenerateGrpcMessage(R"(f: 100 d: 1000)")); + input_stream.Finish(); + + std::string message; + EXPECT_TRUE(translator.NextMessage(&message)); + EXPECT_NE(std::string::npos, message.find("100")); + EXPECT_NE(std::string::npos, message.find("1000")); + EXPECT_EQ(std::string::npos, message.find("100.0")); + EXPECT_EQ(std::string::npos, message.find("1000.0")); +} + +TEST_F(ResponseToJsonTranslatorTest, DoubleIntegerValueNoDecimalPoint) { + ::google::api::Service service; + ASSERT_TRUE( + transcoding::testing::LoadService("bookstore_service.pb.txt", &service)); + TypeHelper type_helper(service.types(), service.enums()); + + TestZeroCopyInputStream input_stream; + ResponseToJsonTranslator translator( + type_helper.Resolver(), "type.googleapis.com/FloatDoubleMessage", false, + &input_stream); + + input_stream.AddChunk( + GenerateGrpcMessage(R"(d: 42)")); + input_stream.Finish(); + + std::string message; + EXPECT_TRUE(translator.NextMessage(&message)); + EXPECT_NE(std::string::npos, message.find("42")); + EXPECT_EQ(std::string::npos, message.find("42.0")); +} + +TEST_F(ResponseToJsonTranslatorTest, FloatDoubleNonIntegerValueHasDecimal) { + ::google::api::Service service; + ASSERT_TRUE( + transcoding::testing::LoadService("bookstore_service.pb.txt", &service)); + TypeHelper type_helper(service.types(), service.enums()); + + TestZeroCopyInputStream input_stream; + ResponseToJsonTranslator translator( + type_helper.Resolver(), "type.googleapis.com/FloatDoubleMessage", false, + &input_stream); + + input_stream.AddChunk( + GenerateGrpcMessage(R"(f: 3.14 d: 2.718)")); + input_stream.Finish(); + + std::string message; + EXPECT_TRUE(translator.NextMessage(&message)); + EXPECT_NE(std::string::npos, message.find(".")); +} + } // namespace } // namespace testing } // namespace transcoding diff --git a/test/testdata/bookstore_service.pb.txt b/test/testdata/bookstore_service.pb.txt index dd00e94..d253ec1 100644 --- a/test/testdata/bookstore_service.pb.txt +++ b/test/testdata/bookstore_service.pb.txt @@ -455,6 +455,277 @@ types { file_name: "wrappers.proto" } } +types { + name: "google.protobuf.StringValue" + fields { + kind: TYPE_STRING + cardinality: CARDINALITY_REQUIRED + number: 1 + name: "value" + json_name: "value" + } + source_context { + file_name: "wrappers.proto" + } +} +types { + name: "google.protobuf.BytesValue" + fields { + kind: TYPE_BYTES + cardinality: CARDINALITY_REQUIRED + number: 1 + name: "value" + json_name: "value" + } + source_context { + file_name: "wrappers.proto" + } +} +types { + name: "google.protobuf.Int32Value" + fields { + kind: TYPE_INT32 + cardinality: CARDINALITY_REQUIRED + number: 1 + name: "value" + json_name: "value" + } + source_context { + file_name: "wrappers.proto" + } +} +types { + name: "google.protobuf.Int64Value" + fields { + kind: TYPE_INT64 + cardinality: CARDINALITY_REQUIRED + number: 1 + name: "value" + json_name: "value" + } + source_context { + file_name: "wrappers.proto" + } +} +types { + name: "google.protobuf.UInt32Value" + fields { + kind: TYPE_UINT32 + cardinality: CARDINALITY_REQUIRED + number: 1 + name: "value" + json_name: "value" + } + source_context { + file_name: "wrappers.proto" + } +} +types { + name: "google.protobuf.UInt64Value" + fields { + kind: TYPE_UINT64 + cardinality: CARDINALITY_REQUIRED + number: 1 + name: "value" + json_name: "value" + } + source_context { + file_name: "wrappers.proto" + } +} +types { + name: "google.protobuf.FloatValue" + fields { + kind: TYPE_FLOAT + cardinality: CARDINALITY_REQUIRED + number: 1 + name: "value" + json_name: "value" + } + source_context { + file_name: "wrappers.proto" + } +} +types { + name: "google.protobuf.DoubleValue" + fields { + kind: TYPE_DOUBLE + cardinality: CARDINALITY_REQUIRED + number: 1 + name: "value" + json_name: "value" + } + source_context { + file_name: "wrappers.proto" + } +} +types { + name: "ScalarBoolMessage" + fields { + kind: TYPE_BOOL + cardinality: CARDINALITY_OPTIONAL + number: 1 + name: "b" + json_name: "b" + } + source_context { + } +} +types { + name: "BoolValueMessage" + fields { + kind: TYPE_MESSAGE + cardinality: CARDINALITY_OPTIONAL + number: 1 + name: "b" + type_url: "type.googleapis.com/google.protobuf.BoolValue" + json_name: "b" + } + source_context { + } +} +types { + name: "StringValueMessage" + fields { + kind: TYPE_MESSAGE + cardinality: CARDINALITY_OPTIONAL + number: 1 + name: "s" + type_url: "type.googleapis.com/google.protobuf.StringValue" + json_name: "s" + } + source_context { + } +} +types { + name: "BytesValueMessage" + fields { + kind: TYPE_MESSAGE + cardinality: CARDINALITY_OPTIONAL + number: 1 + name: "b" + type_url: "type.googleapis.com/google.protobuf.BytesValue" + json_name: "b" + } + source_context { + } +} +types { + name: "Int32ValueMessage" + fields { + kind: TYPE_MESSAGE + cardinality: CARDINALITY_OPTIONAL + number: 1 + name: "i" + type_url: "type.googleapis.com/google.protobuf.Int32Value" + json_name: "i" + } + source_context { + } +} +types { + name: "Int64ValueMessage" + fields { + kind: TYPE_MESSAGE + cardinality: CARDINALITY_OPTIONAL + number: 1 + name: "i" + type_url: "type.googleapis.com/google.protobuf.Int64Value" + json_name: "i" + } + source_context { + } +} +types { + name: "UInt32ValueMessage" + fields { + kind: TYPE_MESSAGE + cardinality: CARDINALITY_OPTIONAL + number: 1 + name: "u" + type_url: "type.googleapis.com/google.protobuf.UInt32Value" + json_name: "u" + } + source_context { + } +} +types { + name: "UInt64ValueMessage" + fields { + kind: TYPE_MESSAGE + cardinality: CARDINALITY_OPTIONAL + number: 1 + name: "u" + type_url: "type.googleapis.com/google.protobuf.UInt64Value" + json_name: "u" + } + source_context { + } +} +types { + name: "FloatValueMessage" + fields { + kind: TYPE_MESSAGE + cardinality: CARDINALITY_OPTIONAL + number: 1 + name: "f" + type_url: "type.googleapis.com/google.protobuf.FloatValue" + json_name: "f" + } + source_context { + } +} +types { + name: "DoubleValueMessage" + fields { + kind: TYPE_MESSAGE + cardinality: CARDINALITY_OPTIONAL + number: 1 + name: "d" + type_url: "type.googleapis.com/google.protobuf.DoubleValue" + json_name: "d" + } + source_context { + } +} +types { + name: "FieldNamingMessage" + fields { + kind: TYPE_STRING + cardinality: CARDINALITY_OPTIONAL + number: 1 + name: "snake_case_field" + json_name: "snakeCaseField" + } + fields { + kind: TYPE_STRING + cardinality: CARDINALITY_OPTIONAL + number: 2 + name: "custom_field" + json_name: "myCustomName" + } + source_context { + } +} +types { + name: "FloatDoubleMessage" + fields { + kind: TYPE_FLOAT + cardinality: CARDINALITY_OPTIONAL + number: 1 + name: "f" + json_name: "f" + } + fields { + kind: TYPE_DOUBLE + cardinality: CARDINALITY_OPTIONAL + number: 2 + name: "d" + json_name: "d" + } + source_context { + } +} enums { name: "google.protobuf.NullValue" enumvalue {