diff --git a/pulsar-client/src/test/java/org/apache/pulsar/client/impl/schema/ProtobufNativeSchemaTest.java b/pulsar-client/src/test/java/org/apache/pulsar/client/impl/schema/ProtobufNativeSchemaTest.java index 59b282dec0c3f..a0ea3c1cc6e95 100644 --- a/pulsar-client/src/test/java/org/apache/pulsar/client/impl/schema/ProtobufNativeSchemaTest.java +++ b/pulsar-client/src/test/java/org/apache/pulsar/client/impl/schema/ProtobufNativeSchemaTest.java @@ -20,14 +20,25 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.protobuf.DescriptorProtos; import com.google.protobuf.Descriptors; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import lombok.extern.slf4j.Slf4j; import org.apache.pulsar.common.schema.SchemaType; +import org.json.JSONException; +import org.json.JSONObject; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; import org.testng.Assert; import org.testng.annotations.Test; @@ -46,6 +57,28 @@ public class ProtobufNativeSchemaTest { + "gBQjUKJW9yZy5hcGFjaGUucHVsc2FyLmNsaWVudC5zY2hlbWEucHJvdG9CDEV4dGVybmFsVGVzdGIGcHJvdG8z\",\"rootMessageT" + "ypeName\":\"proto.TestMessage\",\"rootFileDescriptorName\":\"Test.proto\"}"; + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * Decode a base64-encoded Protobuf FileDescriptorSet into a canonical JSON tree. + * @param b64 a base64 string + * @return a normalized JSON tree that can be compared with JSONAssert, ensuring deterministic + * equality checks regardless of field ordering. + * @throws IllegalArgumentException if b64 isn't valid Base64 + * @throws InvalidProtocolBufferException if the decoded bytes are not a valid FileDescriptorSet + * @throws JSONException if the JSON string is invalid + */ + private static JSONObject fdsToJson(String b64) + throws IllegalArgumentException, InvalidProtocolBufferException, JSONException { + DescriptorProtos.FileDescriptorSet fds = + DescriptorProtos.FileDescriptorSet.parseFrom(Base64.getDecoder().decode(b64)); + String json = JsonFormat.printer() + .includingDefaultValueFields() + .omittingInsignificantWhitespace() + .print(fds); + return new JSONObject(json); + } + @Test public void testEncodeAndDecode() { final String stringFieldValue = "StringFieldValue"; @@ -61,15 +94,49 @@ public void testEncodeAndDecode() { } @Test - public void testSchema() { + public void testSchema() throws Exception { ProtobufNativeSchema protobufSchema = ProtobufNativeSchema.of(org.apache.pulsar.client.schema.proto.Test.TestMessage.class); assertEquals(protobufSchema.getSchemaInfo().getType(), SchemaType.PROTOBUF_NATIVE); assertNotNull(ProtobufNativeSchemaUtils.deserialize(protobufSchema.getSchemaInfo().getSchema())); - assertEquals(new String(protobufSchema.getSchemaInfo().getSchema(), - StandardCharsets.UTF_8), EXPECTED_SCHEMA_JSON); + + // Parse the actual/expected JSON into trees + String actualJson = new String(protobufSchema.getSchemaInfo().getSchema(), StandardCharsets.UTF_8); + JsonNode actualRoot = MAPPER.readTree(actualJson); + JsonNode expectedRoot = MAPPER.readTree(EXPECTED_SCHEMA_JSON); + + // Extract and validate the FileDescriptorSet field for semantic comparison + // (When decoded, Protobuf descriptors can serialize fields in varying orders + // causing hard coded string comparisons to fail) + String fdSetField = "fileDescriptorSet"; + JsonNode actualB64Node = actualRoot.path(fdSetField); + JsonNode expectedB64Node = expectedRoot.path(fdSetField); + Assert.assertFalse(actualB64Node.isMissingNode()); + Assert.assertFalse(expectedB64Node.isMissingNode()); + Assert.assertTrue(actualB64Node.isValueNode()); + Assert.assertTrue(expectedB64Node.isValueNode()); + + // Decode FileDescriptorSets to JSON and compare semantically (order-insensitive) + JSONObject actualFdsObj = fdsToJson(actualB64Node.asText()); + JSONObject expectedFdsObj = fdsToJson(expectedB64Node.asText()); + JSONAssert.assertEquals( + "FileDescriptorSet mismatch: decoded Protobuf descriptors have mismatched schema contents", + expectedFdsObj, + actualFdsObj, + JSONCompareMode.NON_EXTENSIBLE + ); + + // Remove the already verified field and compare remaining schema attributes, order does not matter + ((ObjectNode) actualRoot).remove(fdSetField); + ((ObjectNode) expectedRoot).remove(fdSetField); + JSONAssert.assertEquals( + "Schema metadata mismatch: remaining JSON fields differ after verifying FileDescriptorSet", + MAPPER.writeValueAsString(expectedRoot), + MAPPER.writeValueAsString(actualRoot), + JSONCompareMode.NON_EXTENSIBLE + ); } @Test