1114 lines
32 KiB
Python
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}"
|