-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
tests: add tests on schema compatibility checks
[EC-289]
- Loading branch information
1 parent
5f01c75
commit 4879bef
Showing
3 changed files
with
329 additions
and
3 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,257 @@ | ||
""" | ||
Copyright (c) 2024 Aiven Ltd | ||
See LICENSE for details | ||
""" | ||
from avro.compatibility import SchemaCompatibilityResult, SchemaCompatibilityType | ||
from karapace.compatibility import CompatibilityModes | ||
from karapace.compatibility.schema_compatibility import SchemaCompatibility | ||
from karapace.config import DEFAULTS | ||
from karapace.in_memory_database import InMemoryDatabase | ||
from karapace.schema_models import ParsedTypedSchema, SchemaVersion, TypedSchema, ValidatedTypedSchema | ||
from karapace.schema_registry import KarapaceSchemaRegistry | ||
from karapace.schema_type import SchemaType | ||
from karapace.typing import Subject, Version | ||
from unittest import mock | ||
from unittest.mock import MagicMock, Mock | ||
|
||
import logging | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
class MockedSchemaRegistry: | ||
def __init__(self, compatibility_mode: CompatibilityModes, has_deleted_schema: bool = False): | ||
self.schema_registry = KarapaceSchemaRegistry(DEFAULTS) | ||
self.compatibility_mode = compatibility_mode | ||
self.schema_registry.get_compatibility_mode = Mock(return_value=self.compatibility_mode) | ||
|
||
schema_version1 = Mock(spec=SchemaVersion) | ||
schema_version1.deleted = False | ||
typed_schema1 = Mock(spec=TypedSchema) | ||
schema_version1.schema = typed_schema1 | ||
|
||
schema_version2 = Mock(spec=SchemaVersion) | ||
schema_version2.deleted = has_deleted_schema | ||
typed_schema2 = Mock(spec=TypedSchema) | ||
schema_version2.schema = typed_schema2 | ||
|
||
schema_version3 = Mock(spec=SchemaVersion) | ||
typed_schema3 = Mock(spec=TypedSchema) | ||
schema_version3.deleted = False | ||
schema_version3.schema = typed_schema3 | ||
|
||
self.schema_registry.database = Mock(spec=InMemoryDatabase) | ||
|
||
self.schema_registry.database.find_subject_schemas = Mock( | ||
return_value={ | ||
Version(1): schema_version1, | ||
Version(2): schema_version2, | ||
Version(3): schema_version3, | ||
} | ||
) | ||
|
||
self.parsed_schema1 = Mock(spec=ParsedTypedSchema) | ||
self.parsed_schema2 = Mock(spec=ParsedTypedSchema) | ||
self.parsed_schema3 = Mock(spec=ParsedTypedSchema) | ||
|
||
def resolve_and_parse_mock(schema: TypedSchema) -> ParsedTypedSchema: | ||
if schema == typed_schema1: | ||
return self.parsed_schema1 | ||
if schema == typed_schema2: | ||
return self.parsed_schema2 | ||
if schema == typed_schema3: | ||
return self.parsed_schema3 | ||
raise ValueError(f"Unexpected object {schema}") | ||
|
||
self.schema_registry.resolve_and_parse = MagicMock(side_effect=resolve_and_parse_mock) | ||
|
||
def check_schema_compatibility( | ||
self, | ||
new_schema: ValidatedTypedSchema, | ||
subject: Subject, | ||
) -> SchemaCompatibilityResult: | ||
return self.schema_registry.check_schema_compatibility(new_schema, subject) | ||
|
||
|
||
async def test_schema_compatible_in_transitive_mode() -> None: | ||
# Given | ||
schema_registry = MockedSchemaRegistry(CompatibilityModes.FULL_TRANSITIVE) | ||
new_schema = ValidatedTypedSchema.parse(SchemaType.JSONSCHEMA, '{"type": "array"}') | ||
subject = Subject("subject") | ||
|
||
# Don't test the actual compatibility checks | ||
result = Mock(spec=SchemaCompatibilityResult) | ||
result.compatibility = SchemaCompatibilityType.compatible | ||
SchemaCompatibility.check_compatibility = Mock(return_value=result) | ||
|
||
# When | ||
schema_registry.check_schema_compatibility(new_schema, subject) | ||
|
||
# Then | ||
assert result.compatibility is SchemaCompatibilityType.compatible | ||
|
||
# All 3 schemas are checked against | ||
SchemaCompatibility.check_compatibility.assert_has_calls( | ||
[ | ||
mock.call( | ||
old_schema=schema_registry.parsed_schema1, | ||
new_schema=new_schema, | ||
compatibility_mode=schema_registry.compatibility_mode, | ||
), | ||
mock.call( | ||
old_schema=schema_registry.parsed_schema2, | ||
new_schema=new_schema, | ||
compatibility_mode=schema_registry.compatibility_mode, | ||
), | ||
mock.call( | ||
old_schema=schema_registry.parsed_schema3, | ||
new_schema=new_schema, | ||
compatibility_mode=schema_registry.compatibility_mode, | ||
), | ||
] | ||
) | ||
|
||
|
||
async def test_schema_incompatible_in_transitive_mode() -> None: | ||
# Given | ||
schema_registry = MockedSchemaRegistry(CompatibilityModes.FULL_TRANSITIVE) | ||
new_schema = ValidatedTypedSchema.parse(SchemaType.JSONSCHEMA, '{"type": "array"}') | ||
subject = Subject("subject") | ||
|
||
# Don't test the actual compatibility checks | ||
result = Mock(spec=SchemaCompatibilityResult) | ||
result.compatibility = SchemaCompatibilityType.incompatible | ||
SchemaCompatibility.check_compatibility = Mock(return_value=result) | ||
|
||
# When | ||
schema_registry.check_schema_compatibility(new_schema, subject) | ||
|
||
# Then | ||
assert result.compatibility is SchemaCompatibilityType.incompatible | ||
|
||
# Only one schema is checked against (first fail stops all checks) | ||
SchemaCompatibility.check_compatibility.assert_has_calls( | ||
[ | ||
mock.call( | ||
old_schema=schema_registry.parsed_schema1, | ||
new_schema=new_schema, | ||
compatibility_mode=schema_registry.compatibility_mode, | ||
), | ||
] | ||
) | ||
|
||
|
||
async def test_schema_compatible_in_not_transitive_mode() -> None: | ||
# Given | ||
schema_registry = MockedSchemaRegistry(CompatibilityModes.FULL) | ||
new_schema = ValidatedTypedSchema.parse(SchemaType.JSONSCHEMA, '{"type": "array"}') | ||
subject = Subject("subject") | ||
|
||
# Don't test the actual compatibility checks | ||
result = Mock(spec=SchemaCompatibilityResult) | ||
result.compatibility = SchemaCompatibilityType.compatible | ||
SchemaCompatibility.check_compatibility = Mock(return_value=result) | ||
|
||
# When | ||
schema_registry.check_schema_compatibility(new_schema, subject) | ||
|
||
# Then | ||
assert result.compatibility is SchemaCompatibilityType.compatible | ||
|
||
# Only the last schema is checked against (not transitive) | ||
SchemaCompatibility.check_compatibility.assert_has_calls( | ||
[ | ||
mock.call( | ||
old_schema=schema_registry.parsed_schema3, | ||
new_schema=new_schema, | ||
compatibility_mode=schema_registry.compatibility_mode, | ||
) | ||
] | ||
) | ||
|
||
|
||
async def test_schema_incompatible_in_not_transitive_mode() -> None: | ||
# Given | ||
schema_registry = MockedSchemaRegistry(CompatibilityModes.FULL) | ||
new_schema = ValidatedTypedSchema.parse(SchemaType.JSONSCHEMA, '{"type": "array"}') | ||
subject = Subject("subject") | ||
|
||
# Don't test the actual compatibility checks | ||
result = Mock(spec=SchemaCompatibilityResult) | ||
result.compatibility = SchemaCompatibilityType.incompatible | ||
SchemaCompatibility.check_compatibility = Mock(return_value=result) | ||
|
||
# When | ||
schema_registry.check_schema_compatibility(new_schema, subject) | ||
|
||
# Then | ||
assert result.compatibility is SchemaCompatibilityType.incompatible | ||
|
||
# Only the last schema is checked against (not transitive) | ||
SchemaCompatibility.check_compatibility.assert_has_calls( | ||
[ | ||
mock.call( | ||
old_schema=schema_registry.parsed_schema3, | ||
new_schema=new_schema, | ||
compatibility_mode=schema_registry.compatibility_mode, | ||
) | ||
] | ||
) | ||
|
||
|
||
async def test_schema_compatible_with_no_live_schemas() -> None: | ||
# Given | ||
schema_registry = MockedSchemaRegistry(CompatibilityModes.FULL) | ||
new_schema = ValidatedTypedSchema.parse(SchemaType.JSONSCHEMA, '{"type": "array"}') | ||
subject = Subject("subject") | ||
|
||
# No schemas in registry | ||
schema_registry.schema_registry.database.find_subject_schemas = Mock(return_value={}) | ||
|
||
# Don't test the actual compatibility checks | ||
result = Mock(spec=SchemaCompatibilityResult) | ||
result.compatibility = SchemaCompatibilityType.compatible | ||
SchemaCompatibility.check_compatibility = Mock(return_value=result) | ||
|
||
# When | ||
schema_registry.check_schema_compatibility(new_schema, subject) | ||
|
||
# Then | ||
assert result.compatibility is SchemaCompatibilityType.compatible | ||
|
||
# No check is done (no existing schemas) | ||
SchemaCompatibility.check_compatibility.assert_not_called() | ||
|
||
|
||
async def test_schema_compatible_in_transitive_mode_with_deleted_schema() -> None: | ||
# Given | ||
schema_registry = MockedSchemaRegistry(CompatibilityModes.FULL_TRANSITIVE, has_deleted_schema=True) | ||
new_schema = ValidatedTypedSchema.parse(SchemaType.JSONSCHEMA, '{"type": "array"}') | ||
subject = Subject("subject") | ||
|
||
# Don't test the actual compatibility checks | ||
result = Mock(spec=SchemaCompatibilityResult) | ||
result.compatibility = SchemaCompatibilityType.compatible | ||
SchemaCompatibility.check_compatibility = Mock(return_value=result) | ||
|
||
# When | ||
schema_registry.check_schema_compatibility(new_schema, subject) | ||
|
||
# Then | ||
assert result.compatibility is SchemaCompatibilityType.compatible | ||
|
||
# Only 2 schemas are checked against (the 3rd one is deleted) | ||
SchemaCompatibility.check_compatibility.assert_has_calls( | ||
[ | ||
mock.call( | ||
old_schema=schema_registry.parsed_schema1, | ||
new_schema=new_schema, | ||
compatibility_mode=schema_registry.compatibility_mode, | ||
), | ||
mock.call( | ||
old_schema=schema_registry.parsed_schema3, | ||
new_schema=new_schema, | ||
compatibility_mode=schema_registry.compatibility_mode, | ||
), | ||
] | ||
) |
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