oggit/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_fields_mask.py

1114 lines
32 KiB
Python

import json
import pytest
from collections import OrderedDict
from flask_restx import mask, Api, Resource, fields, marshal, Mask
def assert_data(tested, expected):
"""Compare data without caring about order and type (dict vs. OrderedDict)"""
tested = json.loads(json.dumps(tested))
expected = json.loads(json.dumps(expected))
assert tested == expected
class MaskMixin(object):
def test_empty_mask(self):
assert Mask("") == {}
def test_one_field(self):
assert Mask("field_name") == {"field_name": True}
def test_multiple_field(self):
mask = Mask("field1, field2, field3")
assert_data(
mask,
{
"field1": True,
"field2": True,
"field3": True,
},
)
def test_nested_fields(self):
parsed = Mask("nested{field1,field2}")
expected = {
"nested": {
"field1": True,
"field2": True,
}
}
assert parsed == expected
def test_complex(self):
parsed = Mask("field1, nested{field, sub{subfield}}, field2")
expected = {
"field1": True,
"nested": {
"field": True,
"sub": {
"subfield": True,
},
},
"field2": True,
}
assert_data(parsed, expected)
def test_star(self):
parsed = Mask("nested{field1,field2},*")
expected = {
"nested": {
"field1": True,
"field2": True,
},
"*": True,
}
assert_data(parsed, expected)
def test_order(self):
parsed = Mask("f_3, nested{f_1, f_2, f_3}, f_2, f_1")
expected = OrderedDict(
[
("f_3", True),
(
"nested",
OrderedDict(
[
("f_1", True),
("f_2", True),
("f_3", True),
]
),
),
("f_2", True),
("f_1", True),
]
)
assert parsed == expected
def test_missing_closing_bracket(self):
with pytest.raises(mask.ParseError):
Mask("nested{")
def test_consecutive_coma(self):
with pytest.raises(mask.ParseError):
Mask("field,,")
def test_coma_before_bracket(self):
with pytest.raises(mask.ParseError):
Mask("field,{}")
def test_coma_after_bracket(self):
with pytest.raises(mask.ParseError):
Mask("nested{,}")
def test_unexpected_opening_bracket(self):
with pytest.raises(mask.ParseError):
Mask("{{field}}")
def test_unexpected_closing_bracket(self):
with pytest.raises(mask.ParseError):
Mask("{field}}")
def test_support_colons(self):
assert Mask("field:name") == {"field:name": True}
def test_support_dash(self):
assert Mask("field-name") == {"field-name": True}
def test_support_underscore(self):
assert Mask("field_name") == {"field_name": True}
class MaskUnwrappedTest(MaskMixin):
def parse(self, value):
return Mask(value)
class MaskWrappedTest(MaskMixin):
def parse(self, value):
return Mask("{" + value + "}")
class DObject(object):
"""A dead simple object built from a dictionnary (no recursion)"""
def __init__(self, data):
self.__dict__.update(data)
person_fields = {"name": fields.String, "age": fields.Integer}
class ApplyMaskTest(object):
def test_empty(self):
data = {
"integer": 42,
"string": "a string",
"boolean": True,
}
result = mask.apply(data, "{}")
assert result == {}
def test_single_field(self):
data = {
"integer": 42,
"string": "a string",
"boolean": True,
}
result = mask.apply(data, "{integer}")
assert result == {"integer": 42}
def test_multiple_fields(self):
data = {
"integer": 42,
"string": "a string",
"boolean": True,
}
result = mask.apply(data, "{integer, string}")
assert result == {"integer": 42, "string": "a string"}
def test_star_only(self):
data = {
"integer": 42,
"string": "a string",
"boolean": True,
}
result = mask.apply(data, "*")
assert result == data
def test_with_objects(self):
data = DObject(
{
"integer": 42,
"string": "a string",
"boolean": True,
}
)
result = mask.apply(data, "{integer, string}")
assert result == {"integer": 42, "string": "a string"}
def test_with_ordered_dict(self):
data = OrderedDict(
{
"integer": 42,
"string": "a string",
"boolean": True,
}
)
result = mask.apply(data, "{integer, string}")
assert result == {"integer": 42, "string": "a string"}
def test_nested_field(self):
data = {
"integer": 42,
"string": "a string",
"boolean": True,
"nested": {
"integer": 42,
"string": "a string",
"boolean": True,
},
}
result = mask.apply(data, "{nested}")
assert result == {
"nested": {
"integer": 42,
"string": "a string",
"boolean": True,
}
}
def test_nested_fields(self):
data = {
"nested": {
"integer": 42,
"string": "a string",
"boolean": True,
}
}
result = mask.apply(data, "{nested{integer}}")
assert result == {"nested": {"integer": 42}}
def test_nested_with_start(self):
data = {
"nested": {
"integer": 42,
"string": "a string",
"boolean": True,
},
"other": "value",
}
result = mask.apply(data, "{nested{integer},*}")
assert result == {"nested": {"integer": 42}, "other": "value"}
def test_nested_fields_when_none(self):
data = {"nested": None}
result = mask.apply(data, "{nested{integer}}")
assert result == {"nested": None}
def test_raw_api_fields(self):
family_fields = {
"father": fields.Raw,
"mother": fields.Raw,
}
result = mask.apply(family_fields, "father{name},mother{age}")
data = {
"father": {"name": "John", "age": 42},
"mother": {"name": "Jane", "age": 42},
}
expected = {"father": {"name": "John"}, "mother": {"age": 42}}
assert_data(marshal(data, result), expected)
# Should leave th original mask untouched
assert_data(marshal(data, family_fields), data)
def test_nested_api_fields(self):
family_fields = {
"father": fields.Nested(person_fields),
"mother": fields.Nested(person_fields),
}
result = mask.apply(family_fields, "father{name},mother{age}")
assert set(result.keys()) == set(["father", "mother"])
assert isinstance(result["father"], fields.Nested)
assert set(result["father"].nested.keys()) == set(["name"])
assert isinstance(result["mother"], fields.Nested)
assert set(result["mother"].nested.keys()) == set(["age"])
data = {
"father": {"name": "John", "age": 42},
"mother": {"name": "Jane", "age": 42},
}
expected = {"father": {"name": "John"}, "mother": {"age": 42}}
assert_data(marshal(data, result), expected)
# Should leave th original mask untouched
assert_data(marshal(data, family_fields), data)
def test_multiple_nested_api_fields(self):
level_2 = {"nested_2": fields.Nested(person_fields)}
level_1 = {"nested_1": fields.Nested(level_2)}
root = {"nested": fields.Nested(level_1)}
result = mask.apply(root, "nested{nested_1{nested_2{name}}}")
assert set(result.keys()) == set(["nested"])
assert isinstance(result["nested"], fields.Nested)
assert set(result["nested"].nested.keys()) == set(["nested_1"])
data = {"nested": {"nested_1": {"nested_2": {"name": "John", "age": 42}}}}
expected = {"nested": {"nested_1": {"nested_2": {"name": "John"}}}}
assert_data(marshal(data, result), expected)
# Should leave th original mask untouched
assert_data(marshal(data, root), data)
def test_list_fields_with_simple_field(self):
family_fields = {"name": fields.String, "members": fields.List(fields.String)}
result = mask.apply(family_fields, "members")
assert set(result.keys()) == set(["members"])
assert isinstance(result["members"], fields.List)
assert isinstance(result["members"].container, fields.String)
data = {"name": "Doe", "members": ["John", "Jane"]}
expected = {"members": ["John", "Jane"]}
assert_data(marshal(data, result), expected)
# Should leave th original mask untouched
assert_data(marshal(data, family_fields), data)
def test_list_fields_with_nested(self):
family_fields = {"members": fields.List(fields.Nested(person_fields))}
result = mask.apply(family_fields, "members{name}")
assert set(result.keys()) == set(["members"])
assert isinstance(result["members"], fields.List)
assert isinstance(result["members"].container, fields.Nested)
assert set(result["members"].container.nested.keys()) == set(["name"])
data = {
"members": [
{"name": "John", "age": 42},
{"name": "Jane", "age": 42},
]
}
expected = {"members": [{"name": "John"}, {"name": "Jane"}]}
assert_data(marshal(data, result), expected)
# Should leave th original mask untouched
assert_data(marshal(data, family_fields), data)
def test_list_fields_with_nested_inherited(self, app):
api = Api(app)
person = api.model("Person", {"name": fields.String, "age": fields.Integer})
child = api.inherit("Child", person, {"attr": fields.String})
family = api.model("Family", {"children": fields.List(fields.Nested(child))})
result = mask.apply(family.resolved, "children{name,attr}")
data = {
"children": [
{"name": "John", "age": 5, "attr": "value-john"},
{"name": "Jane", "age": 42, "attr": "value-jane"},
]
}
expected = {
"children": [
{"name": "John", "attr": "value-john"},
{"name": "Jane", "attr": "value-jane"},
]
}
assert_data(marshal(data, result), expected)
# Should leave th original mask untouched
assert_data(marshal(data, family), data)
def test_list_fields_with_raw(self):
family_fields = {"members": fields.List(fields.Raw)}
result = mask.apply(family_fields, "members{name}")
data = {
"members": [
{"name": "John", "age": 42},
{"name": "Jane", "age": 42},
]
}
expected = {"members": [{"name": "John"}, {"name": "Jane"}]}
assert_data(marshal(data, result), expected)
# Should leave th original mask untouched
assert_data(marshal(data, family_fields), data)
def test_list(self):
data = [
{
"integer": 42,
"string": "a string",
"boolean": True,
},
{
"integer": 404,
"string": "another string",
"boolean": False,
},
]
result = mask.apply(data, "{integer, string}")
assert result == [
{"integer": 42, "string": "a string"},
{"integer": 404, "string": "another string"},
]
def test_nested_list(self):
data = {
"integer": 42,
"list": [
{
"integer": 42,
"string": "a string",
},
{
"integer": 404,
"string": "another string",
},
],
}
result = mask.apply(data, "{list}")
assert result == {
"list": [
{
"integer": 42,
"string": "a string",
},
{
"integer": 404,
"string": "another string",
},
]
}
def test_nested_list_fields(self):
data = {
"list": [
{
"integer": 42,
"string": "a string",
},
{
"integer": 404,
"string": "another string",
},
]
}
result = mask.apply(data, "{list{integer}}")
assert result == {"list": [{"integer": 42}, {"integer": 404}]}
def test_missing_field_none_by_default(self):
result = mask.apply({}, "{integer}")
assert result == {"integer": None}
def test_missing_field_skipped(self):
result = mask.apply({}, "{integer}", skip=True)
assert result == {}
def test_missing_nested_field_skipped(self):
result = mask.apply({}, "nested{integer}", skip=True)
assert result == {}
def test_mask_error_on_simple_fields(self):
model = {
"name": fields.String,
}
with pytest.raises(mask.MaskError):
mask.apply(model, "name{notpossible}")
def test_mask_error_on_list_field(self):
model = {"nested": fields.List(fields.String)}
with pytest.raises(mask.MaskError):
mask.apply(model, "nested{notpossible}")
class MaskAPI(object):
def test_marshal_with_honour_field_mask_header(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model)
def get(self):
return {"name": "John Doe", "age": 42, "boolean": True}
data = client.get_json("/test/", headers={"X-Fields": "{name,age}"})
assert data == {"name": "John Doe", "age": 42}
def test_marshal_with_honour_field_mask_list(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model)
def get(self):
return [
{"name": "John Doe", "age": 42, "boolean": True},
{"name": "Jane Doe", "age": 33, "boolean": False},
]
data = client.get_json("/test/", headers={"X-Fields": "{name,age}"})
assert data == [
{
"name": "John Doe",
"age": 42,
},
{
"name": "Jane Doe",
"age": 33,
},
]
def test_marshal_with_honour_complex_field_mask_header(self, app, client):
api = Api(app)
person = api.model("Person", person_fields)
child = api.inherit("Child", person, {"attr": fields.String})
family = api.model(
"Family",
{
"father": fields.Nested(person),
"mother": fields.Nested(person),
"children": fields.List(fields.Nested(child)),
"free": fields.List(fields.Raw),
},
)
house = api.model(
"House", {"family": fields.Nested(family, attribute="people")}
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(house)
def get(self):
return {
"people": {
"father": {"name": "John", "age": 42},
"mother": {"name": "Jane", "age": 42},
"children": [
{"name": "Jack", "age": 5, "attr": "value-1"},
{"name": "Julie", "age": 7, "attr": "value-2"},
],
"free": [
{"key-1": "1-1", "key-2": "1-2"},
{"key-1": "2-1", "key-2": "2-2"},
],
}
}
data = client.get_json(
"/test/",
headers={
"X-Fields": "family{father{name},mother{age},children{name,attr},free{key-2}}"
},
)
assert data == {
"family": {
"father": {"name": "John"},
"mother": {"age": 42},
"children": [
{"name": "Jack", "attr": "value-1"},
{"name": "Julie", "attr": "value-2"},
],
"free": [{"key-2": "1-2"}, {"key-2": "2-2"}],
}
}
def test_marshal_honour_field_mask(self, app):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
)
data = {"name": "John Doe", "age": 42, "boolean": True}
result = api.marshal(data, model, mask="{name,age}")
assert result == {
"name": "John Doe",
"age": 42,
}
def test_marshal_with_honour_default_mask(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model, mask="{name,age}")
def get(self):
return {"name": "John Doe", "age": 42, "boolean": True}
data = self.get_json("/test/")
self.assertEqual(
data,
{
"name": "John Doe",
"age": 42,
},
)
def test_marshal_with_honour_default_model_mask(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
mask="{name,age}",
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model)
def get(self):
return {"name": "John Doe", "age": 42, "boolean": True}
data = client.get_json("/test/")
assert data == {"name": "John Doe", "age": 42}
def test_marshal_with_honour_header_field_mask_with_default_model_mask(
self, app, client
):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
mask="{name,age}",
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model)
def get(self):
return {"name": "John Doe", "age": 42, "boolean": True}
data = client.get_json("/test/", headers={"X-Fields": "{name}"})
assert data == {"name": "John Doe"}
def test_marshal_with_honour_header_default_mask_with_default_model_mask(
self, app, client
):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
mask="{name,boolean}",
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model, mask="{name}")
def get(self):
return {"name": "John Doe", "age": 42, "boolean": True}
data = client.get_json("/test/")
assert data == {"name": "John Doe"}
def test_marshal_with_honour_header_field_mask_with_default_mask(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model, mask="{name,age}")
def get(self):
return {"name": "John Doe", "age": 42, "boolean": True}
data = client.get_json("/test/", headers={"X-Fields": "{name}"})
assert data == {"name": "John Doe"}
def test_marshal_with_honour_header_field_mask_with_default_mask_and_default_model_mask(
self, app, client
):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
mask="{name,boolean}",
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model, mask="{name,age}")
def get(self):
return {"name": "John Doe", "age": 42, "boolean": True}
data = client.get_json("/test/", headers={"X-Fields": "{name}"})
assert data == {"name": "John Doe"}
def test_marshal_with_honour_custom_field_mask(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model)
def get(self):
return {"name": "John Doe", "age": 42, "boolean": True}
app.config["RESTX_MASK_HEADER"] = "X-Mask"
data = client.get_json("/test/", headers={"X-Mask": "{name,age}"})
assert data == {"name": "John Doe", "age": 42}
def test_marshal_does_not_hit_unrequired_attributes(self, app, client):
api = Api(app)
model = api.model(
"Person",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
)
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
@property
def boolean(self):
raise Exception()
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model)
def get(self):
return Person("John Doe", 42)
data = client.get_json("/test/", headers={"X-Fields": "{name,age}"})
assert data == {"name": "John Doe", "age": 42}
def test_marshal_with_skip_missing_fields(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
},
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model)
def get(self):
return {
"name": "John Doe",
"age": 42,
}
data = client.get_json("/test/", headers={"X-Fields": "{name,missing}"})
assert data == {"name": "John Doe"}
def test_marshal_handle_inheritance(self, app):
api = Api(app)
person = api.model(
"Person",
{
"name": fields.String,
"age": fields.Integer,
},
)
child = api.inherit(
"Child",
person,
{
"extra": fields.String,
},
)
data = {"name": "John Doe", "age": 42, "extra": "extra"}
values = (
("name", {"name": "John Doe"}),
("name,extra", {"name": "John Doe", "extra": "extra"}),
("extra", {"extra": "extra"}),
)
for value, expected in values:
result = marshal(data, child, mask=value)
assert result == expected
def test_marshal_with_handle_polymorph(self, app, client):
api = Api(app)
parent = api.model(
"Person",
{
"name": fields.String,
},
)
child1 = api.inherit(
"Child1",
parent,
{
"extra1": fields.String,
},
)
child2 = api.inherit(
"Child2",
parent,
{
"extra2": fields.String,
},
)
class Child1(object):
name = "child1"
extra1 = "extra1"
class Child2(object):
name = "child2"
extra2 = "extra2"
mapping = {Child1: child1, Child2: child2}
thing = api.model(
"Thing",
{
"owner": fields.Polymorph(mapping),
},
)
@api.route("/thing-1/")
class Thing1Resource(Resource):
@api.marshal_with(thing)
def get(self):
return {"owner": Child1()}
@api.route("/thing-2/")
class Thing2Resource(Resource):
@api.marshal_with(thing)
def get(self):
return {"owner": Child2()}
data = client.get_json("/thing-1/", headers={"X-Fields": "owner{name}"})
assert data == {"owner": {"name": "child1"}}
data = client.get_json("/thing-1/", headers={"X-Fields": "owner{extra1}"})
assert data == {"owner": {"extra1": "extra1"}}
data = client.get_json("/thing-2/", headers={"X-Fields": "owner{name}"})
assert data == {"owner": {"name": "child2"}}
def test_raise_400_on_invalid_mask(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
},
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model)
def get(self):
pass
response = client.get("/test/", headers={"X-Fields": "name{,missing}"})
assert response.status_code == 400
assert response.content_type == "application/json"
class SwaggerMaskHeaderTest(object):
def test_marshal_with_expose_mask_header(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model)
def get(self):
return {"name": "John Doe", "age": 42, "boolean": True}
specs = client.get_specs()
op = specs["paths"]["/test/"]["get"]
assert "parameters" in op
assert len(op["parameters"]) == 1
param = op["parameters"][0]
assert param["name"] == "X-Fields"
assert param["type"] == "string"
assert param["format"] == "mask"
assert param["in"] == "header"
assert "required" not in param
assert "default" not in param
def test_marshal_with_expose_custom_mask_header(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model)
def get(self):
return {"name": "John Doe", "age": 42, "boolean": True}
app.config["RESTX_MASK_HEADER"] = "X-Mask"
specs = client.get_specs()
op = specs["paths"]["/test/"]["get"]
assert "parameters" in op
assert len(op["parameters"]) == 1
param = op["parameters"][0]
assert param["name"] == "X-Mask"
def test_marshal_with_disabling_mask_header(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model)
def get(self):
return {"name": "John Doe", "age": 42, "boolean": True}
app.config["RESTX_MASK_SWAGGER"] = False
specs = client.get_specs()
op = specs["paths"]["/test/"]["get"]
assert "parameters" not in op
def test_is_only_exposed_on_marshal_with(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
)
@api.route("/test/")
class TestResource(Resource):
def get(self):
return api.marshal(
{"name": "John Doe", "age": 42, "boolean": True}, model
)
specs = client.get_specs()
op = specs["paths"]["/test/"]["get"]
assert "parameters" not in op
def test_marshal_with_expose_default_mask_header(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model, mask="{name,age}")
def get(self):
pass
specs = client.get_specs()
op = specs["paths"]["/test/"]["get"]
assert "parameters" in op
assert len(op["parameters"]) == 1
param = op["parameters"][0]
assert param["name"] == "X-Fields"
assert param["type"] == "string"
assert param["format"] == "mask"
assert param["default"] == "{name,age}"
assert param["in"] == "header"
assert "required" not in param
def test_marshal_with_expose_default_model_mask_header(self, app, client):
api = Api(app)
model = api.model(
"Test",
{
"name": fields.String,
"age": fields.Integer,
"boolean": fields.Boolean,
},
mask="{name,age}",
)
@api.route("/test/")
class TestResource(Resource):
@api.marshal_with(model)
def get(self):
pass
specs = client.get_specs()
definition = specs["definitions"]["Test"]
assert "x-mask" in definition
assert definition["x-mask"] == "{name,age}"