Get resources correctly with polymorphic
This commit is contained in:
parent
78b5a230d4
commit
ac26d8d610
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
MIN_WORKBENCH = StrictVersion('11.0')
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
|
|
27
tests/files/1-device-with-components.snapshot.yaml
Normal file
27
tests/files/1-device-with-components.snapshot.yaml
Normal 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'
|
|
@ -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'
|
|
@ -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
|
|
@ -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
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
Reference in a new issue