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}"