1import datetime as dt 2import re 3 4import pytest 5from marshmallow import fields, validate 6 7from .schemas import CategorySchema, CustomList, CustomStringField, CustomIntegerField 8from .utils import build_ref, get_schemas 9 10 11def test_field2choices_preserving_order(openapi): 12 choices = ["a", "b", "c", "aa", "0", "cc"] 13 field = fields.String(validate=validate.OneOf(choices)) 14 assert openapi.field2choices(field) == {"enum": choices} 15 16 17@pytest.mark.parametrize( 18 ("FieldClass", "jsontype"), 19 [ 20 (fields.Integer, "integer"), 21 (fields.Number, "number"), 22 (fields.Float, "number"), 23 (fields.String, "string"), 24 (fields.Str, "string"), 25 (fields.Boolean, "boolean"), 26 (fields.Bool, "boolean"), 27 (fields.UUID, "string"), 28 (fields.DateTime, "string"), 29 (fields.Date, "string"), 30 (fields.Time, "string"), 31 (fields.TimeDelta, "integer"), 32 (fields.Email, "string"), 33 (fields.URL, "string"), 34 # Custom fields inherit types from their parents 35 (CustomStringField, "string"), 36 (CustomIntegerField, "integer"), 37 ], 38) 39def test_field2property_type(FieldClass, jsontype, spec_fixture): 40 field = FieldClass() 41 res = spec_fixture.openapi.field2property(field) 42 assert res["type"] == jsontype 43 44 45@pytest.mark.parametrize("FieldClass", [fields.Field, fields.Raw]) 46def test_field2property_no_type_(FieldClass, spec_fixture): 47 field = FieldClass() 48 res = spec_fixture.openapi.field2property(field) 49 assert "type" not in res 50 51 52@pytest.mark.parametrize("ListClass", [fields.List, CustomList]) 53def test_formatted_field_translates_to_array(ListClass, spec_fixture): 54 field = ListClass(fields.String) 55 res = spec_fixture.openapi.field2property(field) 56 assert res["type"] == "array" 57 assert res["items"] == spec_fixture.openapi.field2property(fields.String()) 58 59 60@pytest.mark.parametrize( 61 ("FieldClass", "expected_format"), 62 [ 63 (fields.UUID, "uuid"), 64 (fields.DateTime, "date-time"), 65 (fields.Date, "date"), 66 (fields.Email, "email"), 67 (fields.URL, "url"), 68 ], 69) 70def test_field2property_formats(FieldClass, expected_format, spec_fixture): 71 field = FieldClass() 72 res = spec_fixture.openapi.field2property(field) 73 assert res["format"] == expected_format 74 75 76def test_field_with_description(spec_fixture): 77 field = fields.Str(metadata={"description": "a username"}) 78 res = spec_fixture.openapi.field2property(field) 79 assert res["description"] == "a username" 80 81 82def test_field_with_load_default(spec_fixture): 83 field = fields.Str(dump_default="foo", load_default="bar") 84 res = spec_fixture.openapi.field2property(field) 85 assert res["default"] == "bar" 86 87 88def test_boolean_field_with_false_load_default(spec_fixture): 89 field = fields.Boolean(dump_default=None, load_default=False) 90 res = spec_fixture.openapi.field2property(field) 91 assert res["default"] is False 92 93 94def test_datetime_field_with_load_default(spec_fixture): 95 field = fields.Date(load_default=dt.date(2014, 7, 18)) 96 res = spec_fixture.openapi.field2property(field) 97 assert res["default"] == dt.date(2014, 7, 18).isoformat() 98 99 100def test_field_with_load_default_callable(spec_fixture): 101 field = fields.Str(load_default=lambda: "dummy") 102 res = spec_fixture.openapi.field2property(field) 103 assert "default" not in res 104 105 106def test_field_with_default(spec_fixture): 107 field = fields.Str(metadata={"default": "Manual default"}) 108 res = spec_fixture.openapi.field2property(field) 109 assert res["default"] == "Manual default" 110 111 112def test_field_with_default_and_load_default(spec_fixture): 113 field = fields.Int(load_default=12, metadata={"default": 42}) 114 res = spec_fixture.openapi.field2property(field) 115 assert res["default"] == 42 116 117 118def test_field_with_choices(spec_fixture): 119 field = fields.Str(validate=validate.OneOf(["freddie", "brian", "john"])) 120 res = spec_fixture.openapi.field2property(field) 121 assert set(res["enum"]) == {"freddie", "brian", "john"} 122 123 124def test_field_with_equal(spec_fixture): 125 field = fields.Str(validate=validate.Equal("only choice")) 126 res = spec_fixture.openapi.field2property(field) 127 assert res["enum"] == ["only choice"] 128 129 130def test_only_allows_valid_properties_in_metadata(spec_fixture): 131 field = fields.Str( 132 load_default="foo", 133 metadata={ 134 "description": "foo", 135 "not_valid": "lol", 136 "allOf": ["bar"], 137 "enum": ["red", "blue"], 138 }, 139 ) 140 res = spec_fixture.openapi.field2property(field) 141 assert res["default"] == field.load_default 142 assert "description" in res 143 assert "enum" in res 144 assert "allOf" in res 145 assert "not_valid" not in res 146 147 148def test_field_with_choices_multiple(spec_fixture): 149 field = fields.Str( 150 validate=[ 151 validate.OneOf(["freddie", "brian", "john"]), 152 validate.OneOf(["brian", "john", "roger"]), 153 ] 154 ) 155 res = spec_fixture.openapi.field2property(field) 156 assert set(res["enum"]) == {"brian", "john"} 157 158 159def test_field_with_additional_metadata(spec_fixture): 160 field = fields.Str(metadata={"minLength": 6, "maxLength": 100}) 161 res = spec_fixture.openapi.field2property(field) 162 assert res["maxLength"] == 100 163 assert res["minLength"] == 6 164 165 166@pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True) 167def test_field_with_allow_none(spec_fixture): 168 field = fields.Str(allow_none=True) 169 res = spec_fixture.openapi.field2property(field) 170 if spec_fixture.openapi.openapi_version.major < 3: 171 assert res["x-nullable"] is True 172 elif spec_fixture.openapi.openapi_version.minor < 1: 173 assert res["nullable"] is True 174 else: 175 assert "nullable" not in res 176 assert res["type"] == ["string", "null"] 177 178 179def test_field_with_dump_only(spec_fixture): 180 field = fields.Str(dump_only=True) 181 res = spec_fixture.openapi.field2property(field) 182 assert res["readOnly"] is True 183 184 185@pytest.mark.parametrize("spec_fixture", ("2.0", "3.0.0", "3.1.0"), indirect=True) 186def test_field_with_load_only(spec_fixture): 187 field = fields.Str(load_only=True) 188 res = spec_fixture.openapi.field2property(field) 189 if spec_fixture.openapi.openapi_version.major < 3: 190 assert "writeOnly" not in res 191 else: 192 assert res["writeOnly"] is True 193 194 195def test_field_with_range_no_type(spec_fixture): 196 field = fields.Field(validate=validate.Range(min=1, max=10)) 197 res = spec_fixture.openapi.field2property(field) 198 assert res["x-minimum"] == 1 199 assert res["x-maximum"] == 10 200 assert "type" not in res 201 202 203@pytest.mark.parametrize("field", (fields.Number, fields.Integer)) 204def test_field_with_range_string_type(spec_fixture, field): 205 field = field(validate=validate.Range(min=1, max=10)) 206 res = spec_fixture.openapi.field2property(field) 207 assert res["minimum"] == 1 208 assert res["maximum"] == 10 209 assert isinstance(res["type"], str) 210 211 212@pytest.mark.parametrize("spec_fixture", ("3.1.0",), indirect=True) 213def test_field_with_range_type_list_with_number(spec_fixture): 214 @spec_fixture.openapi.map_to_openapi_type(["integer", "null"], None) 215 class NullableInteger(fields.Field): 216 """Nullable integer""" 217 218 field = NullableInteger(validate=validate.Range(min=1, max=10)) 219 res = spec_fixture.openapi.field2property(field) 220 assert res["minimum"] == 1 221 assert res["maximum"] == 10 222 assert res["type"] == ["integer", "null"] 223 224 225@pytest.mark.parametrize("spec_fixture", ("3.1.0",), indirect=True) 226def test_field_with_range_type_list_without_number(spec_fixture): 227 @spec_fixture.openapi.map_to_openapi_type(["string", "null"], None) 228 class NullableInteger(fields.Field): 229 """Nullable integer""" 230 231 field = NullableInteger(validate=validate.Range(min=1, max=10)) 232 res = spec_fixture.openapi.field2property(field) 233 assert res["x-minimum"] == 1 234 assert res["x-maximum"] == 10 235 assert res["type"] == ["string", "null"] 236 237 238def test_field_with_str_regex(spec_fixture): 239 regex_str = "^[a-zA-Z0-9]$" 240 field = fields.Str(validate=validate.Regexp(regex_str)) 241 ret = spec_fixture.openapi.field2property(field) 242 assert ret["pattern"] == regex_str 243 244 245def test_field_with_pattern_obj_regex(spec_fixture): 246 regex_str = "^[a-zA-Z0-9]$" 247 field = fields.Str(validate=validate.Regexp(re.compile(regex_str))) 248 ret = spec_fixture.openapi.field2property(field) 249 assert ret["pattern"] == regex_str 250 251 252def test_field_with_no_pattern(spec_fixture): 253 field = fields.Str() 254 ret = spec_fixture.openapi.field2property(field) 255 assert "pattern" not in ret 256 257 258def test_field_with_multiple_patterns(recwarn, spec_fixture): 259 regex_validators = [validate.Regexp("winner"), validate.Regexp("loser")] 260 field = fields.Str(validate=regex_validators) 261 with pytest.warns(UserWarning, match="More than one regex validator"): 262 ret = spec_fixture.openapi.field2property(field) 263 assert ret["pattern"] == "winner" 264 265 266def test_field2property_nested_spec_metadatas(spec_fixture): 267 spec_fixture.spec.components.schema("Category", schema=CategorySchema) 268 category = fields.Nested( 269 CategorySchema, 270 metadata={ 271 "description": "A category", 272 "invalid_property": "not in the result", 273 "x_extension": "A great extension", 274 }, 275 ) 276 result = spec_fixture.openapi.field2property(category) 277 assert result == { 278 "allOf": [build_ref(spec_fixture.spec, "schema", "Category")], 279 "description": "A category", 280 "x-extension": "A great extension", 281 } 282 283 284def test_field2property_nested_spec(spec_fixture): 285 spec_fixture.spec.components.schema("Category", schema=CategorySchema) 286 category = fields.Nested(CategorySchema) 287 assert spec_fixture.openapi.field2property(category) == build_ref( 288 spec_fixture.spec, "schema", "Category" 289 ) 290 291 292def test_field2property_nested_many_spec(spec_fixture): 293 spec_fixture.spec.components.schema("Category", schema=CategorySchema) 294 category = fields.Nested(CategorySchema, many=True) 295 ret = spec_fixture.openapi.field2property(category) 296 assert ret["type"] == "array" 297 assert ret["items"] == build_ref(spec_fixture.spec, "schema", "Category") 298 299 300def test_field2property_nested_ref(spec_fixture): 301 category = fields.Nested(CategorySchema) 302 ref = spec_fixture.openapi.field2property(category) 303 assert ref == build_ref(spec_fixture.spec, "schema", "Category") 304 305 306def test_field2property_nested_many(spec_fixture): 307 categories = fields.Nested(CategorySchema, many=True) 308 res = spec_fixture.openapi.field2property(categories) 309 assert res["type"] == "array" 310 assert res["items"] == build_ref(spec_fixture.spec, "schema", "Category") 311 312 313def test_nested_field_with_property(spec_fixture): 314 category_1 = fields.Nested(CategorySchema) 315 category_2 = fields.Nested(CategorySchema, dump_only=True) 316 category_3 = fields.Nested(CategorySchema, many=True) 317 category_4 = fields.Nested(CategorySchema, many=True, dump_only=True) 318 spec_fixture.spec.components.schema("Category", schema=CategorySchema) 319 320 assert spec_fixture.openapi.field2property(category_1) == build_ref( 321 spec_fixture.spec, "schema", "Category" 322 ) 323 assert spec_fixture.openapi.field2property(category_2) == { 324 "allOf": [build_ref(spec_fixture.spec, "schema", "Category")], 325 "readOnly": True, 326 } 327 assert spec_fixture.openapi.field2property(category_3) == { 328 "items": build_ref(spec_fixture.spec, "schema", "Category"), 329 "type": "array", 330 } 331 assert spec_fixture.openapi.field2property(category_4) == { 332 "items": build_ref(spec_fixture.spec, "schema", "Category"), 333 "readOnly": True, 334 "type": "array", 335 } 336 337 338class TestField2PropertyPluck: 339 @pytest.fixture(autouse=True) 340 def _setup(self, spec_fixture): 341 self.field2property = spec_fixture.openapi.field2property 342 343 self.spec = spec_fixture.spec 344 self.spec.components.schema("Category", schema=CategorySchema) 345 self.unplucked = get_schemas(self.spec)["Category"]["properties"]["breed"] 346 347 def test_spec(self, spec_fixture): 348 breed = fields.Pluck(CategorySchema, "breed") 349 assert self.field2property(breed) == self.unplucked 350 351 def test_with_property(self): 352 breed = fields.Pluck(CategorySchema, "breed", dump_only=True) 353 assert self.field2property(breed) == {**self.unplucked, "readOnly": True} 354 355 def test_metadata(self): 356 breed = fields.Pluck( 357 CategorySchema, 358 "breed", 359 metadata={ 360 "description": "Category breed", 361 "invalid_property": "not in the result", 362 "x_extension": "A great extension", 363 }, 364 ) 365 assert self.field2property(breed) == { 366 **self.unplucked, 367 "description": "Category breed", 368 "x-extension": "A great extension", 369 } 370 371 def test_many(self): 372 breed = fields.Pluck(CategorySchema, "breed", many=True) 373 assert self.field2property(breed) == {"type": "array", "items": self.unplucked} 374 375 def test_many_with_property(self): 376 breed = fields.Pluck(CategorySchema, "breed", many=True, dump_only=True) 377 assert self.field2property(breed) == { 378 "items": self.unplucked, 379 "type": "array", 380 "readOnly": True, 381 } 382 383 384def test_custom_properties_for_custom_fields(spec_fixture): 385 def custom_string2properties(self, field, **kwargs): 386 ret = {} 387 if isinstance(field, CustomStringField): 388 if self.openapi_version.major == 2: 389 ret["x-customString"] = True 390 else: 391 ret["x-customString"] = False 392 return ret 393 394 spec_fixture.marshmallow_plugin.converter.add_attribute_function( 395 custom_string2properties 396 ) 397 properties = spec_fixture.marshmallow_plugin.converter.field2property( 398 CustomStringField() 399 ) 400 assert properties["x-customString"] == ( 401 spec_fixture.openapi.openapi_version == "2.0" 402 ) 403 404 405def test_field2property_with_non_string_metadata_keys(spec_fixture): 406 class _DesertSentinel: 407 pass 408 409 field = fields.Boolean(metadata={"description": "A description"}) 410 field.metadata[_DesertSentinel()] = "to be ignored" 411 result = spec_fixture.openapi.field2property(field) 412 assert result == {"description": "A description", "type": "boolean"} 413