First Snapshot attempt

This commit is contained in:
Xavier Bustamante Talavera 2018-04-30 19:58:19 +02:00
parent 8723b379b0
commit 78b5a230d4
11 changed files with 346 additions and 120 deletions

View File

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

View File

@ -5,7 +5,7 @@ from ereuse_utils.naming import Naming
from sqlalchemy import BigInteger, Column, Float, ForeignKey, Integer, Sequence, SmallInteger, \ from sqlalchemy import BigInteger, Column, Float, ForeignKey, Integer, Sequence, SmallInteger, \
Unicode, inspect Unicode, inspect
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import backref, relationship from sqlalchemy.orm import ColumnProperty, backref, relationship
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing, \ from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing, \
check_range check_range
@ -28,8 +28,13 @@ class Device(Thing):
height = Column(Float(precision=3, decimal_return_scale=3), height = Column(Float(precision=3, decimal_return_scale=3),
check_range('height', 0.1, 3)) # type: float check_range('height', 0.1, 3)) # type: float
def __init__(self, *args, **kw) -> None:
super().__init__(*args, **kw)
with suppress(TypeError):
self.hid = Naming.hid(self.manufacturer, self.serial_number, self.model) # type: str
@property @property
def physical_properties(self) -> Dict[Column, object or None]: def physical_properties(self) -> Dict[str, object or None]:
""" """
Fields that describe the physical properties of a device. Fields that describe the physical properties of a device.
@ -39,9 +44,11 @@ class Device(Thing):
""" """
# todo ensure to remove materialized values when start using them # todo ensure to remove materialized values when start using them
# todo or self.__table__.columns if inspect fails # todo or self.__table__.columns if inspect fails
return {c: getattr(self, c.name, None) return {c.key: getattr(self, c.key, None)
for c in inspect(self.__class__).attrs for c in inspect(self.__class__).attrs
if not c.foreign_keys and c not in {self.id, self.type}} if isinstance(c, ColumnProperty)
and not getattr(c, 'foreign_keys', None)
and c.key not in {'id', 'type', 'created', 'updated', 'parent_id', 'hid'}}
@declared_attr @declared_attr
def __mapper_args__(cls): def __mapper_args__(cls):
@ -57,10 +64,8 @@ class Device(Thing):
args[POLYMORPHIC_ON] = cls.type args[POLYMORPHIC_ON] = cls.type
return args return args
def __init__(self, *args, **kw) -> None: def __lt__(self, other):
super().__init__(*args, **kw) return self.id < other.id
with suppress(TypeError):
self.hid = Naming.hid(self.manufacturer, self.serial_number, self.model)
class Computer(Device): class Computer(Device):

View File

@ -1,3 +1,4 @@
import re
from contextlib import suppress from contextlib import suppress
from itertools import groupby from itertools import groupby
from typing import Iterable, List, Set from typing import Iterable, List, Set
@ -18,7 +19,7 @@ class Sync:
@classmethod @classmethod
def run(cls, device: Device, def run(cls, device: Device,
components: Iterable[Component] or None, components: Iterable[Component] or None,
force_creation: bool = False) -> (Device, List[Component], List[Add or Remove]): force_creation: bool = False) -> (Device, List[Add or Remove]):
""" """
Synchronizes the device and components with the database. Synchronizes the device and components with the database.
@ -40,39 +41,41 @@ class Sync:
it doesn't generate HID or have an ID? it doesn't generate HID or have an ID?
Only for the device param. Only for the device param.
:return: A tuple of: :return: A tuple of:
1. The device from the database (with an ID). 1. The device from the database (with an ID) whose
2. The same passed-in components from the database (with ``components`` field contain the db version
ids). of the passed-in components.
3. A list of Add / Remove (not yet added to session). 2. A list of Add / Remove (not yet added to session).
""" """
blacklist = set() # Helper for execute_register() db_device, _ = cls.execute_register(device, force_creation=force_creation)
db_device = cls.execute_register(device, blacklist, force_creation) db_components, events = [], []
if id(device) != id(db_device): if components is not None: # We have component info (see above)
# Did I get another device from db? blacklist = set() # type: Set[int]
# In such case update the device from db with new stuff not_new_components = set()
cls.merge(device, db_device) for component in components:
db_components = [] db_component, is_new = cls.execute_register(component, blacklist, parent=db_device)
for component in components:
db_component = cls.execute_register(component, blacklist, parent=db_device)
if id(component) != id(db_component):
cls.merge(component, db_component)
db_components.append(db_component) db_components.append(db_component)
events = tuple() if not is_new:
if components is not None: not_new_components.add(db_component)
# Only perform Add / Remove when # We only want to perform Add/Remove to not new components
events = cls.add_remove(db_device, set(db_components)) events = cls.add_remove(db_device, not_new_components)
return db_device, db_components, events db_device.components = db_components
return db_device, events
@staticmethod @classmethod
def execute_register(device: Device, def execute_register(cls, device: Device,
blacklist: Set[int], blacklist: Set[int] = None,
force_creation: bool = False, force_creation: bool = False,
parent: Computer = None) -> Device: parent: Computer = None) -> (Device, bool):
""" """
Synchronizes one device to the DB. Synchronizes one device to the DB.
This method tries to update the device in the database if it This method tries to create a device into the database, and
already exists, otherwise it creates a new one. if it already exists it returns a "local synced version",
this is the same ``device`` you passed-in but with updated
values from the database one (like the id value).
When we say "local" we mean that if, the device existed on the
database, we do not "touch" any of its values on the DB.
:param device: The device to synchronize to the DB. :param device: The device to synchronize to the DB.
:param blacklist: A set of components already found by :param blacklist: A set of components already found by
@ -85,8 +88,11 @@ class Sync:
S/N). S/N).
:param parent: For components, the computer that contains them. :param parent: For components, the computer that contains them.
Helper used by Component.similar_one(). Helper used by Component.similar_one().
:return: A synchronized device with the DB. It can be a new :return: A tuple with:
device or an already existing one. 1. A synchronized device with the DB. It can be a new
device or an already existing one.
2. A flag stating if the device is new or it existed
already in the DB.
:raise NeedsId: The device has not any identifier we can use. :raise NeedsId: The device has not any identifier we can use.
To still create the device use To still create the device use
``force_creation``. ``force_creation``.
@ -103,56 +109,73 @@ class Sync:
# ensure we don't get it again for another component # ensure we don't get it again for another component
# with the same physical properties # with the same physical properties
blacklist.add(db_component.id) blacklist.add(db_component.id)
return db_component return cls.merge(device, db_component), False
elif not force_creation: elif not force_creation:
raise NeedsId() raise NeedsId()
db.session.begin_nested() # Create transaction savepoint to auto-rollback on insertion err
try: try:
# Let's try to insert or update with db.session.begin_nested():
db.session.insert(device) # Create transaction savepoint to auto-rollback on insertion err
db.session.flush() # Let's try to insert or update
db.session.add(device)
db.session.flush()
except IntegrityError as e: except IntegrityError as e:
if e.orig.diag.sqlstate == UNIQUE_VIOLATION: if e.orig.diag.sqlstate == UNIQUE_VIOLATION:
db.session.rollback()
# This device already exists in the DB # This device already exists in the DB
field, value = 'az' # todo get from e.orig.diag field, value = re.findall('\(.*?\)', e.orig.diag.message_detail) # type: str
return Device.query.find(getattr(device.__class__, field) == value).one() field = field.replace('(', '').replace(')', '')
value = value.replace('(', '').replace(')', '')
db_device = Device.query.filter(getattr(device.__class__, field) == value).one()
return cls.merge(device, db_device), False
else: else:
raise e raise e
else: else:
return device # Our device is new return device, True # Our device is new
@classmethod @classmethod
def merge(cls, device: Device, db_device: Device): def merge(cls, device: Device, db_device: Device):
""" """
Copies the physical properties of the device to the db_device. Copies the physical properties of the device to the db_device.
""" """
for field, value in device.physical_properties: for field_name, value in device.physical_properties.items():
if value is not None: if value is not None:
setattr(db_device, field.name, value) setattr(db_device, field_name, value)
return db_device return db_device
@classmethod @classmethod
def add_remove(cls, device: Device, def add_remove(cls, device: Device,
new_components: Set[Component]) -> List[Add or Remove]: components: Set[Component]) -> List[Add or Remove]:
""" """
Generates the Add and Remove events by evaluating the Generates the Add and Remove events (but doesn't add them to
differences between the components the session).
:param device:
:param new_components: :param device: A device which ``components`` attribute contains
:return: the old list of components. The components that
are not in ``components`` will be Removed.
:param components: List of components that are potentially to
be Added. Some of them can already exist
on the device, in which case they won't
be re-added.
:return: A list of Add / Remove events.
""" """
events = []
old_components = set(device.components) old_components = set(device.components)
add = Add(device=Device, components=list(new_components - old_components))
events = [
Remove(device=device, components=list(old_components - new_components)),
add
]
# For the components we are adding, let's remove them from their old parents adding = components - old_components
def get_parent(component: Component): if adding:
return component.parent add = Add(device=device, components=list(adding))
# For the components we are adding, let's remove them from their old parents
def g_parent(component: Component) -> int:
return component.parent or Computer(id=0) # Computer with id 0 is our Identity
for parent, _components in groupby(sorted(add.components, key=g_parent), key=g_parent):
if parent.id != 0:
events.append(Remove(device=parent, components=list(_components)))
events.append(add)
removing = old_components - components
if removing:
events.append(Remove(device=device, components=list(removing)))
for parent, components in groupby(sorted(add.components, key=get_parent), key=get_parent):
if parent is not None:
events.append(Remove(device=parent, components=list(components)))
return events return events

View File

@ -1,6 +1,7 @@
from datetime import timedelta from datetime import timedelta
from colour import Color from colour import Color
from flask import g
from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, DateTime, Enum as DBEnum, \ from sqlalchemy import BigInteger, Boolean, CheckConstraint, Column, DateTime, Enum as DBEnum, \
ForeignKey, Integer, Interval, JSON, Sequence, SmallInteger, Unicode ForeignKey, Integer, Interval, JSON, Sequence, SmallInteger, Unicode
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
@ -15,7 +16,8 @@ from ereuse_devicehub.resources.event.enums import Appearance, Bios, Functionali
from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing, \ from ereuse_devicehub.resources.models import STR_BIG_SIZE, STR_SIZE, STR_SM_SIZE, Thing, \
check_range check_range
from ereuse_devicehub.resources.user.models import User from ereuse_devicehub.resources.user.models import User
from teal.db import CASCADE, CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, POLYMORPHIC_ON from teal.db import CASCADE, CASCADE_OWN, INHERIT_COND, POLYMORPHIC_ID, POLYMORPHIC_ON, \
StrictVersionType
class JoinedTableMixin: class JoinedTableMixin:
@ -40,7 +42,10 @@ class Event(Thing):
backref=backref('events', lazy=True, cascade=CASCADE), backref=backref('events', lazy=True, cascade=CASCADE),
primaryjoin='Event.snapshot_id == Snapshot.id') primaryjoin='Event.snapshot_id == Snapshot.id')
author_id = Column(UUID(as_uuid=True), ForeignKey(User.id), nullable=False) author_id = Column(UUID(as_uuid=True),
ForeignKey(User.id),
nullable=False,
default=lambda: g.user.id)
author = relationship(User, author = relationship(User,
backref=backref('events', lazy=True), backref=backref('events', lazy=True),
primaryjoin=author_id == User.id) primaryjoin=author_id == User.id)
@ -142,28 +147,26 @@ class Step(db.Model):
class Snapshot(JoinedTableMixin, EventWithOneDevice): class Snapshot(JoinedTableMixin, EventWithOneDevice):
uuid = Column(UUID(as_uuid=True), nullable=False, unique=True) # type: UUID uuid = Column(UUID(as_uuid=True), nullable=False, unique=True) # type: UUID
version = Column(Unicode(STR_SM_SIZE), nullable=False) # type: str version = Column(StrictVersionType(STR_SM_SIZE), nullable=False) # type: str
software = Column(DBEnum(SoftwareType), nullable=False) # type: SoftwareType software = Column(DBEnum(SoftwareType), nullable=False) # type: SoftwareType
appearance = Column(DBEnum(Appearance), nullable=False) # type: Appearance appearance = Column(DBEnum(Appearance)) # type: Appearance
appearance_score = Column(SmallInteger, appearance_score = Column(SmallInteger,
check_range('appearance_score', -3, 5), check_range('appearance_score', -3, 5)) # type: int
nullable=False) # type: int functionality = Column(DBEnum(Functionality)) # type: Functionality
functionality = Column(DBEnum(Functionality), nullable=False) # type: Functionality
functionality_score = Column(SmallInteger, functionality_score = Column(SmallInteger,
check_range('functionality_score', min=-3, max=5), check_range('functionality_score', min=-3, max=5)) # type: int
nullable=False) # type: int
labelling = Column(Boolean) # type: bool labelling = Column(Boolean) # type: bool
bios = Column(DBEnum(Bios)) # type: Bios bios = Column(DBEnum(Bios)) # type: Bios
condition = Column(SmallInteger, condition = Column(SmallInteger,
check_range('condition', min=0, max=5), check_range('condition', min=0, max=5)) # type: int
nullable=False) # type: int
elapsed = Column(Interval, nullable=False) # type: timedelta elapsed = Column(Interval, nullable=False) # type: timedelta
install_name = Column(Unicode(STR_BIG_SIZE)) # type: str install_name = Column(Unicode(STR_BIG_SIZE)) # type: str
install_elapsed = Column(Interval) # type: timedelta install_elapsed = Column(Interval) # type: timedelta
install_success = Column(Boolean) # type: bool install_success = Column(Boolean) # type: bool
inventory_elapsed = Column(Interval) # type: timedelta inventory_elapsed = Column(Interval) # type: timedelta
color = Column(ColorType) # type: Color color = Column(ColorType) # type: Color
orientation = DBEnum(Orientation) # type: Orientation orientation = Column(DBEnum(Orientation)) # type: Orientation
force_creation = Column(Boolean)
@validates('components') @validates('components')
def validate_components_only_workbench(self, _, components): def validate_components_only_workbench(self, _, components):

View File

@ -1,5 +1,5 @@
from flask import current_app as app from flask import current_app as app
from marshmallow import ValidationError, validates_schema from marshmallow import ValidationError, post_load, validates_schema
from marshmallow.fields import Boolean, DateTime, Integer, Nested, String, TimeDelta, UUID from marshmallow.fields import Boolean, DateTime, Integer, Nested, String, TimeDelta, UUID
from marshmallow.validate import Length, Range from marshmallow.validate import Length, Range
from marshmallow_enum import EnumField from marshmallow_enum import EnumField
@ -137,6 +137,7 @@ class Snapshot(EventWithOneDevice):
inventory = Nested(Inventory) inventory = Nested(Inventory)
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')
@validates_schema @validates_schema
def validate_workbench_version(self, data: dict): def validate_workbench_version(self, data: dict):
@ -154,6 +155,14 @@ class Snapshot(EventWithOneDevice):
raise ValidationError('Only Workbench can add component info', raise ValidationError('Only Workbench can add component info',
field_names=['components']) field_names=['components'])
@post_load
def normalize_nested(self, data: dict):
data.update(data.pop('condition'))
data['condition'] = data.pop('general', None)
data.update({'install_' + key: value for key, value in data.pop('install', {})})
data['inventory_elapsed'] = data.get('inventory', {}).pop('elapsed', None)
return data
class Test(EventWithOneDevice): class Test(EventWithOneDevice):
elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True) elapsed = TimeDelta(precision=TimeDelta.SECONDS, required=True)

View File

@ -1,6 +1,6 @@
from distutils.version import StrictVersion from distutils.version import StrictVersion
from flask import request from flask import request, Response
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
@ -21,13 +21,19 @@ SUPPORTED_WORKBENCH = StrictVersion('11.0')
class SnapshotView(View): class SnapshotView(View):
def post(self): def post(self):
"""Creates a Snapshot.""" """Creates a Snapshot."""
snapshot = Snapshot(**request.get_json()) # todo put this in schema.load()? s = request.get_json()
# Note that if we set the device / components into the snapshot
# model object, when we flush them to the db we will flush
# snapshot, and we want to wait to flush snapshot at the end
device = s.pop('device')
components = s.pop('components') if s['software'] == SoftwareType.Workbench else None
# noinspection PyArgumentList # noinspection PyArgumentList
c = snapshot.components if snapshot.software == SoftwareType.Workbench else None del s['type']
snapshot.device, snapshot.components, snapshot.events = Sync.run(snapshot.device, c) snapshot = Snapshot(**s)
snapshot.device, snapshot.events = Sync.run(device, components, snapshot.force_creation)
db.session.add(snapshot) db.session.add(snapshot)
# transform it back # transform it back
return self.schema.jsonify(snapshot) return Response(status=201)
class TestHardDriveView(View): class TestHardDriveView(View):

View File

@ -1,4 +1,3 @@
import json as stdlib_json
from pathlib import Path from pathlib import Path
import pytest import pytest
@ -41,6 +40,12 @@ def client(app: Devicehub) -> Client:
return app.test_client() return app.test_client()
@pytest.fixture()
def app_context(app: Devicehub):
with app.app_context():
yield
@pytest.fixture() @pytest.fixture()
def user(app: Devicehub) -> UserClient: def user(app: Devicehub) -> UserClient:
"""Gets a client with a logged-in dummy user.""" """Gets a client with a logged-in dummy user."""
@ -61,6 +66,20 @@ def create_user(email='foo@foo.com', password='foo') -> User:
return user return user
@pytest.fixture()
def auth_app_context(app: Devicehub):
"""Creates an app context with a set user."""
with app.app_context():
user = create_user()
class Auth: # Mock
username = user.token
password = ''
app.auth.perform_auth(Auth())
yield
def file(name: str) -> dict: def file(name: str) -> dict:
"""Opens and parses a JSON file from the ``files`` subdir.""" """Opens and parses a JSON file from the ``files`` subdir."""
with Path(__file__).parent.joinpath('files').joinpath(name + '.yaml').open() as f: with Path(__file__).parent.joinpath('files').joinpath(name + '.yaml').open() as f:

View File

@ -0,0 +1,14 @@
device:
type: 'Microtower'
serial_number: 'd1s'
model: 'd1ml'
manufacturer: 'd1mr'
components:
- type: 'GraphicCard'
serial_number: 'gc1s'
model: 'gc1ml'
manufacturer: 'gc1mr'
- type: 'RamModule'
serial_number: 'rm1s'
model: 'rm1ml'
manufacturer: 'rm1mr'

View File

@ -1,7 +1,15 @@
import pytest
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.models import Desktop, Device, GraphicCard, NetworkAdapter from ereuse_devicehub.resources.device.exceptions import NeedsId
from ereuse_devicehub.resources.device.models import Component, Computer, Desktop, Device, \
GraphicCard, 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.event.models import Add, Remove
from teal.db import ResourceNotFound
from tests.conftest import file
def test_device_model(app: Devicehub): def test_device_model(app: Devicehub):
@ -47,6 +55,120 @@ def test_device_model(app: Devicehub):
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()
device_s.load({'serial_number': 'foo1', 'model': 'foo', 'manufacturer': 'bar2'}) device_s.load({'serialNumber': 'foo1', 'model': 'foo', 'manufacturer': 'bar2'})
device_s.dump({'id': 1}) device_s.dump({'id': 1})
@pytest.mark.usefixtures('app_context')
def test_physical_properties():
c = Motherboard(slots=2,
usb=3,
serial_number='sn',
model='ml',
manufacturer='mr',
width=2.0,
pid='abc')
pc = Computer(components=[c])
db.session.add(pc)
db.session.commit()
assert c.physical_properties == {
'gid': None,
'usb': 3,
'pid': 'abc',
'serial_number': 'sn',
'pcmcia': None,
'model': 'ml',
'slots': 2,
'serial': None,
'firewire': None,
'manufacturer': 'mr',
'weight': None,
'height': None,
'width': 2.0
}
@pytest.mark.usefixtures('app_context')
def test_component_similar_one():
snapshot = file('pc-components.db')
d = snapshot['device']
snapshot['components'][0]['serial_number'] = snapshot['components'][1]['serial_number'] = None
pc = Computer(**d, components=[Component(**c) for c in snapshot['components']])
component1, component2 = pc.components # type: Component
db.session.add(pc)
# Let's create a new component named 'A' similar to 1
componentA = Component(model=component1.model, manufacturer=component1.manufacturer)
similar_to_a = componentA.similar_one(pc, set())
assert similar_to_a == component1
# Component B does not have the same model
componentB = Component(model='nope', manufacturer=component1.manufacturer)
with pytest.raises(ResourceNotFound):
assert componentB.similar_one(pc, set())
# If we blacklist component A we won't get anything
with pytest.raises(ResourceNotFound):
assert componentA.similar_one(pc, blacklist={componentA.id})
@pytest.mark.usefixtures('auth_app_context')
def test_add_remove():
# Original state:
# pc has c1 and c2
# pc2 has c3
# c4 is not with any pc
values = file('pc-components.db')
pc = values['device']
c1, c2 = [Component(**c) for c in values['components']]
pc = Computer(**pc, components=[c1, c2])
db.session.add(pc)
c3 = Component(serial_number='nc1')
pc2 = Computer(serial_number='s2', components=[c3])
c4 = Component(serial_number='c4s')
db.session.add(pc2)
db.session.add(c4)
db.session.commit()
# Test:
# pc has only c3
events = Sync.add_remove(device=pc, components={c3, c4})
assert len(events) == 3
assert isinstance(events[0], Remove)
assert events[0].device == pc2
assert events[0].components == [c3]
assert isinstance(events[1], Add)
assert events[1].device == pc
assert set(events[1].components) == {c3, c4}
assert isinstance(events[2], Remove)
assert events[2].device == pc
assert set(events[2].components) == {c1, c2}
@pytest.mark.usefixtures('app_context')
def test_execute_register_computer():
# Case 1: device does not exist on DB
pc = Computer(**file('pc-components.db')['device'])
db_pc, _ = Sync.execute_register(pc, set())
assert pc.physical_properties == db_pc.physical_properties
@pytest.mark.usefixtures('app_context')
def test_execute_register_computer_existing():
pc = Computer(**file('pc-components.db')['device'])
db.session.add(pc)
db.session.commit() # We need two separate sessions
pc = Computer(**file('pc-components.db')['device'])
# 1: device exists on DB
db_pc, _ = Sync.execute_register(pc, set())
assert pc.physical_properties == db_pc.physical_properties
@pytest.mark.usefixtures('app_context')
def test_execute_register_computer_no_hid():
pc = Computer(**file('pc-components.db')['device'])
# 1: device has no HID
pc.hid = pc.model = None
with pytest.raises(NeedsId):
Sync.execute_register(pc, set())
# 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

View File

@ -0,0 +1,24 @@
import pytest
from flask import g
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.event.models import EventWithOneDevice
from tests.conftest import create_user
@pytest.mark.usefixtures('app_context')
def test_author():
"""
Checks the default created author.
Note that the author can be accessed after inserting the row.
"""
user = create_user()
g.user = user
e = EventWithOneDevice(device=Device())
db.session.add(e)
assert e.author is None
assert e.author_id is None
db.session.commit()
assert e.author == user

View File

@ -1,6 +1,8 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from uuid import uuid4 from uuid import uuid4
import pytest
from ereuse_devicehub.client import UserClient 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
@ -11,42 +13,40 @@ from ereuse_devicehub.resources.user.models import User
from tests.conftest import file from tests.conftest import file
def test_snapshot_model(app: Devicehub): @pytest.mark.usefixtures('auth_app_context')
def test_snapshot_model():
""" """
Tests creating a Snapshot with its relationships ensuring correct Tests creating a Snapshot with its relationships ensuring correct
DB mapping. DB mapping.
""" """
with app.app_context(): device = Microtower(serial_number='a1')
user = User(email='foo@bar.com') # noinspection PyArgumentList
device = Microtower(serial_number='a1') snapshot = Snapshot(uuid=uuid4(),
# noinspection PyArgumentList date=datetime.now(),
snapshot = Snapshot(uuid=uuid4(), version='1.0',
date=datetime.now(), software=SoftwareType.DesktopApp,
version='1.0', appearance=Appearance.A,
software=SoftwareType.DesktopApp, appearance_score=5,
appearance=Appearance.A, functionality=Functionality.A,
appearance_score=5, functionality_score=5,
functionality=Functionality.A, labelling=False,
functionality_score=5, bios=Bios.C,
labelling=False, condition=5,
bios=Bios.C, elapsed=timedelta(seconds=25))
condition=5, snapshot.device = device
elapsed=timedelta(seconds=25)) snapshot.request = SnapshotRequest(request={'foo': 'bar'})
snapshot.device = device
snapshot.author = user
snapshot.request = SnapshotRequest(request={'foo': 'bar'})
db.session.add(snapshot) db.session.add(snapshot)
db.session.commit() db.session.commit()
device = Microtower.query.one() # type: Microtower device = Microtower.query.one() # type: Microtower
assert device.events_one[0].type == Snapshot.__name__ assert device.events_one[0].type == Snapshot.__name__
db.session.delete(device) db.session.delete(device)
db.session.commit() db.session.commit()
assert Snapshot.query.one_or_none() is None assert Snapshot.query.one_or_none() is None
assert SnapshotRequest.query.one_or_none() is None assert SnapshotRequest.query.one_or_none() is None
assert User.query.one() is not None assert User.query.one() is not None
assert Microtower.query.one_or_none() is None assert Microtower.query.one_or_none() is None
assert Device.query.one_or_none() is None assert Device.query.one_or_none() is None
def test_snapshot_schema(app: Devicehub): def test_snapshot_schema(app: Devicehub):