def test_item_path(fconfig: Config, db: SQLAlchemy): """ Tests that the URL converter works for the item endpoints of the resources. The following test has set an URL converter of type int, and will allow only integers using the flask rules. """ DeviceDef, *_ = fconfig.RESOURCE_DEFINITIONS # type: Tuple[ResourceDef, ...] def cannot_find(id): assert id == 1 return Response(status=200) DeviceDef.VIEW.one = MagicMock(side_effect=cannot_find) app = Teal(config=fconfig, db=db) client = app.test_client() # type: Client with populated_db(db, app): # All ok, we expect an int and got an int client.get(res=DeviceDef.type, item=1) DeviceDef.VIEW.one.assert_called_once_with(1) # Conversion of 'int' works in here client.get(res=DeviceDef.type, item='1') assert DeviceDef.VIEW.one.call_count == 2 # Anything else fails and our function is directly not executed client.get(res=DeviceDef.type, item='foo', status=NotFound) assert DeviceDef.VIEW.one.call_count == 2
def test_resource_without_path(config: Config, db: SQLAlchemy): """Test resources that don't have url_prefix.""" class FooDef(Resource): VIEW = View __type__ = 'Foo' def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None, static_url_path=None, template_folder=None, url_prefix='', # We set url_prefix to empty string subdomain=None, url_defaults=None, root_path=None): super().__init__(app, import_name, static_folder, static_url_path, template_folder, url_prefix, subdomain, url_defaults, root_path) config.RESOURCE_DEFINITIONS = FooDef, app = Teal(config=config, db=db) with app.test_request_context(): assert url_for_resource(FooDef) == '/' assert url_for_resource(FooDef, 1) == '/1'
def test_post(fconfig: Config, db: SQLAlchemy): """ Tests posting resources, going through API (Marshmallow) and DB (SQLAlchemy) validation, and retrieving and returning a result. """ DeviceDef, ComponentDef, ComputerDef = fconfig.RESOURCE_DEFINITIONS # type: Tuple[ResourceDef] Computer = ComputerDef.MODEL Component = ComponentDef.MODEL PC = { 'id': 1, 'model': 'foo', 'components': [{'id': 2, 'type': 'Component'}, {'id': 3, 'type': 'Component'}] } def post(): pc = request.get_json() pc = Computer(**pc) db.session.add(pc) db.session.commit() return Response(status=201) def _one(id): pc = Computer.query.filter_by(id=id).first() return_pc = { 'id': pc.id, 'model': pc.model, 'type': pc.type, } # todo convert components to JSON return return_pc def one(id: int): return jsonify(_one(id)) def find(_): return jsonify([_one(1)]) ComputerDef.VIEW.post = MagicMock(side_effect=post) ComputerDef.VIEW.one = MagicMock(side_effect=one) ComputerDef.VIEW.find = MagicMock(side_effect=find) app = Teal(config=fconfig, db=db) client = app.test_client() # type: Client with populated_db(db, app): client.post(res=ComputerDef.type, data=PC) # Wrong data data, _ = client.post(res=ComputerDef.type, data={'id': 'foo'}, status=ValidationError) assert data == { 'code': 422, 'type': 'ValidationError', 'message': {'id': ['Not a valid integer.']} } # Get the first data data, _ = client.get(res=ComputerDef.type, item=1) assert data == {'id': 1, 'model': 'foo', 'type': 'Computer'} # Get all data data, _ = client.get(res=ComputerDef.type) assert data == [{'id': 1, 'model': 'foo', 'type': 'Computer'}]
def test_http_exception(fconfig: Config, db: SQLAlchemy): """Tests correct handling of HTTP exceptions.""" DeviceDef, *_ = fconfig.RESOURCE_DEFINITIONS # type: Tuple[ResourceDef] DeviceDef.VIEW.get = MagicMock(side_effect=NotFound) client = Teal(config=fconfig, db=db).test_client() # type: Client d, _ = client.get(res=DeviceDef.type, status=NotFound) assert d['code'] == 404
def test_token_auth_view(db: SQLAlchemy): """ Ensures that an authorization endpoint correctly protects against wrong credentials (this case tokens), allowing the endpoint to only specific cases. """ class TestTokenAuth(TokenAuth): authenticate = MagicMock(side_effect=Unauthorized) class FooSchema(Schema): pass class FooView(View): get = MagicMock(side_effect=lambda id: jsonify({'did': 'it!'})) class Foo(db.Model): id = db.Column(db.Integer, primary_key=True) class FooDef(Resource): SCHEMA = FooSchema VIEW = FooView MODEL = Foo AUTH = True class TestConfig(Config): RESOURCE_DEFINITIONS = [FooDef] app = Teal(config=TestConfig(), Auth=TestTokenAuth, db=db) client = app.test_client() # No token # No auth header sent client.get(res=FooSchema.t, status=Unauthorized) assert TestTokenAuth.authenticate.call_count == 0 # Wrong format # System couldn't parse Auth header client.get(res=FooSchema.t, token='this is wrong', status=Unauthorized) assert TestTokenAuth.authenticate.call_count == 0 # Wrong credentials # System can parse credentials but they are incorrect client.get(res=FooSchema.t, token=b64encode(b'nok:').decode(), status=Unauthorized) # Authenticate method was hit assert TestTokenAuth.authenticate.call_count == 1 # OK # Our authenticate method now returns some dummy user instead of # raising Unauthorized TestTokenAuth.authenticate = MagicMock(return_value={'id': '1'}) data, _ = client.get(res=FooSchema.t, token=b64encode(b'ok:').decode()) TestTokenAuth.authenticate.assert_called_once_with('ok', '') # The endpoint was hit assert data == {'did': 'it!'} FooView.get.assert_called_once_with(id=None)
def test_init_db(db: SQLAlchemy, config: Config): """Tests :meth:`teal.resource.Resource.init_db` with one inventory.""" class Foo(db.Model): id = Column(db.Integer, primary_key=True) class FooDef(Resource): __type__ = 'Foo' def init_db(self, db: SQLAlchemy, exclude_schema=None): db.session.add(Foo()) config.RESOURCE_DEFINITIONS = FooDef, app = Teal(config=config, db=db) with app.app_context(): app.init_db() with app.app_context(): # If no commit happened in init_db() or anything else # this would not exist assert Foo.query.filter_by(id=1).one() # Test again but executing init-db through the command-line runner = app.test_cli_runner() runner.invoke('init-db') with app.app_context(): assert Foo.query.filter_by(id=2).one() # Test with --erase option runner.invoke('init-db', '--erase') with app.app_context(): assert Foo.query.count() == 1
def test_cors(fconfig: Config, db: SchemaSQLAlchemy): DeviceDef, *_ = fconfig.RESOURCE_DEFINITIONS # type: Tuple[ResourceDef] def foo(*args, **kw): return Response(status=200) DeviceDef.VIEW.get = MagicMock(side_effect=foo) client = Teal(config=fconfig, db=db).test_client() # type: Client _, response = client.get('/devices/') headers = response.headers.to_list() assert ('Access-Control-Expose-Headers', 'Authorization') in headers assert ('Access-Control-Allow-Origin', '*') in headers
def test_nested_on(fconfig: Config, db: SQLAlchemy): """Tests the NestedOn marshmallow field.""" DeviceDef, ComponentDef, ComputerDef = fconfig.RESOURCE_DEFINITIONS class GraphicCardSchema(ComponentDef.SCHEMA): speed = Integer() class GraphicCard(ComponentDef.MODEL): speed = db.Column(db.Integer) class GraphicCardDef(ComponentDef): SCHEMA = GraphicCardSchema MODEL = GraphicCard fconfig.RESOURCE_DEFINITIONS += (GraphicCardDef,) app = Teal(config=fconfig, db=db) pc_template = { 'id': 1, 'components': [ {'id': 2, 'type': 'Component'}, {'id': 3, 'type': 'GraphicCard', 'speed': 4} ] } with app.app_context(): schema = app.resources['Computer'].SCHEMA() result = schema.load(pc_template) assert pc_template['id'] == result['id'] assert isinstance(result['components'][0], ComponentDef.MODEL) assert isinstance(result['components'][1], GraphicCardDef.MODEL) # Let's add the graphic card's speed field to the component with pytest.raises(ValidationError, message={'components': {'speed': ['Unknown field']}}): pc = deepcopy(pc_template) pc['components'][0]['speed'] = 4 schema.load(pc) # Let's remove the 'type' with pytest.raises(ValidationError, message={ 'components': ['\'Type\' field required to disambiguate resources.'] }): pc = deepcopy(pc_template) del pc['components'][0]['type'] del pc['components'][1]['type'] schema.load(pc) # Let's set a 'type' that is not a Component with pytest.raises(ValidationError, message={'components': ['Computer is not a sub-type of Component']}): pc = deepcopy(pc_template) pc['components'][0]['type'] = 'Computer' schema.load(pc)
def test_etag_secondary(client: Client, app: Teal): """Tests creating, linking and accessing an ETag through its secondary (NFC) id.""" with app.app_context(): et = ETag(secondary='NFCID') db.session.add(et) db.session.commit() client.get('/', item='NFCID', accept=ANY, status=400) with app.app_context(): tag = ETag.query.filter_by(secondary='NFCID').one() tag.devicehub = URL('https://dh.com') db.session.commit() _, r = client.get('/', item='NFCID', accept=ANY, status=302) assert r.location == 'https://dh.com/tags/FO-3MP5M/device'
def test_tag_export(runner: FlaskCliRunner, app: Teal): with app.app_context(): t = ETag() db.session.add(t) db.session.commit() with NamedTemporaryFile('r+') as f: result = runner.invoke(args=('export', f.name), catch_exceptions=False) assert result.exit_code == 0 result = runner.invoke(args=('import', f.name), catch_exceptions=False) assert result.exit_code == 0 with app.app_context(): t = Tag() db.session.add(t) db.session.commit() assert Tag.decode(t.id) == 2
def test_http_exception(fconfig: Config, db: SQLAlchemy): """Tests correct handling of HTTP exceptions.""" DeviceDef, *_ = fconfig.RESOURCE_DEFINITIONS # type: Tuple[ResourceDef] DeviceDef.VIEW.get = MagicMock(side_effect=NotFound) client = Teal(config=fconfig, db=db).test_client() # type: Client d, _ = client.get(res=DeviceDef.type, status=NotFound) assert d == { 'code': 404, 'message': '404 Not Found: The requested URL was not found on the server. ' 'If you entered the URL manually please check your spelling and try again.', 'type': 'NotFound' }
def test_json_encoder(app: Teal): """ Ensures that Teal is using the custom JSON Encoder through Flask's json. """ with app.app_context(): # Try to dump a type flask's json encoder cannot handle json.dumps({'foo': StrictVersion('1.3')})
def test_get_not_linked_tag(app: Teal, client: Client): """Tests getting a tag that has not been linked yet to a Devicehub.""" with app.app_context(): t = Tag() db.session.add(t) db.session.commit() id = t.id client.get(res=Tag.t, item=id, status=NoRemoteTag)
def test_args(fconfig: Config, db: SQLAlchemy): """Tests the handling of query arguments in the URL.""" DeviceDef, *_ = fconfig.RESOURCE_DEFINITIONS # type: Tuple[ResourceDef] class FindArgsFoo(DeviceDef.VIEW.FindArgs): foo = Integer() DeviceDef.VIEW.FindArgs = FindArgsFoo def find(args: dict): assert args == {'foo': 25} return Response(status=200) DeviceDef.VIEW.find = MagicMock(side_effect=find) client = Teal(config=fconfig, db=db).test_client() # type: Client # Ok client.get(res=DeviceDef.type, query=[('foo', 25)]) # Extra not needed data client.get(res=DeviceDef.type, query=[('foo', 25), ('bar', 'nope')]) # Wrong data r, _ = client.get(res=DeviceDef.type, query=[('foo', 'nope')], status=UnprocessableEntity)
def test_not_found(app: Teal): """ When not finding a resource, the db should raise a ``NotFound`` exception instead of the built-in for SQLAlchemy. """ with app.app_context(): Device = app.resources['Device'].MODEL with pytest.raises(NotFound): Device.query.one()
def test_models(app: Teal, db: SQLAlchemy): """Checks that the models used in the fixture work.""" DeviceDef, ComponentDef, ComputerDef = \ app.config['RESOURCE_DEFINITIONS'] # type: Tuple[ResourceDef] Component = ComponentDef.MODEL Computer = ComputerDef.MODEL component = Component(id=1, model='foo') pc = Computer(id=2, model='bar', components=[component]) with app.app_context(): db.session.add(pc) queried_pc = Computer.query.first() assert pc == queried_pc
def test_validator_is_type(app: Teal): """Checks the validator IsType""" with app.app_context(): is_type = IsType() is_type('Device') is_type('Component') with pytest.raises(ValidationError): is_type('Foo') is_subtype = IsType('Component') is_subtype('Component') with pytest.raises(ValidationError): is_subtype('Computer')
def app(request): class TestConfig(TagsConfig): SQLALCHEMY_DATABASE_URI = 'postgresql://*****:*****@localhost/tagtest' # SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/tagtest' TAG_PROVIDER_ID = 'FO' TAG_HASH_SALT = 'So salty' SERVER_NAME = 'foo.bar' TESTING = True DEVICEHUBS = {'soToken': 'https://dh.com'} app = Teal(config=TestConfig(), db=db, Auth=Auth) db.create_all(app=app) # More robust than 'yield' request.addfinalizer(lambda *args, **kw: db.drop_all(app=app)) return app
def test_tag_creation(app: Teal): with app.app_context(): t = Tag() db.session.add(t) db.session.commit() assert t.id assert t.devicehub is None assert t.id == '3MP5M' et = ETag() db.session.add(et) db.session.commit() assert et.id == 'FO-WNBRM' assert et.id.split('-')[0] == 'FO' assert len(et.id.split('-')[1]) == 5 assert et.devicehub is None
def test_inheritance_access(fconfig: Config, db: SQLAlchemy): """ Tests that the right endpoint is called when accessing sub-resources. """ DeviceDef, ComponentDef, ComputerDef = fconfig.RESOURCE_DEFINITIONS # type: Tuple[ResourceDef] DUMMY_DICT = {'ok': 'yes'} def foo(*args, **kw): return jsonify(DUMMY_DICT) DeviceDef.VIEW.get = MagicMock(side_effect=foo) ComponentDef.VIEW.get = MagicMock(side_effect=foo) ComputerDef.VIEW.get = MagicMock(side_effect=foo) client = Teal(config=fconfig, db=db).test_client() # type: Client # Access any non-defined URI client.get(uri='/this-does-not-exist', status=NotFound) assert DeviceDef.VIEW.get.call_count == \ ComponentDef.VIEW.get.call_count == \ ComputerDef.VIEW.get.call_count == 0 # Access to a non-defined method for a resource client.post(res=DeviceDef.type, status=MethodNotAllowed, data=dict()) assert DeviceDef.VIEW.get.call_count == \ ComponentDef.VIEW.get.call_count == \ ComputerDef.VIEW.get.call_count == 0 # Get top resource Device # Only device endpoint is called d, _ = client.get(res=DeviceDef.type) assert d == DUMMY_DICT DeviceDef.VIEW.get.assert_called_once_with(id=None) assert ComponentDef.VIEW.get.call_count == 0 assert ComputerDef.VIEW.get.call_count == 0 # Get computer # Only component endpoint is called d, _ = client.get(res=ComputerDef.type) assert d == DUMMY_DICT assert DeviceDef.VIEW.get.call_count == 1 # from before assert ComponentDef.VIEW.get.call_count == 0 ComputerDef.VIEW.get.assert_called_once_with(id=None)
def test_query_sort(app: Teal): """Tests sorting params.""" with app.app_context(): Device = app.resources['Device'].MODEL class Sorting(Sort): models = SortField(Device.model) ids = SortField(Device.id) schema = Sorting() sort = tuple( str(s) for s in schema.load({ 'models': True, 'ids': False })) assert len(sort) == 2 assert 'device.model ASC' in sort assert 'device.id DESC' in sort
def test_query_join(app: Teal): """Checks that nested queries work.""" with app.app_context(): Device = app.resources['Device'].MODEL Computer = app.resources['Computer'].MODEL class Inner(Query): foo = ILike(Device.model) class Q(Query): idq = Between(Device.id, Integer()) componentq = Join(Device.id == Computer.id, Inner) schema = Q() query = schema.load({'idq': [1, 4], 'componentq': {'foo': 'bar'}}) s, params = compiled(Device, query) assert 'device.id BETWEEN %(id_1)s AND %(id_2)s' in s assert 'device.model ILIKE %(model_1)s' in s assert params == {'id_1': 1, 'model_1': 'bar%', 'id_2': 4}
def test_query(app: Teal): with app.app_context(): Device = app.resources['Device'].MODEL class Q(Query): idq = Or(Equal(Device.id, Str())) modelq = ILike(Device.model) schema = Q() query = schema.load({'idq': ['a', 'b', 'c'], 'modelq': 'foobar'}) s, params = compiled(Device, query) # Order between query clauses can change assert 'device.model ILIKE %(model_1)s' in s assert '(device.id = %(id_1)s OR device.id = %(id_2)s OR device.id = %(id_3)s' in s assert params == { 'id_2': 'b', 'id_3': 'c', 'id_1': 'a', 'model_1': 'foobar%' }
def app_context(app: Teal): with app.app_context(): yield
def app(fconfig: Config, db: SQLAlchemy) -> Teal: app = Teal(config=fconfig, db=db) with app.app_context(): app.init_db() yield app
def test_url_for_resource(app: Teal): with app.test_request_context(): # Note we need a test_request_context or flask won't know # which base_url to use. assert url_for_resource('Computer') == '/computers/' assert url_for_resource('Computer', 24) == '/computers/24'
def client(app: Teal) -> Client: return app.test_client()
def runner(app: Teal) -> FlaskCliRunner: return app.test_cli_runner()
from ereuse_tag.auth import Auth from ereuse_tag.config import TagsConfig from ereuse_tag.db import db from teal.teal import Teal from teal.auth import TokenAuth class DeviceTagConf(TagsConfig): TAG_PROVIDER_ID = 'DT' TAG_HASH_SALT = '$6f/Wspgaswc1xJq5xj' SQLALCHEMY_DATABASE_URI = 'postgresql://*****:*****@localhost/tags' DEVICEHUBS = { '7ad6eb73-d95c-4cdf-bf9f-b33be4e514b3': 'http://localhost:5000/testdb' } API_DOC_CONFIG_TITLE = 'Tags' API_DOC_CONFIG_VERSION = '0.1' API_DOC_CONFIG_COMPONENTS = { 'securitySchemes': { 'bearerAuth': TokenAuth.API_DOCS } } API_DOC_CLASS_DISCRIMINATOR = 'type' app = Teal(config=DeviceTagConf(), db=db, Auth=Auth)