-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix deserialization of missing fields in gRPC
Mockingbird wrong handled message if sender didn't fill a field in proto3 syntax. The right behavior is using default value for missing fields, but mockingbird returned error.
- Loading branch information
1 parent
299a5d6
commit bd75f54
Showing
7 changed files
with
234 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
backend/mockingbird/src/test/resources/not_optional_proto2.proto
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
syntax = "proto2"; | ||
|
||
enum Bar { | ||
BAR_ZERO = 0; | ||
BAR_ONE = 1; | ||
} | ||
|
||
message Foo { | ||
required string field1 = 1; | ||
required Bar field2 = 3; | ||
} |
11 changes: 11 additions & 0 deletions
11
backend/mockingbird/src/test/resources/not_optional_proto3.proto
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
syntax = "proto3"; | ||
|
||
enum Bar { | ||
BAR_ZERO = 0; | ||
BAR_ONE = 1; | ||
} | ||
|
||
message Foo { | ||
string field1 = 1; | ||
Bar field2 = 3; | ||
} |
11 changes: 11 additions & 0 deletions
11
backend/mockingbird/src/test/resources/optional_proto2.proto
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
syntax = "proto2"; | ||
|
||
enum Bar { | ||
BAR_ZERO = 0; | ||
BAR_ONE = 1; | ||
} | ||
|
||
message Foo { | ||
required string field1 = 1; | ||
optional Bar field2 = 3; | ||
} |
11 changes: 11 additions & 0 deletions
11
backend/mockingbird/src/test/resources/optional_proto3.proto
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
syntax = "proto3"; | ||
|
||
enum Bar { | ||
BAR_ZERO = 0; | ||
BAR_ONE = 1; | ||
} | ||
|
||
message Foo { | ||
string field1 = 1; | ||
optional Bar field2 = 3; | ||
} |
164 changes: 164 additions & 0 deletions
164
.../mockingbird/src/test/scala/ru/tinkoff/tcb/protobuf/SerializationOptionalFieldsSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
package ru.tinkoff.tcb.protobuf | ||
|
||
import com.github.os72.protobuf.dynamic.DynamicSchema | ||
import com.google.protobuf.DynamicMessage | ||
import com.google.protobuf.InvalidProtocolBufferException | ||
import com.google.protobuf.util.JsonFormat | ||
import io.circe.* | ||
import zio.test.* | ||
import zio.test.Assertion.* | ||
|
||
import ru.tinkoff.tcb.mockingbird.grpc.GrpcExractor.FromDynamicSchema | ||
import ru.tinkoff.tcb.mockingbird.grpc.GrpcExractor.FromGrpcProtoDefinition | ||
|
||
object SerializationOptionalFieldsSpec extends ZIOSpecDefault { | ||
val msgOptionalFieldAbsent = Array[Byte](0x0a, 0x04, 0x31, 0x71, 0x77, 0x65) | ||
val msgOptionalFieldHasDefaultValue = Array[Byte](0x0a, 0x04, 0x31, 0x71, 0x77, 0x65, 0x18, 0x00) | ||
val msgOptionalFieldHasAnotherValue = Array[Byte](0x0a, 0x04, 0x31, 0x71, 0x77, 0x65, 0x18, 0x01) | ||
val typeName = "Foo" | ||
val printer = JsonFormat.printer().includingDefaultValueFields().preservingProtoFieldNames().sortingMapKeys() | ||
|
||
val optionalSyntax2 = "optional_proto2.proto" | ||
val notOptionalSyntax2 = "not_optional_proto2.proto" | ||
val optionalSyntax3 = "optional_proto3.proto" | ||
val notOptionalSyntax3 = "not_optional_proto3.proto" | ||
|
||
val field1Val = "1qwe" | ||
val field2DefaultVal = "BAR_ZERO" | ||
val field2AnotherVal = "BAR_ONE" | ||
|
||
override def spec: Spec[TestEnvironment & Scope, Any] = | ||
suite("Serialization of optional fields suite")( | ||
test("An optional field in proto2 syntax: the field is absent") { | ||
for { | ||
msg <- parseWithProtoFromResource(optionalSyntax2, msgOptionalFieldAbsent) | ||
obtain <- parseJson(printer.print(msg)) | ||
expected <- parseJson(s"""{ | ||
| "field1": "$field1Val", | ||
| "field2": "$field2DefaultVal" | ||
|}""".stripMargin) | ||
} yield assertTrue(obtain == expected) | ||
}, | ||
test("An optional field in proto2 syntax: the field has default value") { | ||
for { | ||
msg <- parseWithProtoFromResource(optionalSyntax2, msgOptionalFieldHasDefaultValue) | ||
obtain <- parseJson(printer.print(msg)) | ||
expected <- parseJson(s"""{ | ||
| "field1": "$field1Val", | ||
| "field2": "$field2DefaultVal" | ||
|}""".stripMargin) | ||
} yield assertTrue(obtain == expected) | ||
}, | ||
test("An optional field in proto2 syntax: the field has another value") { | ||
for { | ||
msg <- parseWithProtoFromResource(optionalSyntax2, msgOptionalFieldHasAnotherValue) | ||
obtain <- parseJson(printer.print(msg)) | ||
expected <- parseJson(s"""{ | ||
| "field1": "$field1Val", | ||
| "field2": "$field2AnotherVal" | ||
|}""".stripMargin) | ||
} yield assertTrue(obtain == expected) | ||
}, | ||
test("An required field in proto2 syntax: the field is absent") { | ||
for { | ||
result <- parseWithProtoFromResource(notOptionalSyntax2, msgOptionalFieldAbsent).exit | ||
} yield assert(result)(failsWithA[InvalidProtocolBufferException]) | ||
}, | ||
test("An required field in proto2 syntax: the field has default value") { | ||
for { | ||
msg <- parseWithProtoFromResource(notOptionalSyntax2, msgOptionalFieldHasDefaultValue) | ||
obtain <- parseJson(printer.print(msg)) | ||
expected <- parseJson(s"""{ | ||
| "field1": "$field1Val", | ||
| "field2": "$field2DefaultVal" | ||
|}""".stripMargin) | ||
} yield assertTrue(obtain == expected) | ||
}, | ||
test("An required field in proto2 syntax: the field has another value") { | ||
for { | ||
msg <- parseWithProtoFromResource(notOptionalSyntax2, msgOptionalFieldHasAnotherValue) | ||
obtain <- parseJson(printer.print(msg)) | ||
expected <- parseJson(s"""{ | ||
| "field1": "$field1Val", | ||
| "field2": "$field2AnotherVal" | ||
|}""".stripMargin) | ||
} yield assertTrue(obtain == expected) | ||
}, | ||
test("An optional field in proto3 syntax: the field is absent") { | ||
for { | ||
msg <- parseWithProtoFromResource(optionalSyntax3, msgOptionalFieldAbsent) | ||
obtain <- parseJson(printer.print(msg)) | ||
expected <- parseJson(s"""{ | ||
| "field1": "$field1Val" | ||
|}""".stripMargin) | ||
} yield assertTrue(obtain == expected) | ||
}, | ||
test("An optional field in proto3 syntax: the field has default value") { | ||
for { | ||
msg <- parseWithProtoFromResource(optionalSyntax3, msgOptionalFieldHasDefaultValue) | ||
obtain <- parseJson(printer.print(msg)) | ||
expected <- parseJson(s"""{ | ||
| "field1": "$field1Val", | ||
| "field2": "$field2DefaultVal" | ||
|}""".stripMargin) | ||
} yield assertTrue(obtain == expected) | ||
}, | ||
test("An optional field in proto3 syntax: the field has another value") { | ||
for { | ||
msg <- parseWithProtoFromResource(optionalSyntax3, msgOptionalFieldHasAnotherValue) | ||
obtain <- parseJson(printer.print(msg)) | ||
expected <- parseJson(s"""{ | ||
| "field1": "$field1Val", | ||
| "field2": "$field2AnotherVal" | ||
|}""".stripMargin) | ||
} yield assertTrue(obtain == expected) | ||
}, | ||
test("An regular field in proto3 syntax: the field is absent") { | ||
for { | ||
msg <- parseWithProtoFromResource(notOptionalSyntax3, msgOptionalFieldAbsent) | ||
obtain <- parseJson(printer.print(msg)) | ||
expected <- parseJson(s"""{ | ||
| "field1": "$field1Val", | ||
| "field2": "$field2DefaultVal" | ||
|}""".stripMargin) | ||
} yield assertTrue(obtain == expected) | ||
}, | ||
test("An regular field in proto3 syntax: the field has default value") { | ||
for { | ||
msg <- parseWithProtoFromResource(notOptionalSyntax3, msgOptionalFieldHasDefaultValue) | ||
obtain <- parseJson(printer.print(msg)) | ||
expected <- parseJson(s"""{ | ||
| "field1": "$field1Val", | ||
| "field2": "$field2DefaultVal" | ||
|}""".stripMargin) | ||
} yield assertTrue(obtain == expected) | ||
}, | ||
test("An regular field in proto3 syntax: the field has another value") { | ||
for { | ||
msg <- parseWithProtoFromResource(notOptionalSyntax3, msgOptionalFieldHasAnotherValue) | ||
obtain <- parseJson(printer.print(msg)) | ||
expected <- parseJson(s"""{ | ||
| "field1": "$field1Val", | ||
| "field2": "$field2AnotherVal" | ||
|}""".stripMargin) | ||
} yield assertTrue(obtain == expected) | ||
}, | ||
) | ||
|
||
def parseWithProtoFromResource(protoName: String, rawData: Array[Byte]) = | ||
for { | ||
schema <- getSchemaFromResource(protoName) | ||
desc = schema.getMessageDescriptor(typeName) | ||
result <- ZIO.attempt(DynamicMessage.parseFrom(desc, rawData)) | ||
} yield result | ||
|
||
def getSchemaFromResource(name: String) = | ||
for { | ||
bytes <- Utils.getProtoDescriptionFromResource(name) | ||
// We are checking reconstructed schema | ||
schema <- ZIO.attempt(DynamicSchema.parseFrom(bytes).toGrpcProtoDefinition.toDynamicSchema) | ||
} yield schema | ||
|
||
def parseJson(js: String): Task[Json] = | ||
ZIO.fromEither(parser.parse(js)) | ||
} |