From ac26d8d6109cfab169ef70e5528bf7d65d9d6f78 Mon Sep 17 00:00:00 2001 From: Xavier Bustamante Talavera Date: Fri, 11 May 2018 18:58:48 +0200 Subject: [PATCH] Get resources correctly with polymorphic --- ereuse_devicehub/client.py | 24 ++++ ereuse_devicehub/config.py | 6 +- ereuse_devicehub/marshmallow.py | 1 + ereuse_devicehub/resources/device/__init__.py | 8 +- ereuse_devicehub/resources/device/models.py | 7 ++ ereuse_devicehub/resources/device/schemas.py | 34 ++++-- ereuse_devicehub/resources/device/sync.py | 2 - ereuse_devicehub/resources/device/views.py | 8 +- ereuse_devicehub/resources/event/schemas.py | 15 +-- ereuse_devicehub/resources/event/views.py | 10 +- ereuse_devicehub/resources/schemas.py | 10 +- ereuse_devicehub/resources/user/__init__.py | 7 +- ereuse_devicehub/resources/user/schemas.py | 28 ++++- ereuse_devicehub/resources/user/views.py | 11 +- tests/conftest.py | 1 - .../1-device-with-components.snapshot.yaml | 27 +++++ ...ice-with-components-of-first.snapshot.yaml | 16 +++ ...-and-adding-processor-from-2.snapshot.yaml | 17 +++ ...ssor.snapshot-and-adding-graphic-card.yaml | 15 +++ tests/test_device.py | 111 ++++++++++++------ tests/test_snapshot.py | 64 +++++++++- 21 files changed, 344 insertions(+), 78 deletions(-) create mode 100644 tests/files/1-device-with-components.snapshot.yaml create mode 100644 tests/files/2-second-device-with-components-of-first.snapshot.yaml create mode 100644 tests/files/3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot.yaml create mode 100644 tests/files/4-first-device-but-removing-processor.snapshot-and-adding-graphic-card.yaml diff --git a/ereuse_devicehub/client.py b/ereuse_devicehub/client.py index 12e2882d..0eef1c53 100644 --- a/ereuse_devicehub/client.py +++ b/ereuse_devicehub/client.py @@ -1,7 +1,11 @@ +from typing import Type, Union + +from boltons.typeutils import issubclass from ereuse_utils.test import JSON from flask import Response from werkzeug.exceptions import HTTPException +from ereuse_devicehub.resources.models import Thing from teal.client import Client as TealClient @@ -10,6 +14,26 @@ class Client(TealClient): allow_subdomain_redirects=False): super().__init__(application, response_wrapper, use_cookies, allow_subdomain_redirects) + def open(self, uri: str, res: str or Type[Thing] = None, status: int or HTTPException = 200, + query: dict = {}, accept=JSON, content_type=JSON, item=None, headers: dict = None, + token: str = None, **kw) -> (dict or str, Response): + if issubclass(res, Thing): + res = res.__name__ + return super().open(uri, res, status, query, accept, content_type, item, headers, token, + **kw) + + def get(self, uri: str = '', res: Union[Type[Thing], str] = None, query: dict = {}, + status: int or HTTPException = 200, item: Union[int, str] = None, accept: str = JSON, + headers: dict = None, token: str = None, **kw) -> (dict or str, Response): + return super().get(uri, res, query, status, item, accept, headers, token, **kw) + + def post(self, data: str or dict, uri: str = '', res: Union[Type[Thing], str] = None, + query: dict = {}, status: int or HTTPException = 201, content_type: str = JSON, + accept: str = JSON, headers: dict = None, token: str = None, **kw) -> ( + dict or str, Response): + return super().post(data, uri, res, query, status, content_type, accept, headers, token, + **kw) + def login(self, email: str, password: str): assert isinstance(email, str) assert isinstance(password, str) diff --git a/ereuse_devicehub/config.py b/ereuse_devicehub/config.py index 6915d301..87ea0b1e 100644 --- a/ereuse_devicehub/config.py +++ b/ereuse_devicehub/config.py @@ -2,7 +2,7 @@ from distutils.version import StrictVersion from ereuse_devicehub.resources.device import ComponentDef, ComputerDef, DesktopDef, DeviceDef, \ GraphicCardDef, HardDriveDef, LaptopDef, MicrotowerDef, MotherboardDef, NetbookDef, \ - NetworkAdapterDef, RamModuleDef, ServerDef + NetworkAdapterDef, ProcessorDef, RamModuleDef, ServerDef from ereuse_devicehub.resources.event import EventDef, SnapshotDef from ereuse_devicehub.resources.user import UserDef from teal.config import Config @@ -12,8 +12,8 @@ class DevicehubConfig(Config): RESOURCE_DEFINITIONS = ( DeviceDef, ComputerDef, DesktopDef, LaptopDef, NetbookDef, ServerDef, MicrotowerDef, ComponentDef, GraphicCardDef, HardDriveDef, MotherboardDef, NetworkAdapterDef, - RamModuleDef, UserDef, EventDef, SnapshotDef + RamModuleDef, ProcessorDef, UserDef, EventDef, SnapshotDef ) PASSWORD_SCHEMES = {'pbkdf2_sha256'} SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh-db1' - MIN_WORKBENCH = StrictVersion('11.0') \ No newline at end of file + MIN_WORKBENCH = StrictVersion('11.0') diff --git a/ereuse_devicehub/marshmallow.py b/ereuse_devicehub/marshmallow.py index 0c672f54..113b3bc8 100644 --- a/ereuse_devicehub/marshmallow.py +++ b/ereuse_devicehub/marshmallow.py @@ -6,6 +6,7 @@ from teal.marshmallow import NestedOn as TealNestedOn class NestedOn(TealNestedOn): + __doc__ = TealNestedOn.__doc__ def __init__(self, nested, polymorphic_on='type', db: SQLAlchemy = db, default=missing_, exclude=tuple(), only=None, **kwargs): diff --git a/ereuse_devicehub/resources/device/__init__.py b/ereuse_devicehub/resources/device/__init__.py index 1acafb2f..7d426c54 100644 --- a/ereuse_devicehub/resources/device/__init__.py +++ b/ereuse_devicehub/resources/device/__init__.py @@ -1,6 +1,6 @@ from ereuse_devicehub.resources.device.schemas import Component, Computer, Desktop, Device, \ - GraphicCard, HardDrive, Laptop, Microtower, Motherboard, Netbook, NetworkAdapter, RamModule, \ - Server + GraphicCard, HardDrive, Laptop, Microtower, Motherboard, Netbook, NetworkAdapter, Processor, \ + RamModule, Server from ereuse_devicehub.resources.device.views import DeviceView from teal.resource import Converters, Resource @@ -58,3 +58,7 @@ class NetworkAdapterDef(ComponentDef): class RamModuleDef(ComponentDef): SCHEMA = RamModule + + +class ProcessorDef(ComponentDef): + SCHEMA = Processor diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index c45921bf..5a73c6e0 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -144,6 +144,13 @@ class NetworkAdapter(Component): speed = Column(SmallInteger, check_range('speed', min=10, max=10000)) # type: int +class Processor(Component): + id = Column(BigInteger, ForeignKey(Component.id), primary_key=True) # type: int + speed = Column(Float, check_range('speed', 0.1, 15)) + cores = Column(SmallInteger, check_range('cores', 1, 10)) + address = Column(SmallInteger, check_range('address', 8, 256)) + + class RamModule(Component): id = Column(BigInteger, ForeignKey(Component.id), primary_key=True) # type: int size = Column(SmallInteger, check_range('size', min=128, max=17000)) diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index 9cfacae3..eeb0a8a5 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -1,6 +1,8 @@ -from marshmallow.fields import Float, Integer, Nested, Str -from marshmallow.validate import Length, Range +from marshmallow import post_dump +from marshmallow.fields import Float, Integer, Str +from marshmallow.validate import Length, OneOf, Range +from ereuse_devicehub.marshmallow import NestedOn from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE from ereuse_devicehub.resources.schemas import Thing, UnitCodes @@ -29,11 +31,21 @@ class Device(Thing): height = Float(validate=Range(0.1, 3), unit=UnitCodes.m, description='The height of the device in meters.') - events = Nested('Event', many=True, dump_only=True, only='id') + events = NestedOn('Event', many=True, dump_only=True) + events_one = NestedOn('Event', many=True, dump_only=True, description='Not used.') + events_components = NestedOn('Event', many=True, dump_only=True, description='Not used.') + + @post_dump + def merge_events(self, data: dict) -> dict: + if isinstance(data.get('events_one', None), list): + data.setdefault('events', []).extend(data.pop('events_one')) + if isinstance(data.get('events_components', None), list): + data.setdefault('events', []).extend(data.pop('events_components')) + return data class Computer(Device): - components = Nested('Component', many=True, dump_only=True, only='id') + components = NestedOn('Component', many=True, dump_only=True) pass @@ -58,7 +70,7 @@ class Microtower(Computer): class Component(Device): - parent = Nested(Device, dump_only=True, only='id') + parent = NestedOn(Device, dump_only=True) class GraphicCard(Component): @@ -71,9 +83,9 @@ class HardDrive(Component): size = Integer(validate=Range(0, 10 ** 8), unit=UnitCodes.mbyte, description='The size of the hard-drive in MB.') - erasure = Nested('EraseBasic', load_only=True) - tests = Nested('TestHardDrive', many=True, load_only=True) - benchmarks = Nested('BenchmarkHardDrive', load_only=True, many=True) + erasure = NestedOn('EraseBasic', load_only=True) + tests = NestedOn('TestHardDrive', many=True, load_only=True) + benchmarks = NestedOn('BenchmarkHardDrive', load_only=True, many=True) class Motherboard(Component): @@ -90,6 +102,12 @@ class NetworkAdapter(Component): description='The maximum speed this network adapter can handle, in mbps.') +class Processor(Component): + speed = Float(validate=Range(min=0.1, max=15), unit=UnitCodes.ghz) + cores = Integer(validate=Range(min=1, max=10)) # todo from numberOfCores + address = Integer(validate=OneOf({8, 16, 32, 64, 128, 256})) + + class RamModule(Component): size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte) speed = Float(validate=Range(min=100, max=10000), unit=UnitCodes.mhz) diff --git a/ereuse_devicehub/resources/device/sync.py b/ereuse_devicehub/resources/device/sync.py index 670e2df0..6a43bae7 100644 --- a/ereuse_devicehub/resources/device/sync.py +++ b/ereuse_devicehub/resources/device/sync.py @@ -160,7 +160,6 @@ class Sync: """ events = [] old_components = set(device.components) - adding = components - old_components if adding: add = Add(device=device, components=list(adding)) @@ -177,5 +176,4 @@ class Sync: removing = old_components - components if removing: events.append(Remove(device=device, components=list(removing))) - return events diff --git a/ereuse_devicehub/resources/device/views.py b/ereuse_devicehub/resources/device/views.py index b599a0aa..eff67a45 100644 --- a/ereuse_devicehub/resources/device/views.py +++ b/ereuse_devicehub/resources/device/views.py @@ -5,4 +5,10 @@ from teal.resource import View class DeviceView(View): def one(self, id: int): """Gets one device.""" - return Device.query.filter_by(id=id).one() + device = Device.query.filter_by(id=id).one() + return self.schema.jsonify_polymorphic(device) + + def find(self, args: dict): + """Gets many devices""" + devices = Device.query.all() + return self.schema.jsonify_polymorphic_many(devices) diff --git a/ereuse_devicehub/resources/event/schemas.py b/ereuse_devicehub/resources/event/schemas.py index aa93af61..cbb716d0 100644 --- a/ereuse_devicehub/resources/event/schemas.py +++ b/ereuse_devicehub/resources/event/schemas.py @@ -30,17 +30,17 @@ class Event(Thing): 'hardware without margin of doubt.') incidence = Boolean(default=False, description='Was something wrong in this event?') - snapshot = Nested('Snapshot', dump_only=True, only='id') + snapshot = NestedOn('Snapshot', dump_only=True, only='id') description = String(default='', description='A comment about the event.') - components = Nested(Component, dump_only=True, only='id', many=True) + components = NestedOn(Component, dump_only=True, many=True) class EventWithOneDevice(Event): - device = Nested(Device, only='id') + device = NestedOn(Device, only='id') class EventWithMultipleDevices(Event): - device = Nested(Device, many=True, only='id') + device = NestedOn(Device, many=True, only='id') class Add(EventWithOneDevice): @@ -52,14 +52,14 @@ class Remove(EventWithOneDevice): class Allocate(EventWithMultipleDevices): - to = Nested(User, only='id', - description='The user the devices are allocated to.') + to = NestedOn(User, + description='The user the devices are allocated to.') organization = String(validate=Length(STR_SIZE), description='The organization where the user was when this happened.') class Deallocate(EventWithMultipleDevices): - from_rel = Nested(User, only='id', + from_rel = Nested(User, data_key='from', description='The user where the devices are not allocated to anymore.') organization = String(validate=Length(STR_SIZE), @@ -138,6 +138,7 @@ class Snapshot(EventWithOneDevice): color = Color(description='Main color of the device.') orientation = EnumField(Orientation, description='Is the device main stand wider or larger?') force_creation = Boolean(data_key='forceCreation') + events = NestedOn(Event, many=True) @validates_schema def validate_workbench_version(self, data: dict): diff --git a/ereuse_devicehub/resources/event/views.py b/ereuse_devicehub/resources/event/views.py index 658b97ce..28f9edf6 100644 --- a/ereuse_devicehub/resources/event/views.py +++ b/ereuse_devicehub/resources/event/views.py @@ -1,6 +1,6 @@ from distutils.version import StrictVersion -from flask import request, Response +from flask import request from ereuse_devicehub.db import db from ereuse_devicehub.resources.device.sync import Sync @@ -28,12 +28,14 @@ class SnapshotView(View): device = s.pop('device') components = s.pop('components') if s['software'] == SoftwareType.Workbench else None # noinspection PyArgumentList - del s['type'] snapshot = Snapshot(**s) snapshot.device, snapshot.events = Sync.run(device, components, snapshot.force_creation) + snapshot.components = snapshot.device.components db.session.add(snapshot) - # transform it back - return Response(status=201) + db.session.flush() # Take to DB so we get db-generated values + ret = self.schema.jsonify(snapshot) # transform it back + ret.status_code = 201 + return ret class TestHardDriveView(View): diff --git a/ereuse_devicehub/resources/schemas.py b/ereuse_devicehub/resources/schemas.py index 4aa16be1..6f329c3d 100644 --- a/ereuse_devicehub/resources/schemas.py +++ b/ereuse_devicehub/resources/schemas.py @@ -1,7 +1,9 @@ from enum import Enum -from marshmallow.fields import DateTime, List, Nested, URL, String +from marshmallow import post_load +from marshmallow.fields import DateTime, List, String, URL +from ereuse_devicehub.marshmallow import NestedOn from teal.resource import Schema @@ -22,4 +24,8 @@ class Thing(Schema): same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs') updated = DateTime('iso', dump_only=True) created = DateTime('iso', dump_only=True) - author = Nested('User', only='id', dump_only=True) + author = NestedOn('User', dump_only=True, exclude=('token',)) + + @post_load + def remove_type(self, data: dict): + data.pop('type', None) diff --git a/ereuse_devicehub/resources/user/__init__.py b/ereuse_devicehub/resources/user/__init__.py index 24deaa1f..05a6111b 100644 --- a/ereuse_devicehub/resources/user/__init__.py +++ b/ereuse_devicehub/resources/user/__init__.py @@ -28,9 +28,10 @@ class UserDef(Resource): """ Creates an user. """ - with self.app.test_request_context(): - self.schema.load({'email': email, 'password': password}) + with self.app.app_context(): + self.SCHEMA(only={'email', 'password'}, exclude=('token',)) \ + .load({'email': email, 'password': password}) user = User(email=email, password=password) db.session.add(user) db.session.commit() - return user.dump() + return self.schema.dump(user) diff --git a/ereuse_devicehub/resources/user/schemas.py b/ereuse_devicehub/resources/user/schemas.py index f7dc3937..d5c4c06b 100644 --- a/ereuse_devicehub/resources/user/schemas.py +++ b/ereuse_devicehub/resources/user/schemas.py @@ -1,6 +1,6 @@ from base64 import b64encode -from marshmallow import pre_dump +from marshmallow import post_dump from marshmallow.fields import Email, String, UUID from ereuse_devicehub.resources.schemas import Thing @@ -10,13 +10,33 @@ class User(Thing): id = UUID(dump_only=True) email = Email(required=True) password = String(load_only=True, required=True) + name = String() token = String(dump_only=True, description='Use this token in an Authorization header to access the app.' 'The token can change overtime.') - @pre_dump + def __init__(self, + only=None, + exclude=('token',), + prefix='', + many=False, + context=None, + load_only=(), + dump_only=(), + partial=False): + """ + Instantiates the User. + + By default we exclude token from both load/dump + so they are not taken / set in normal usage by mistake. + """ + super().__init__(only, exclude, prefix, many, context, load_only, dump_only, partial) + + @post_dump def base64encode_token(self, data: dict): """Encodes the token to base64 so clients don't have to.""" - # framework needs ':' at the end - data['token'] = b64encode(str.encode(str(data['token']) + ':')) + if 'token' in data: + # In many cases we don't dump the token (ex. relationships) + # Framework needs ':' at the end + data['token'] = b64encode(str.encode(str(data['token']) + ':')).decode() return data diff --git a/ereuse_devicehub/resources/user/views.py b/ereuse_devicehub/resources/user/views.py index 374cc4a8..6369a9b1 100644 --- a/ereuse_devicehub/resources/user/views.py +++ b/ereuse_devicehub/resources/user/views.py @@ -1,6 +1,6 @@ from uuid import UUID -from flask import current_app as app, request +from flask import g, request from ereuse_devicehub.resources.user.exceptions import WrongCredentials from ereuse_devicehub.resources.user.models import User @@ -14,10 +14,13 @@ class UserView(View): def login(): - user_s = app.resources['User'].schema # type: UserS - u = user_s.load(request.get_json(), partial=('email', 'password')) + # We use custom schema as we only want to parse a subset of user + user_s = g.resource_def.SCHEMA(only=('email', 'password')) # type: UserS + # noinspection PyArgumentList + u = request.get_json(schema=user_s) user = User.query.filter_by(email=u['email']).one_or_none() if user and user.password == u['password']: - return user_s.jsonify(user) + schema_with_token = g.resource_def.SCHEMA(exclude=set()) + return schema_with_token.jsonify(user) else: raise WrongCredentials() diff --git a/tests/conftest.py b/tests/conftest.py index a9dc1bb0..55041c01 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,7 +28,6 @@ def _app(config: TestConfig) -> Devicehub: @pytest.fixture() def app(request, _app: Devicehub) -> Devicehub: - db.drop_all(app=_app) # In case the test before was killed db.create_all(app=_app) # More robust than 'yield' request.addfinalizer(lambda *args, **kw: db.drop_all(app=_app)) diff --git a/tests/files/1-device-with-components.snapshot.yaml b/tests/files/1-device-with-components.snapshot.yaml new file mode 100644 index 00000000..e7249b5b --- /dev/null +++ b/tests/files/1-device-with-components.snapshot.yaml @@ -0,0 +1,27 @@ +device: + manufacturer: 'p1' + serialNumber: 'p1' + model: 'p1' + type: 'Desktop' +secured: False +components: + - manufacturer: 'p1c1m' + serialNumber: 'p1c1s' + type: 'Motherboard' + - manufacturer: 'p1c2m' + serialNumber: 'p1c2s' + model: 'p1c2' + speed: 1.23 + cores: 2 + type: 'Processor' + - manufacturer: 'p1c3m' + serialNumber: 'p1c3s' + type: 'GraphicCard' + memory: 1.5 +condition: + appearance: 'A' + functionality: 'B' +elapsed: 25 +software: 'Workbench' +uuid: '76860eca-c3fd-41f6-a801-6af7bd8cf832' +version: '11.0' \ No newline at end of file diff --git a/tests/files/2-second-device-with-components-of-first.snapshot.yaml b/tests/files/2-second-device-with-components-of-first.snapshot.yaml new file mode 100644 index 00000000..1ed31f01 --- /dev/null +++ b/tests/files/2-second-device-with-components-of-first.snapshot.yaml @@ -0,0 +1,16 @@ +device: + manufacturer: 'p2m' + serialNumber: 'p2s' + model: 'p2' + type: 'Microtower' +secured: False +components: + - manufacturer: 'p2c1m' + serialNumber: 'p2c1s' + type: 'Motherboard' + - manufacturer: 'p1c2m' + serialNumber: 'p1c2s' + model: 'p1' + speed: 1.23 + cores: 2 + type: 'Processor' diff --git a/tests/files/3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot.yaml b/tests/files/3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot.yaml new file mode 100644 index 00000000..935d83b7 --- /dev/null +++ b/tests/files/3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot.yaml @@ -0,0 +1,17 @@ +device: + manufactuer: 'p1' + serialNumber: 'p1' + model: 'p1' + type: 'Desktop' +secured: False +components: + - manufacturer: 'p1c2m' + serialNumber: 'p1c2s' + model: 'p1' + type: 'Processor' + cores: 2 + speed: 1.23 + - manufacturer: 'p1c3m' + serialNumber: 'p1c3s' + type: 'GraphicCard' + memory: 1.5 \ No newline at end of file diff --git a/tests/files/4-first-device-but-removing-processor.snapshot-and-adding-graphic-card.yaml b/tests/files/4-first-device-but-removing-processor.snapshot-and-adding-graphic-card.yaml new file mode 100644 index 00000000..a86a8842 --- /dev/null +++ b/tests/files/4-first-device-but-removing-processor.snapshot-and-adding-graphic-card.yaml @@ -0,0 +1,15 @@ +device: + manufactuer: 'p1' + serialNumber: 'p1' + model: 'p1' + type: 'Desktop' +secured: False +components: + - manufacturer: 'p1c4m' + serialNumber: 'p1c4s' + type: 'NetworkAdapter' + speed: 1000 + - manufacturer: 'p1c3m' + serialNumber: 'p1c3s' + type: 'GraphicCard' + memory: 1.5 \ No newline at end of file diff --git a/tests/test_device.py b/tests/test_device.py index bf4229d3..72302aae 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1,10 +1,11 @@ import pytest +from ereuse_devicehub.client import UserClient from ereuse_devicehub.db import db from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.resources.device.exceptions import NeedsId from ereuse_devicehub.resources.device.models import Component, Computer, Desktop, Device, \ - GraphicCard, Motherboard, NetworkAdapter + GraphicCard, Laptop, Microtower, Motherboard, NetworkAdapter from ereuse_devicehub.resources.device.schemas import Device as DeviceS from ereuse_devicehub.resources.device.sync import Sync from ereuse_devicehub.resources.event.models import Add, Remove @@ -12,46 +13,47 @@ from teal.db import ResourceNotFound from tests.conftest import file -def test_device_model(app: Devicehub): +@pytest.mark.usefixtures('app_context') +def test_device_model(): """ Tests that the correctness of the device model and its relationships. """ - with app.test_request_context(): - pc = Desktop(model='p1mo', manufacturer='p1ma', serial_number='p1s') - pc.components = components = [ - NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'), - GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500) - ] - db.session.add(pc) - db.session.commit() - pc = Desktop.query.one() - assert pc.serial_number == 'p1s' - assert pc.components == components - network_adapter = NetworkAdapter.query.one() - assert network_adapter.parent == pc + pc = Desktop(model='p1mo', manufacturer='p1ma', serial_number='p1s') + pc.components = components = [ + NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'), + GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500) + ] + db.session.add(pc) + db.session.commit() + pc = Desktop.query.one() + assert pc.serial_number == 'p1s' + assert pc.components == components + network_adapter = NetworkAdapter.query.one() + assert network_adapter.parent == pc - # Removing a component from pc doesn't delete the component - del pc.components[0] - db.session.commit() - pc = Device.query.first() # this is the same as querying for Desktop directly - assert pc.components[0].type == GraphicCard.__name__ - network_adapter = NetworkAdapter.query.one() - assert network_adapter not in pc.components - assert network_adapter.parent is None + # Removing a component from pc doesn't delete the component + del pc.components[0] + db.session.commit() + pc = Device.query.first() # this is the same as querying for Desktop directly + assert pc.components[0].type == GraphicCard.__name__ + network_adapter = NetworkAdapter.query.one() + assert network_adapter not in pc.components + assert network_adapter.parent is None - # Deleting the pc deletes everything - gcard = GraphicCard.query.one() - db.session.delete(pc) - assert pc.id == 1 - assert Desktop.query.first() is None - db.session.commit() - assert Desktop.query.first() is None - assert network_adapter.id == 2 - assert NetworkAdapter.query.first() is not None, 'We removed the network adaptor' - assert gcard.id == 3, 'We should still hold a reference to a zombie graphic card' - assert GraphicCard.query.first() is None, 'We should have deleted it –it was inside the pc' + # Deleting the pc deletes everything + gcard = GraphicCard.query.one() + db.session.delete(pc) + assert pc.id == 1 + assert Desktop.query.first() is None + db.session.commit() + assert Desktop.query.first() is None + assert network_adapter.id == 2 + assert NetworkAdapter.query.first() is not None, 'We removed the network adaptor' + assert gcard.id == 3, 'We should still hold a reference to a zombie graphic card' + assert GraphicCard.query.first() is None, 'We should have deleted it –it was inside the pc' +@pytest.mark.usefixtures('app_context') def test_device_schema(): """Ensures the user does not upload non-writable or extra fields.""" device_s = DeviceS() @@ -172,3 +174,44 @@ def test_execute_register_computer_no_hid(): # 2: device has no HID and we force it db_pc, _ = Sync.execute_register(pc, set(), force_creation=True) assert pc.physical_properties == db_pc.physical_properties + + +def test_get_device(app: Devicehub, user: UserClient): + """Checks GETting a Desktop with its components.""" + with app.app_context(): + pc = Desktop(model='p1mo', manufacturer='p1ma', serial_number='p1s') + pc.components = [ + NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'), + GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500) + ] + db.session.add(pc) + db.session.commit() + pc, _ = user.get(res=Device, item=1) + assert pc['events'] == [] + assert 'events_components' not in pc, 'events_components are internal use only' + assert 'events_one' not in pc, 'they are internal use only' + assert 'author' not in pc + assert tuple(c['id'] for c in pc['components']) == (2, 3) + assert pc['hid'] == 'p1ma-p1s-p1mo' + assert pc['model'] == 'p1mo' + assert pc['manufacturer'] == 'p1ma' + assert pc['serialNumber'] == 'p1s' + assert pc['type'] == 'Desktop' + + +def test_get_devices(app: Devicehub, user: UserClient): + """Checks GETting multiple devices.""" + with app.app_context(): + pc = Desktop(model='p1mo', manufacturer='p1ma', serial_number='p1s') + pc.components = [ + NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'), + GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500) + ] + pc1 = Microtower(model='p2mo', manufacturer='p2ma', serial_number='p2s') + pc2 = Laptop(model='p3mo', manufacturer='p3ma', serial_number='p3s') + db.session.add_all((pc, pc1, pc2)) + db.session.commit() + devices, _ = user.get(res=Device) + assert tuple(d['id'] for d in devices) == (1, 2, 3, 4, 5) + assert tuple(d['type'] for d in devices) == ('Desktop', 'Microtower', + 'Laptop', 'NetworkAdapter', 'GraphicCard') diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index 1f584de9..59bded42 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from typing import List from uuid import uuid4 import pytest @@ -13,6 +14,44 @@ from ereuse_devicehub.resources.user.models import User from tests.conftest import file +def assert_similar_device(device1: dict, device2: dict): + """ + Like Model.is_similar() but adapted for testing. + """ + assert isinstance(device1, dict) and device1 + assert isinstance(device2, dict) and device2 + for key in 'serialNumber', 'model', 'manufacturer', 'type': + assert device1.get(key, None) == device2.get(key, None) + + +def assert_similar_components(components1: List[dict], components2: List[dict]): + """ + Asserts that the components in components1 are + similar than the components in components2. + """ + assert len(components1) == len(components2) + for c1, c2 in zip(components1, components2): + assert_similar_device(c1, c2) + + +def snapshot_and_check(user: UserClient, + input_snapshot: dict, + num_events: int = 0, + perform_second_snapshot=True) -> dict: + """ + + """ + snapshot, _ = user.post(res=Snapshot, data=input_snapshot) + assert len(snapshot['events']) == num_events + assert input_snapshot['device'] + assert_similar_device(input_snapshot['device'], snapshot['device']) + assert_similar_components(input_snapshot['components'], snapshot['components']) + if perform_second_snapshot: + return snapshot_and_check(user, input_snapshot, num_events, False) + else: + return snapshot + + @pytest.mark.usefixtures('auth_app_context') def test_snapshot_model(): """ @@ -56,6 +95,25 @@ def test_snapshot_schema(app: Devicehub): def test_snapshot_post(user: UserClient): - """Tests the post snapshot endpoint (validation, etc).""" - s = file('basic.snapshot') - snapshot, _ = user.post(s, res=Snapshot.__name__) + """ + Tests the post snapshot endpoint (validation, etc) + and data correctness. + """ + snapshot = snapshot_and_check(user, file('basic.snapshot')) + assert snapshot['software'] == 'Workbench' + assert snapshot['version'] == '11.0' + assert snapshot['uuid'] == 'f5efd26e-8754-46bc-87bf-fbccc39d60d9' + assert snapshot['events'] == [] + assert snapshot['elapsed'] == 4 + assert snapshot['author']['id'] == user.user['id'] + assert 'events' not in snapshot['device'] + assert 'author' not in snapshot['device'] + + +def test_snapshot_add_remove(user: UserClient): + s1 = file('1-device-with-components.snapshot') + snapshot_and_check(user, s1) + + s2 = file('2-second-device-with-components-of-first.snapshot') + s3 = file('3-first-device-but-removing-motherboard-and-adding-processor-from-2.snapshot') + s4 = file('4-first-device-but-removing-processor.snapshot-and-adding-graphic-card')