diff --git a/tests/integration/test_dependencies_compatibility_protobuf.py b/tests/integration/test_dependencies_compatibility_protobuf.py index 51f5c558c..47c8b3564 100644 --- a/tests/integration/test_dependencies_compatibility_protobuf.py +++ b/tests/integration/test_dependencies_compatibility_protobuf.py @@ -4,8 +4,10 @@ Copyright (c) 2019 Aiven Ltd See LICENSE for details """ + from karapace.client import Client from karapace.protobuf.kotlin_wrapper import trim_margin +from karapace.typing import Subject from tests.utils import create_subject_name_factory import pytest @@ -669,3 +671,352 @@ async def test_protobuf_customer_update_when_having_references(registry_async_cl res = await registry_async_client.post(f"subjects/{subject_customer}/versions", json=body) assert res.status_code == 200 + + +async def test_protobuf_schema_compatibility_full_path_renaming(registry_async_client: Client) -> None: + subject_dependency = Subject("dependency") + subject_entity = Subject("entity") + + for subject in [subject_dependency, subject_entity]: + res = await registry_async_client.put(f"config/{subject}", json={"compatibility": "FULL"}) + assert res.status_code == 200 + + dependency = """\ + syntax = "proto3"; + package my.awesome.customer.request.v1beta1; + message RequestId { + string request_id = 1; + }\ + """ + + original_full_path = """\ + syntax = "proto3"; + import "my/awesome/customer/request/v1beta1/request_id.proto"; + message MessageRequest { + my.awesome.customer.request.v1beta1.RequestId request_id = 1; + }\ + """ + + evolved_partial_path = """\ + syntax = "proto3"; + import "my/awesome/customer/request/v1beta1/request_id.proto"; + message MessageRequest { + awesome.customer.request.v1beta1.RequestId request_id = 1; + }\ + """ + + # registering the dependency + body = {"schemaType": "PROTOBUF", "schema": dependency} + res = await registry_async_client.post(f"subjects/{subject_dependency}/versions", json=body) + + assert res.status_code == 200 + + # registering the entity + + body = { + "schemaType": "PROTOBUF", + "schema": original_full_path, + "references": [ + { + "name": "place.proto", + "subject": subject_dependency, + "version": -1, + } + ], + } + res = await registry_async_client.post(f"subjects/{subject_entity}/versions", json=body) + assert res.status_code == 200 + previous_id = res.json()["id"] + + # registering the evolved entity + + body = { + "schemaType": "PROTOBUF", + "schema": evolved_partial_path, + "references": [ + { + "name": "place.proto", + "subject": subject_dependency, + "version": -1, + } + ], + } + res = await registry_async_client.post(f"subjects/{subject_entity}/versions", json=body) + assert res.status_code == 200 + assert res.json()["id"] == previous_id + + +async def test_protobuf_schema_compatibility_partial_path_renaming(registry_async_client: Client) -> None: + subject_dependency = Subject("dependency") + subject_entity = Subject("entity") + + for subject in [subject_dependency, subject_entity]: + res = await registry_async_client.put(f"config/{subject}", json={"compatibility": "FULL"}) + assert res.status_code == 200 + + dependency = """\ + syntax = "proto3"; + package my.awesome.customer.request.v1beta1; + message RequestId { + string request_id = 1; + }\ + """ + + original_partial_path = """\ + syntax = "proto3"; + import "my/awesome/customer/request/v1beta1/request_id.proto"; + message MessageRequest { + my.awesome.customer.request.v1beta1.RequestId request_id = 1; + }\ + """ + + evolved_full_path = """\ + syntax = "proto3"; + import "my/awesome/customer/request/v1beta1/request_id.proto"; + message MessageRequest { + awesome.customer.request.v1beta1.RequestId request_id = 1; + }\ + """ + + # registering the dependency + body = {"schemaType": "PROTOBUF", "schema": dependency} + res = await registry_async_client.post(f"subjects/{subject_dependency}/versions", json=body) + + assert res.status_code == 200 + + # registering the entity + + body = { + "schemaType": "PROTOBUF", + "schema": original_partial_path, + "references": [ + { + "name": "place.proto", + "subject": subject_dependency, + "version": -1, + } + ], + } + res = await registry_async_client.post(f"subjects/{subject_entity}/versions", json=body) + assert res.status_code == 200 + previous_id = res.json()["id"] + + # registering the evolved entity + + body = { + "schemaType": "PROTOBUF", + "schema": evolved_full_path, + "references": [ + { + "name": "place.proto", + "subject": subject_dependency, + "version": -1, + } + ], + } + res = await registry_async_client.post(f"subjects/{subject_entity}/versions", json=body) + assert res.status_code == 200 + assert ( + res.json()["id"] == previous_id + ), "The registered schema should be recognized as the same, no evolutions are being applied" + + +async def test_protobuf_schema_compatibility_import_renaming_should_fail(registry_async_client: Client) -> None: + first_subject_dependency = Subject("first_dependency") + second_subject_dependency = Subject("second_dependency") + subject_entity = Subject("entity") + + for subject in [first_subject_dependency, second_subject_dependency, subject_entity]: + res = await registry_async_client.put(f"config/{subject}", json={"compatibility": "FULL"}) + assert res.status_code == 200 + + first_dependency = """\ + syntax = "proto3"; + package my.awesome.customer.request.v1beta1; + message RequestId { + string request_id = 1; + }\ + """ + + second_dependency = """\ + syntax = "proto3"; + package awesome.customer.request.v1beta1; + message RequestId { + string request_id = 1; + }\ + """ + + original_partial_path = """\ + syntax = "proto3"; + import "my/awesome/customer/request/v1beta1/request_id.proto"; + import "awesome/customer/request/v1beta1/request_id.proto"; + + message MessageRequest { + awesome.customer.request.v1beta1.RequestId request_id = 1; + }\ + """ + + evolved_partial_path = """\ + syntax = "proto3"; + import "awesome/customer/request/v1beta1/request_id.proto"; + import "my/awesome/customer/request/v1beta1/request_id.proto"; + + message MessageRequest { + awesome.customer.request.v1beta1.RequestId request_id = 1; + }\ + """ + + # registering the first dependency + body = {"schemaType": "PROTOBUF", "schema": first_dependency} + res = await registry_async_client.post(f"subjects/{first_subject_dependency}/versions", json=body) + + assert res.status_code == 200 + + # registering the second dependency + body = {"schemaType": "PROTOBUF", "schema": second_dependency} + res = await registry_async_client.post(f"subjects/{second_subject_dependency}/versions", json=body) + + assert res.status_code == 200 + + # registering the entity + body = { + "schemaType": "PROTOBUF", + "schema": original_partial_path, + "references": [ + { + "name": f"{first_subject_dependency}.proto", + "subject": first_subject_dependency, + "version": -1, + }, + { + "name": f"{second_subject_dependency}.proto", + "subject": second_subject_dependency, + "version": -1, + }, + ], + } + res = await registry_async_client.post(f"subjects/{subject_entity}/versions", json=body) + assert res.status_code == 200 + + # registering the evolved entity + body = { + "schemaType": "PROTOBUF", + "schema": evolved_partial_path, + "references": [ + { + "name": f"{first_subject_dependency}.proto", + "subject": first_subject_dependency, + "version": -1, + }, + { + "name": f"{second_subject_dependency}.proto", + "subject": second_subject_dependency, + "version": -1, + }, + ], + } + res = await registry_async_client.post(f"subjects/{subject_entity}/versions", json=body) + assert res.status_code != 200, "The real used type is changed due to the relative import." + + +@pytest.mark.parametrize("compatibility,expected_to_fail", [("FULL", True), ("FORWARD", True), ("BACKWARD", False)]) +async def test_protobuf_schema_update_add_message( + registry_async_client: Client, + compatibility: str, + expected_to_fail: bool, +) -> None: + subject_place = create_subject_name_factory("test_protobuf_place")() + subject_customer = create_subject_name_factory("test_protobuf_customer")() + + for subject in [subject_place, subject_customer]: + res = await registry_async_client.put(f"config/{subject}", json={"compatibility": compatibility}) + assert res.status_code == 200 + + place_proto = """\ +syntax = "proto3"; +package a1; +message Place { + string city = 1; + int32 zone = 2; +} +""" + + body = {"schemaType": "PROTOBUF", "schema": place_proto} + res = await registry_async_client.post(f"subjects/{subject_place}/versions", json=body) + + assert res.status_code == 200 + + customer_proto = """\ +syntax = "proto3"; +package a1; +import "place.proto"; +import "google/type/postal_address.proto"; +// @producer: another comment +message Customer { + string name = 1; + int32 code = 2; + Place place = 3; + google.type.PostalAddress address = 4; +} +""" + body = { + "schemaType": "PROTOBUF", + "schema": customer_proto, + "references": [ + { + "name": "place.proto", + "subject": subject_place, + "version": -1, + } + ], + } + res = await registry_async_client.post(f"subjects/{subject_customer}/versions", json=body) + + assert res.status_code == 200 + + customer_proto_updated = """\ +syntax = "proto3"; +package a1; + +import "place.proto"; +import "google/type/postal_address.proto"; + +// @consumer: the comment was incorrect, updating it now +message Customer { + string name = 1; + int32 code = 2; + Place place = 3; + google.type.PostalAddress address = 4; +} +message Bar { + string another = 1; +} +""" + + body = { + "schemaType": "PROTOBUF", + "schema": customer_proto_updated, + "references": [ + { + "name": "place.proto", + "subject": subject_place, + "version": -1, + } + ], + } + res = await registry_async_client.post(f"subjects/{subject_customer}/versions", json=body) + + if expected_to_fail: + assert res.status_code == 409 + assert res.json() == { + "message": f"Incompatible schema, compatibility_mode={compatibility} Incompatible modification " + f"Modification.MESSAGE_DROP found", + "error_code": 409, + } + else: + assert res.status_code == 200 + + res = await registry_async_client.get(f"subjects/{subject_customer}/versions/2") + + assert res.status_code == 200 + assert res.json()["schema"] == customer_proto_updated