Get resources correctly with polymorphic

This commit is contained in:
Xavier Bustamante Talavera 2018-05-11 18:58:48 +02:00
parent 78b5a230d4
commit ac26d8d610
21 changed files with 344 additions and 78 deletions

View File

@ -1,7 +1,11 @@
from typing import Type, Union
from boltons.typeutils import issubclass
from ereuse_utils.test import JSON from ereuse_utils.test import JSON
from flask import Response from flask import Response
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from ereuse_devicehub.resources.models import Thing
from teal.client import Client as TealClient from teal.client import Client as TealClient
@ -10,6 +14,26 @@ class Client(TealClient):
allow_subdomain_redirects=False): allow_subdomain_redirects=False):
super().__init__(application, response_wrapper, use_cookies, allow_subdomain_redirects) 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): def login(self, email: str, password: str):
assert isinstance(email, str) assert isinstance(email, str)
assert isinstance(password, str) assert isinstance(password, str)

View File

@ -2,7 +2,7 @@ from distutils.version import StrictVersion
from ereuse_devicehub.resources.device import ComponentDef, ComputerDef, DesktopDef, DeviceDef, \ from ereuse_devicehub.resources.device import ComponentDef, ComputerDef, DesktopDef, DeviceDef, \
GraphicCardDef, HardDriveDef, LaptopDef, MicrotowerDef, MotherboardDef, NetbookDef, \ 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.event import EventDef, SnapshotDef
from ereuse_devicehub.resources.user import UserDef from ereuse_devicehub.resources.user import UserDef
from teal.config import Config from teal.config import Config
@ -12,7 +12,7 @@ class DevicehubConfig(Config):
RESOURCE_DEFINITIONS = ( RESOURCE_DEFINITIONS = (
DeviceDef, ComputerDef, DesktopDef, LaptopDef, NetbookDef, ServerDef, MicrotowerDef, DeviceDef, ComputerDef, DesktopDef, LaptopDef, NetbookDef, ServerDef, MicrotowerDef,
ComponentDef, GraphicCardDef, HardDriveDef, MotherboardDef, NetworkAdapterDef, ComponentDef, GraphicCardDef, HardDriveDef, MotherboardDef, NetworkAdapterDef,
RamModuleDef, UserDef, EventDef, SnapshotDef RamModuleDef, ProcessorDef, UserDef, EventDef, SnapshotDef
) )
PASSWORD_SCHEMES = {'pbkdf2_sha256'} PASSWORD_SCHEMES = {'pbkdf2_sha256'}
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh-db1' SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/dh-db1'

View File

@ -6,6 +6,7 @@ from teal.marshmallow import NestedOn as TealNestedOn
class NestedOn(TealNestedOn): class NestedOn(TealNestedOn):
__doc__ = TealNestedOn.__doc__
def __init__(self, nested, polymorphic_on='type', db: SQLAlchemy = db, default=missing_, def __init__(self, nested, polymorphic_on='type', db: SQLAlchemy = db, default=missing_,
exclude=tuple(), only=None, **kwargs): exclude=tuple(), only=None, **kwargs):

View File

@ -1,6 +1,6 @@
from ereuse_devicehub.resources.device.schemas import Component, Computer, Desktop, Device, \ from ereuse_devicehub.resources.device.schemas import Component, Computer, Desktop, Device, \
GraphicCard, HardDrive, Laptop, Microtower, Motherboard, Netbook, NetworkAdapter, RamModule, \ GraphicCard, HardDrive, Laptop, Microtower, Motherboard, Netbook, NetworkAdapter, Processor, \
Server RamModule, Server
from ereuse_devicehub.resources.device.views import DeviceView from ereuse_devicehub.resources.device.views import DeviceView
from teal.resource import Converters, Resource from teal.resource import Converters, Resource
@ -58,3 +58,7 @@ class NetworkAdapterDef(ComponentDef):
class RamModuleDef(ComponentDef): class RamModuleDef(ComponentDef):
SCHEMA = RamModule SCHEMA = RamModule
class ProcessorDef(ComponentDef):
SCHEMA = Processor

View File

@ -144,6 +144,13 @@ class NetworkAdapter(Component):
speed = Column(SmallInteger, check_range('speed', min=10, max=10000)) # type: int 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): class RamModule(Component):
id = Column(BigInteger, ForeignKey(Component.id), primary_key=True) # type: int id = Column(BigInteger, ForeignKey(Component.id), primary_key=True) # type: int
size = Column(SmallInteger, check_range('size', min=128, max=17000)) size = Column(SmallInteger, check_range('size', min=128, max=17000))

View File

@ -1,6 +1,8 @@
from marshmallow.fields import Float, Integer, Nested, Str from marshmallow import post_dump
from marshmallow.validate import Length, Range 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.models import STR_BIG_SIZE, STR_SIZE
from ereuse_devicehub.resources.schemas import Thing, UnitCodes from ereuse_devicehub.resources.schemas import Thing, UnitCodes
@ -29,11 +31,21 @@ class Device(Thing):
height = Float(validate=Range(0.1, 3), height = Float(validate=Range(0.1, 3),
unit=UnitCodes.m, unit=UnitCodes.m,
description='The height of the device in meters.') 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): class Computer(Device):
components = Nested('Component', many=True, dump_only=True, only='id') components = NestedOn('Component', many=True, dump_only=True)
pass pass
@ -58,7 +70,7 @@ class Microtower(Computer):
class Component(Device): class Component(Device):
parent = Nested(Device, dump_only=True, only='id') parent = NestedOn(Device, dump_only=True)
class GraphicCard(Component): class GraphicCard(Component):
@ -71,9 +83,9 @@ class HardDrive(Component):
size = Integer(validate=Range(0, 10 ** 8), size = Integer(validate=Range(0, 10 ** 8),
unit=UnitCodes.mbyte, unit=UnitCodes.mbyte,
description='The size of the hard-drive in MB.') description='The size of the hard-drive in MB.')
erasure = Nested('EraseBasic', load_only=True) erasure = NestedOn('EraseBasic', load_only=True)
tests = Nested('TestHardDrive', many=True, load_only=True) tests = NestedOn('TestHardDrive', many=True, load_only=True)
benchmarks = Nested('BenchmarkHardDrive', load_only=True, many=True) benchmarks = NestedOn('BenchmarkHardDrive', load_only=True, many=True)
class Motherboard(Component): class Motherboard(Component):
@ -90,6 +102,12 @@ class NetworkAdapter(Component):
description='The maximum speed this network adapter can handle, in mbps.') 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): class RamModule(Component):
size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte) size = Integer(validate=Range(min=128, max=17000), unit=UnitCodes.mbyte)
speed = Float(validate=Range(min=100, max=10000), unit=UnitCodes.mhz) speed = Float(validate=Range(min=100, max=10000), unit=UnitCodes.mhz)

View File

@ -160,7 +160,6 @@ class Sync:
""" """
events = [] events = []
old_components = set(device.components) old_components = set(device.components)
adding = components - old_components adding = components - old_components
if adding: if adding:
add = Add(device=device, components=list(adding)) add = Add(device=device, components=list(adding))
@ -177,5 +176,4 @@ class Sync:
removing = old_components - components removing = old_components - components
if removing: if removing:
events.append(Remove(device=device, components=list(removing))) events.append(Remove(device=device, components=list(removing)))
return events return events

View File

@ -5,4 +5,10 @@ from teal.resource import View
class DeviceView(View): class DeviceView(View):
def one(self, id: int): def one(self, id: int):
"""Gets one device.""" """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)

View File

@ -30,17 +30,17 @@ class Event(Thing):
'hardware without margin of doubt.') 'hardware without margin of doubt.')
incidence = Boolean(default=False, incidence = Boolean(default=False,
description='Was something wrong in this event?') 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.') 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): class EventWithOneDevice(Event):
device = Nested(Device, only='id') device = NestedOn(Device, only='id')
class EventWithMultipleDevices(Event): class EventWithMultipleDevices(Event):
device = Nested(Device, many=True, only='id') device = NestedOn(Device, many=True, only='id')
class Add(EventWithOneDevice): class Add(EventWithOneDevice):
@ -52,14 +52,14 @@ class Remove(EventWithOneDevice):
class Allocate(EventWithMultipleDevices): class Allocate(EventWithMultipleDevices):
to = Nested(User, only='id', to = NestedOn(User,
description='The user the devices are allocated to.') description='The user the devices are allocated to.')
organization = String(validate=Length(STR_SIZE), organization = String(validate=Length(STR_SIZE),
description='The organization where the user was when this happened.') description='The organization where the user was when this happened.')
class Deallocate(EventWithMultipleDevices): class Deallocate(EventWithMultipleDevices):
from_rel = Nested(User, only='id', from_rel = Nested(User,
data_key='from', data_key='from',
description='The user where the devices are not allocated to anymore.') description='The user where the devices are not allocated to anymore.')
organization = String(validate=Length(STR_SIZE), organization = String(validate=Length(STR_SIZE),
@ -138,6 +138,7 @@ class Snapshot(EventWithOneDevice):
color = Color(description='Main color of the device.') color = Color(description='Main color of the device.')
orientation = EnumField(Orientation, description='Is the device main stand wider or larger?') orientation = EnumField(Orientation, description='Is the device main stand wider or larger?')
force_creation = Boolean(data_key='forceCreation') force_creation = Boolean(data_key='forceCreation')
events = NestedOn(Event, many=True)
@validates_schema @validates_schema
def validate_workbench_version(self, data: dict): def validate_workbench_version(self, data: dict):

View File

@ -1,6 +1,6 @@
from distutils.version import StrictVersion from distutils.version import StrictVersion
from flask import request, Response from flask import request
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.sync import Sync from ereuse_devicehub.resources.device.sync import Sync
@ -28,12 +28,14 @@ class SnapshotView(View):
device = s.pop('device') device = s.pop('device')
components = s.pop('components') if s['software'] == SoftwareType.Workbench else None components = s.pop('components') if s['software'] == SoftwareType.Workbench else None
# noinspection PyArgumentList # noinspection PyArgumentList
del s['type']
snapshot = Snapshot(**s) snapshot = Snapshot(**s)
snapshot.device, snapshot.events = Sync.run(device, components, snapshot.force_creation) snapshot.device, snapshot.events = Sync.run(device, components, snapshot.force_creation)
snapshot.components = snapshot.device.components
db.session.add(snapshot) db.session.add(snapshot)
# transform it back db.session.flush() # Take to DB so we get db-generated values
return Response(status=201) ret = self.schema.jsonify(snapshot) # transform it back
ret.status_code = 201
return ret
class TestHardDriveView(View): class TestHardDriveView(View):

View File

@ -1,7 +1,9 @@
from enum import Enum 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 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') same_as = List(URL(dump_only=True), dump_only=True, data_key='sameAs')
updated = DateTime('iso', dump_only=True) updated = DateTime('iso', dump_only=True)
created = 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)

View File

@ -28,9 +28,10 @@ class UserDef(Resource):
""" """
Creates an user. Creates an user.
""" """
with self.app.test_request_context(): with self.app.app_context():
self.schema.load({'email': email, 'password': password}) self.SCHEMA(only={'email', 'password'}, exclude=('token',)) \
.load({'email': email, 'password': password})
user = User(email=email, password=password) user = User(email=email, password=password)
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return user.dump() return self.schema.dump(user)

View File

@ -1,6 +1,6 @@
from base64 import b64encode from base64 import b64encode
from marshmallow import pre_dump from marshmallow import post_dump
from marshmallow.fields import Email, String, UUID from marshmallow.fields import Email, String, UUID
from ereuse_devicehub.resources.schemas import Thing from ereuse_devicehub.resources.schemas import Thing
@ -10,13 +10,33 @@ class User(Thing):
id = UUID(dump_only=True) id = UUID(dump_only=True)
email = Email(required=True) email = Email(required=True)
password = String(load_only=True, required=True) password = String(load_only=True, required=True)
name = String()
token = String(dump_only=True, token = String(dump_only=True,
description='Use this token in an Authorization header to access the app.' description='Use this token in an Authorization header to access the app.'
'The token can change overtime.') '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): def base64encode_token(self, data: dict):
"""Encodes the token to base64 so clients don't have to.""" """Encodes the token to base64 so clients don't have to."""
# framework needs ':' at the end if 'token' in data:
data['token'] = b64encode(str.encode(str(data['token']) + ':')) # 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 return data

View File

@ -1,6 +1,6 @@
from uuid import UUID 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.exceptions import WrongCredentials
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
@ -14,10 +14,13 @@ class UserView(View):
def login(): def login():
user_s = app.resources['User'].schema # type: UserS # We use custom schema as we only want to parse a subset of user
u = user_s.load(request.get_json(), partial=('email', 'password')) 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() user = User.query.filter_by(email=u['email']).one_or_none()
if user and user.password == u['password']: 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: else:
raise WrongCredentials() raise WrongCredentials()

View File

@ -28,7 +28,6 @@ def _app(config: TestConfig) -> Devicehub:
@pytest.fixture() @pytest.fixture()
def app(request, _app: Devicehub) -> Devicehub: def app(request, _app: Devicehub) -> Devicehub:
db.drop_all(app=_app) # In case the test before was killed
db.create_all(app=_app) db.create_all(app=_app)
# More robust than 'yield' # More robust than 'yield'
request.addfinalizer(lambda *args, **kw: db.drop_all(app=_app)) request.addfinalizer(lambda *args, **kw: db.drop_all(app=_app))

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -1,10 +1,11 @@
import pytest import pytest
from ereuse_devicehub.client import UserClient
from ereuse_devicehub.db import db from ereuse_devicehub.db import db
from ereuse_devicehub.devicehub import Devicehub from ereuse_devicehub.devicehub import Devicehub
from ereuse_devicehub.resources.device.exceptions import NeedsId from ereuse_devicehub.resources.device.exceptions import NeedsId
from ereuse_devicehub.resources.device.models import Component, Computer, Desktop, Device, \ 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.schemas import Device as DeviceS
from ereuse_devicehub.resources.device.sync import Sync from ereuse_devicehub.resources.device.sync import Sync
from ereuse_devicehub.resources.event.models import Add, Remove from ereuse_devicehub.resources.event.models import Add, Remove
@ -12,46 +13,47 @@ from teal.db import ResourceNotFound
from tests.conftest import file 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. 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 = Desktop(model='p1mo', manufacturer='p1ma', serial_number='p1s') pc.components = components = [
pc.components = components = [ NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'),
NetworkAdapter(model='c1mo', manufacturer='c1ma', serial_number='c1s'), GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500)
GraphicCard(model='c2mo', manufacturer='c2ma', memory=1500) ]
] db.session.add(pc)
db.session.add(pc) db.session.commit()
db.session.commit() pc = Desktop.query.one()
pc = Desktop.query.one() assert pc.serial_number == 'p1s'
assert pc.serial_number == 'p1s' assert pc.components == components
assert pc.components == components network_adapter = NetworkAdapter.query.one()
network_adapter = NetworkAdapter.query.one() assert network_adapter.parent == pc
assert network_adapter.parent == pc
# Removing a component from pc doesn't delete the component # Removing a component from pc doesn't delete the component
del pc.components[0] del pc.components[0]
db.session.commit() db.session.commit()
pc = Device.query.first() # this is the same as querying for Desktop directly pc = Device.query.first() # this is the same as querying for Desktop directly
assert pc.components[0].type == GraphicCard.__name__ assert pc.components[0].type == GraphicCard.__name__
network_adapter = NetworkAdapter.query.one() network_adapter = NetworkAdapter.query.one()
assert network_adapter not in pc.components assert network_adapter not in pc.components
assert network_adapter.parent is None assert network_adapter.parent is None
# Deleting the pc deletes everything # Deleting the pc deletes everything
gcard = GraphicCard.query.one() gcard = GraphicCard.query.one()
db.session.delete(pc) db.session.delete(pc)
assert pc.id == 1 assert pc.id == 1
assert Desktop.query.first() is None assert Desktop.query.first() is None
db.session.commit() db.session.commit()
assert Desktop.query.first() is None assert Desktop.query.first() is None
assert network_adapter.id == 2 assert network_adapter.id == 2
assert NetworkAdapter.query.first() is not None, 'We removed the network adaptor' 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 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' 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(): def test_device_schema():
"""Ensures the user does not upload non-writable or extra fields.""" """Ensures the user does not upload non-writable or extra fields."""
device_s = DeviceS() device_s = DeviceS()
@ -172,3 +174,44 @@ def test_execute_register_computer_no_hid():
# 2: device has no HID and we force it # 2: device has no HID and we force it
db_pc, _ = Sync.execute_register(pc, set(), force_creation=True) db_pc, _ = Sync.execute_register(pc, set(), force_creation=True)
assert pc.physical_properties == db_pc.physical_properties 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')

View File

@ -1,4 +1,5 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List
from uuid import uuid4 from uuid import uuid4
import pytest import pytest
@ -13,6 +14,44 @@ from ereuse_devicehub.resources.user.models import User
from tests.conftest import file 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') @pytest.mark.usefixtures('auth_app_context')
def test_snapshot_model(): def test_snapshot_model():
""" """
@ -56,6 +95,25 @@ def test_snapshot_schema(app: Devicehub):
def test_snapshot_post(user: UserClient): def test_snapshot_post(user: UserClient):
"""Tests the post snapshot endpoint (validation, etc).""" """
s = file('basic.snapshot') Tests the post snapshot endpoint (validation, etc)
snapshot, _ = user.post(s, res=Snapshot.__name__) 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')