def test_interface_field_added_and_removed(): a = schema(""" interface Person { name: String age: Int } """) b = schema(""" interface Person { name: String age: Int favorite_number: Float } """) diff = Schema(a, b).diff() assert diff and len(diff) == 1 assert diff[ 0].message == "Field `favorite_number` of type `Float` was added to interface `Person`" assert diff[0].path == 'Person.favorite_number' assert diff[0].criticality == Criticality.dangerous( 'Adding an interface to an object type may break existing clients ' 'that were not programming defensively against a new possible type.') diff = Schema(b, a).diff() assert diff[ 0].message == "Field `favorite_number` was removed from interface `Person`" assert diff[0].path == 'Person.favorite_number' assert diff[0].criticality == Criticality.dangerous( 'Removing an interface field can break existing queries that use this in a fragment spread.' )
def test_enums_added(): a = schema(""" type Query { a: String } enum Letters { A B } """) b = schema(""" type Query { a: String } enum Letters { A B C D } """) diff = Schema(a, b).diff() assert len(diff) == 2 expected_diff = { "Enum value `C` was added to `Letters` enum", "Enum value `D` was added to `Letters` enum", } expected_paths = {'Letters.C', 'Letters.D'} for change in diff: assert change.message in expected_diff assert change.path in expected_paths assert change.criticality == Criticality.dangerous( "Adding an enum value may break existing clients that " "were not programming defensively against an added case when querying an enum." )
def test_directive_default_value_changed(): default_100 = schema(""" directive @limit(number: Int=100) on FIELD_DEFINITION type A { a: String } """) default_0 = schema(""" directive @limit(number: Int=0) on FIELD_DEFINITION type A { a: String } """) diff = Schema(default_100, default_0).diff() assert diff and len(diff) == 1 expected_message = ( 'Default value for argument `number` on `@limit` directive changed from `100` to `0`' ) assert diff[0].message == expected_message assert diff[0].path == '@limit' assert diff[0].criticality == Criticality.dangerous( 'Changing the default value for an argument may change ' 'the runtime behaviour of a field if it was never provided.' )
class DangerousChange(Change): criticality = Criticality.dangerous('this is dangerous') def message(self): return 'test message' def path(self): return 'test path'
def __init__(self, directive, arg_name, old_default, new_default): self.criticality = Criticality.dangerous( "Changing the default value for an argument may change the runtime " "behaviour of a field if it was never provided.") self.directive = directive self.arg_name = arg_name self.old_default = old_default self.new_default = new_default
def test_add_type_to_union(): two_types = schema(""" type Query { c: Int } type Result { message: String } type Error { message: String details: String } type Unknown { message: String details: String traceback: String } union Outcome = Result | Error """) three_types = schema(""" type Query { c: Int } type Result { message: String } type Error { message: String details: String } type Unknown { message: String details: String traceback: String } union Outcome = Result | Error | Unknown """) diff = Schema(two_types, three_types).diff() assert diff and len(diff) == 1 assert diff[ 0].message == "Union member `Unknown` was added to `Outcome` Union type" assert diff[0].path == 'Outcome' assert diff[0].criticality == Criticality.dangerous( 'Adding a possible type to Unions may break existing clients ' 'that were not programming defensively against a new possible type.') diff = Schema(three_types, two_types).diff() assert diff and len(diff) == 1 assert diff[ 0].message == "Union member `Unknown` was removed from `Outcome` Union type" assert diff[0].path == 'Outcome' assert diff[0].criticality == Criticality.breaking( 'Removing a union member from a union can break queries that use this union member in a fragment spread' )
class MyChange(Change): criticality = Criticality.dangerous('This might be dangerous') @property def message(self): return 'Unagi' @property def path(self): return 'Unagi.Friends'
class FieldArgumentDefaultValueChanged(FieldAbstractArgumentChange): criticality = Criticality.dangerous( "Changing the default value for an argument may change the runtime " "behaviour of a field if it was never provided.") @property def message(self): return ( f"Default value for argument `{self.arg_name}` on field `{self.parent}.{self.field_name}` " f"changed from `{self.old_arg.default_value!r}` to `{self.new_arg.default_value!r}`" )
def __init__(self, parent, field_name, field): self.parent = parent self.field_name = field_name self.field = field self.criticality = ( Criticality.dangerous( "Removing deprecated fields without sufficient time for clients " "to update their queries may break their code" ) if field.deprecation_reason else Criticality.breaking( "Removing a field is a breaking change. It is preferred to deprecate the field before removing it." ) )
class UnionMemberAdded(Change): criticality = Criticality.dangerous( "Adding a possible type to Unions may break existing clients " "that were not programming defensively against a new possible type.") def __init__(self, union, value): self.union = union self.value = value @property def message(self): return f"Union member `{self.value}` was added to `{self.union.name}` Union type" @property def path(self): return f"{self.union.name}"
class NewInterfaceImplemented(Change): criticality = Criticality.dangerous( "Adding an interface to an object type may break existing clients " "that were not programming defensively against a new possible type." ) def __init__(self, interface, type_): self.interface = interface self.type_ = type_ @property def message(self): return f"`{self.type_.name}` implements new interface `{self.interface.name}`" @property def path(self): return f"{self.type_}"
class InterfaceFieldRemoved(Change): criticality = Criticality.dangerous( "Removing an interface field can break existing " "queries that use this in a fragment spread." ) def __init__(self, interface, field_name): self.interface = interface self.field_name = field_name @property def message(self): return f"Field `{self.field_name}` was removed from interface `{self.interface}`" @property def path(self): return f"{self.interface.name}.{self.field_name}"
class InterfaceFieldAdded(Change): criticality = Criticality.dangerous( "Adding an interface to an object type may break existing clients " "that were not programming defensively against a new possible type." ) def __init__(self, interface, field_name, field): self.interface = interface self.field_name = field_name self.field = field @property def message(self): return f"Field `{self.field_name}` of type `{self.field.type}` was added to interface `{self.interface}`" @property def path(self): return f"{self.interface.name}.{self.field_name}"
def test_input_field_default_value_changed(): a = schema(""" input Params { love: Int = 0 } """) b = schema(""" input Params { love: Int = 100 } """) diff = Schema(a, b).diff() assert diff and len(diff) == 1 assert diff[ 0].message == "Default value for input field `Params.love` changed from `0` to `100`" assert diff[0].path == 'Params.love' assert diff[0].criticality == Criticality.dangerous( 'Changing the default value for an argument may change ' 'the runtime behaviour of a field if it was never provided.')
class InputFieldDefaultChanged(Change): criticality = Criticality.dangerous( "Changing the default value for an argument may change the runtime " "behaviour of a field if it was never provided." ) def __init__(self, input_, name, new_field, old_field): self.input_ = input_ self.name = name self.new_field = new_field self.old_field = old_field @property def message(self): return ( f"Default value for input field `{self.input_.name}.{self.name}` " f"changed from `{self.old_field.default_value!r}` to `{self.new_field.default_value!r}`" ) @property def path(self): return f"{self.input_.name}.{self.name}"