Add Tests managing inventories; small bugfixes
This commit is contained in:
parent
6c4c89ac48
commit
15f705dd50
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import click.testing
|
import click.testing
|
||||||
|
import ereuse_utils
|
||||||
import flask.cli
|
import flask.cli
|
||||||
|
|
||||||
from ereuse_devicehub.config import DevicehubConfig
|
from ereuse_devicehub.config import DevicehubConfig
|
||||||
|
@ -19,11 +20,35 @@ class DevicehubGroup(flask.cli.FlaskGroup):
|
||||||
self.create_app = self.create_app_factory(inventory)
|
self.create_app = self.create_app_factory(inventory)
|
||||||
return super().main(*args, **kwargs)
|
return super().main(*args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def create_app_factory(inventory):
|
def create_app_factory(cls, inventory):
|
||||||
return lambda: Devicehub(inventory)
|
return lambda: Devicehub(inventory, config=cls.CONFIG())
|
||||||
|
|
||||||
|
|
||||||
@click.group(cls=DevicehubGroup)
|
def get_version(ctx, param, value):
|
||||||
|
if not value or ctx.resilient_parsing:
|
||||||
|
return
|
||||||
|
click.echo('Devicehub {}'.format(ereuse_utils.version('ereuse-devicehub')), color=ctx.color)
|
||||||
|
flask.cli.get_version(ctx, param, value)
|
||||||
|
|
||||||
|
|
||||||
|
@click.option('--version',
|
||||||
|
help='Devicehub version.',
|
||||||
|
expose_value=False,
|
||||||
|
callback=get_version,
|
||||||
|
is_flag=True,
|
||||||
|
is_eager=True)
|
||||||
|
@click.group(cls=DevicehubGroup,
|
||||||
|
context_settings=Devicehub.cli_context_settings,
|
||||||
|
add_version_option=False,
|
||||||
|
help="""
|
||||||
|
Manages the Devicehub of the inventory {}.
|
||||||
|
|
||||||
|
Use 'export dhi=xx' to set the inventory that this CLI
|
||||||
|
manages. For example 'export dhi=db1' and then executing
|
||||||
|
'dh tag add' adds a tag in the db1 database. Operations
|
||||||
|
that affect the common database (like creating an user)
|
||||||
|
are not affected by this.
|
||||||
|
""".format(os.environ.get('dhi')))
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
|
@ -42,12 +43,19 @@ class Devicehub(Teal):
|
||||||
super().__init__(config, db, inventory, import_name, static_url_path, static_folder,
|
super().__init__(config, db, inventory, import_name, static_url_path, static_folder,
|
||||||
static_host,
|
static_host,
|
||||||
host_matching, subdomain_matching, template_folder, instance_path,
|
host_matching, subdomain_matching, template_folder, instance_path,
|
||||||
instance_relative_config, root_path, Auth)
|
instance_relative_config, root_path, False, Auth)
|
||||||
self.id = inventory
|
self.id = inventory
|
||||||
"""The Inventory ID of this instance. In Teal is the app.schema."""
|
"""The Inventory ID of this instance. In Teal is the app.schema."""
|
||||||
self.dummy = Dummy(self)
|
self.dummy = Dummy(self)
|
||||||
self.cli.command('regenerate-search')(self.regenerate_search)
|
|
||||||
self.cli.command('init-db')(self.init_db)
|
@self.cli.group(short_help='Inventory management.',
|
||||||
|
help='Manages the inventory {}.'.format(os.environ.get('dhi')))
|
||||||
|
def inv():
|
||||||
|
pass
|
||||||
|
|
||||||
|
inv.command('add')(self.init_db)
|
||||||
|
inv.command('del')(self.delete_inventory)
|
||||||
|
inv.command('search')(self.regenerate_search)
|
||||||
self.before_request(self._prepare_request)
|
self.before_request(self._prepare_request)
|
||||||
|
|
||||||
# noinspection PyMethodOverriding
|
# noinspection PyMethodOverriding
|
||||||
|
@ -82,12 +90,21 @@ class Devicehub(Teal):
|
||||||
tag_token: uuid.UUID,
|
tag_token: uuid.UUID,
|
||||||
erase: bool,
|
erase: bool,
|
||||||
common: bool):
|
common: bool):
|
||||||
"""Initializes this inventory with the provided configurations."""
|
"""Creates an inventory.
|
||||||
|
|
||||||
|
This creates the database and adds the inventory to the
|
||||||
|
inventory tables with the passed-in settings, and does nothing if the
|
||||||
|
inventory already exists.
|
||||||
|
|
||||||
|
After you create the inventory you might want to create an user
|
||||||
|
executing *dh user add*.
|
||||||
|
"""
|
||||||
assert _app_ctx_stack.top, 'Use an app context.'
|
assert _app_ctx_stack.top, 'Use an app context.'
|
||||||
print('Initializing database...'.ljust(30), end='')
|
print('Initializing database...'.ljust(30), end='')
|
||||||
with click_spinner.spinner():
|
with click_spinner.spinner():
|
||||||
if erase:
|
if erase:
|
||||||
self.db.drop_all(common_schema=common)
|
self.db.drop_all(common_schema=common)
|
||||||
|
assert not db.has_schema(self.id), 'Schema {} already exists.'.format(self.id)
|
||||||
exclude_schema = 'common' if not common else None
|
exclude_schema = 'common' if not common else None
|
||||||
self._init_db(exclude_schema=exclude_schema)
|
self._init_db(exclude_schema=exclude_schema)
|
||||||
InventoryDef.set_inventory_config(name, org_name, org_id, tag_url, tag_token)
|
InventoryDef.set_inventory_config(name, org_name, org_id, tag_url, tag_token)
|
||||||
|
@ -96,6 +113,20 @@ class Devicehub(Teal):
|
||||||
self.db.session.commit()
|
self.db.session.commit()
|
||||||
print('done.')
|
print('done.')
|
||||||
|
|
||||||
|
@click.confirmation_option(prompt='Are you sure you want to delete the inventory {}?'
|
||||||
|
.format(os.environ.get('dhi')))
|
||||||
|
def delete_inventory(self):
|
||||||
|
"""Erases an inventory.
|
||||||
|
|
||||||
|
This removes its private database and its entry in the common
|
||||||
|
inventory.
|
||||||
|
|
||||||
|
This deletes users that have only access to this inventory.
|
||||||
|
"""
|
||||||
|
InventoryDef.delete_inventory()
|
||||||
|
self.db.session.commit()
|
||||||
|
self.db.drop_all(common_schema=False)
|
||||||
|
|
||||||
def regenerate_search(self):
|
def regenerate_search(self):
|
||||||
"""Re-creates from 0 all the search tables."""
|
"""Re-creates from 0 all the search tables."""
|
||||||
DeviceSearch.regenerate_search_table(self.db.session)
|
DeviceSearch.regenerate_search_table(self.db.session)
|
||||||
|
|
|
@ -63,27 +63,21 @@ class Dummy:
|
||||||
common=True)
|
common=True)
|
||||||
print('Creating stuff...'.ljust(30), end='')
|
print('Creating stuff...'.ljust(30), end='')
|
||||||
with click_spinner.spinner():
|
with click_spinner.spinner():
|
||||||
out = runner.invoke(args=['create-org', *self.ORG], catch_exceptions=False).output
|
out = runner.invoke('org', 'add', *self.ORG).output
|
||||||
org_id = json.loads(out)['id']
|
org_id = json.loads(out)['id']
|
||||||
user = self.user_client('user@dhub.com', '1234')
|
user = self.user_client('user@dhub.com', '1234')
|
||||||
# todo put user's agent into Org
|
# todo put user's agent into Org
|
||||||
for id in self.TAGS:
|
for id in self.TAGS:
|
||||||
user.post({'id': id}, res=Tag)
|
user.post({'id': id}, res=Tag)
|
||||||
for id, sec in self.ET:
|
for id, sec in self.ET:
|
||||||
runner.invoke(args=[
|
runner.invoke('tag', 'add', id,
|
||||||
'create-tag', id,
|
|
||||||
'-p', 'https://t.devicetag.io',
|
'-p', 'https://t.devicetag.io',
|
||||||
'-s', sec,
|
'-s', sec,
|
||||||
'-o', org_id
|
'-o', org_id)
|
||||||
],
|
|
||||||
catch_exceptions=False)
|
|
||||||
# create tag for pc-laudem
|
# create tag for pc-laudem
|
||||||
runner.invoke(args=[
|
runner.invoke('tag', 'add', 'tagA',
|
||||||
'create-tag', 'tagA',
|
|
||||||
'-p', 'https://t.devicetag.io',
|
'-p', 'https://t.devicetag.io',
|
||||||
'-s', 'tagA-secondary'
|
'-s', 'tagA-secondary')
|
||||||
],
|
|
||||||
catch_exceptions=False)
|
|
||||||
files = tuple(Path(__file__).parent.joinpath('files').iterdir())
|
files = tuple(Path(__file__).parent.joinpath('files').iterdir())
|
||||||
print('done.')
|
print('done.')
|
||||||
sample_pc = None # We treat this one as a special sample for demonstrations
|
sample_pc = None # We treat this one as a special sample for demonstrations
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
from boltons.typeutils import classproperty
|
||||||
from teal.resource import Converters, Resource
|
from teal.resource import Converters, Resource
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
|
@ -22,7 +23,7 @@ class OrganizationDef(AgentDef):
|
||||||
static_url_path=None,
|
static_url_path=None,
|
||||||
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||||
root_path=None):
|
root_path=None):
|
||||||
cli_commands = ((self.create_org, 'create-org'),)
|
cli_commands = ((self.create_org, 'add'),)
|
||||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||||
|
|
||||||
|
@ -44,6 +45,10 @@ class OrganizationDef(AgentDef):
|
||||||
print(json.dumps(o, indent=2))
|
print(json.dumps(o, indent=2))
|
||||||
return o
|
return o
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def cli_name(cls):
|
||||||
|
return 'org'
|
||||||
|
|
||||||
|
|
||||||
class Membership(Resource):
|
class Membership(Resource):
|
||||||
SCHEMA = schemas.Membership
|
SCHEMA = schemas.Membership
|
||||||
|
|
|
@ -83,8 +83,13 @@ class Agent(Thing):
|
||||||
|
|
||||||
class Organization(JoinedTableMixin, Agent):
|
class Organization(JoinedTableMixin, Agent):
|
||||||
default_of = db.relationship(Inventory,
|
default_of = db.relationship(Inventory,
|
||||||
single_parent=True,
|
|
||||||
uselist=False,
|
uselist=False,
|
||||||
|
lazy=True,
|
||||||
|
backref=backref('org', lazy=True),
|
||||||
|
# We need to use this as we cannot do Inventory.foreign -> Org
|
||||||
|
# as foreign keys can only reference to one table
|
||||||
|
# and we have multiple organization table (one per schema)
|
||||||
|
foreign_keys=[Inventory.org_id],
|
||||||
primaryjoin=lambda: Organization.id == Inventory.org_id)
|
primaryjoin=lambda: Organization.id == Inventory.org_id)
|
||||||
|
|
||||||
def __init__(self, name: str, **kwargs) -> None:
|
def __init__(self, name: str, **kwargs) -> None:
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import boltons.urlutils
|
import boltons.urlutils
|
||||||
import click
|
|
||||||
import ereuse_utils.cli
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from teal.db import ResourceNotFound
|
from teal.db import ResourceNotFound
|
||||||
from teal.resource import Resource
|
from teal.resource import Resource
|
||||||
|
@ -20,35 +18,8 @@ class InventoryDef(Resource):
|
||||||
static_url_path=None,
|
static_url_path=None,
|
||||||
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||||
root_path=None):
|
root_path=None):
|
||||||
cli_commands = (
|
|
||||||
(self.set_inventory_config_cli, 'set-inventory-config'),
|
|
||||||
)
|
|
||||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
url_prefix, subdomain, url_defaults, root_path)
|
||||||
|
|
||||||
@click.option('--name', '-n',
|
|
||||||
default='Test 1',
|
|
||||||
help='The human name of the inventory.')
|
|
||||||
@click.option('--org-name', '-on',
|
|
||||||
default=None,
|
|
||||||
help='The name of the default organization that owns this inventory.')
|
|
||||||
@click.option('--org-id', '-oi',
|
|
||||||
default=None,
|
|
||||||
help='The Tax ID of the organization.')
|
|
||||||
@click.option('--tag-url', '-tu',
|
|
||||||
type=ereuse_utils.cli.URL(scheme=True, host=True, path=False),
|
|
||||||
default=None,
|
|
||||||
help='The base url (scheme and host) of the tag provider.')
|
|
||||||
@click.option('--tag-token', '-tt',
|
|
||||||
type=click.UUID,
|
|
||||||
default=None,
|
|
||||||
help='The token provided by the tag provider. It is an UUID.')
|
|
||||||
def set_inventory_config_cli(self, **kwargs):
|
|
||||||
"""Sets the inventory configuration. Only updates passed-in
|
|
||||||
values.
|
|
||||||
"""
|
|
||||||
self.set_inventory_config(**kwargs)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_inventory_config(cls,
|
def set_inventory_config(cls,
|
||||||
|
@ -72,8 +43,23 @@ class InventoryDef(Resource):
|
||||||
except ResourceNotFound:
|
except ResourceNotFound:
|
||||||
org = Organization(tax_id=org_id, name=org_name)
|
org = Organization(tax_id=org_id, name=org_name)
|
||||||
org.default_of = inventory
|
org.default_of = inventory
|
||||||
db.session.add(org)
|
|
||||||
if tag_url:
|
if tag_url:
|
||||||
inventory.tag_provider = tag_url
|
inventory.tag_provider = tag_url
|
||||||
if tag_token:
|
if tag_token:
|
||||||
inventory.tag_token = tag_token
|
inventory.tag_token = tag_token
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete_inventory(cls):
|
||||||
|
"""Removes an inventory alongside with the users that have
|
||||||
|
only access to this inventory.
|
||||||
|
"""
|
||||||
|
from ereuse_devicehub.resources.user.models import User, UserInventory
|
||||||
|
inv = Inventory.query.filter_by(id=current_app.id).one()
|
||||||
|
db.session.delete(inv)
|
||||||
|
db.session.flush()
|
||||||
|
# Remove users that end-up without any inventory
|
||||||
|
# todo this should be done in a trigger / event
|
||||||
|
users = User.query \
|
||||||
|
.filter(User.id.notin_(db.session.query(UserInventory.user_id).distinct()))
|
||||||
|
for user in users:
|
||||||
|
db.session.delete(user)
|
||||||
|
|
|
@ -13,7 +13,8 @@ class Inventory(Thing):
|
||||||
tag_provider = db.Column(db.URL(), nullable=False)
|
tag_provider = db.Column(db.URL(), nullable=False)
|
||||||
tag_token = db.Column(db.UUID(as_uuid=True), unique=True, nullable=False)
|
tag_token = db.Column(db.UUID(as_uuid=True), unique=True, nullable=False)
|
||||||
tag_token.comment = """The token to access a Tag service."""
|
tag_token.comment = """The token to access a Tag service."""
|
||||||
org_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey('organization.id'), nullable=False)
|
# todo no validation that UUID is from an existing organization
|
||||||
|
org_id = db.Column(db.UUID(as_uuid=True), nullable=False)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.Index('id_hash', id, postgresql_using='hash'),
|
db.Index('id_hash', id, postgresql_using='hash'),
|
||||||
|
|
|
@ -29,8 +29,8 @@ class TagDef(Resource):
|
||||||
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
template_folder=None, url_prefix=None, subdomain=None, url_defaults=None,
|
||||||
root_path=None):
|
root_path=None):
|
||||||
cli_commands = (
|
cli_commands = (
|
||||||
(self.create_tag, 'create-tag'),
|
(self.create_tag, 'add'),
|
||||||
(self.create_tags_csv, 'create-tags-csv')
|
(self.create_tags_csv, 'add-csv')
|
||||||
)
|
)
|
||||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||||
|
|
|
@ -5,7 +5,6 @@ from flask import current_app
|
||||||
from teal.resource import Converters, Resource
|
from teal.resource import Converters, Resource
|
||||||
|
|
||||||
from ereuse_devicehub.db import db
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.inventory import Inventory
|
|
||||||
from ereuse_devicehub.resources.user import schemas
|
from ereuse_devicehub.resources.user import schemas
|
||||||
from ereuse_devicehub.resources.user.models import User
|
from ereuse_devicehub.resources.user.models import User
|
||||||
from ereuse_devicehub.resources.user.views import UserView, login
|
from ereuse_devicehub.resources.user.views import UserView, login
|
||||||
|
@ -20,7 +19,7 @@ class UserDef(Resource):
|
||||||
def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None,
|
def __init__(self, app, import_name=__name__.split('.')[0], static_folder=None,
|
||||||
static_url_path=None, template_folder=None, url_prefix=None, subdomain=None,
|
static_url_path=None, template_folder=None, url_prefix=None, subdomain=None,
|
||||||
url_defaults=None, root_path=None):
|
url_defaults=None, root_path=None):
|
||||||
cli_commands = ((self.create_user, 'create-user'),)
|
cli_commands = ((self.create_user, 'add'),)
|
||||||
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
super().__init__(app, import_name, static_folder, static_url_path, template_folder,
|
||||||
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
url_prefix, subdomain, url_defaults, root_path, cli_commands)
|
||||||
self.add_url_rule('/login/', view_func=login, methods={'POST'})
|
self.add_url_rule('/login/', view_func=login, methods={'POST'})
|
||||||
|
@ -29,7 +28,9 @@ class UserDef(Resource):
|
||||||
@option('-i', '--inventory',
|
@option('-i', '--inventory',
|
||||||
multiple=True,
|
multiple=True,
|
||||||
help='Inventories user has access to. By default this one.')
|
help='Inventories user has access to. By default this one.')
|
||||||
@option('-a', '--agent', help='The name of an agent to create with the user.')
|
@option('-a', '--agent',
|
||||||
|
help='Create too an Individual agent representing this user, '
|
||||||
|
'and give a name to this individual.')
|
||||||
@option('-c', '--country', help='The country of the agent (if --agent is set).')
|
@option('-c', '--country', help='The country of the agent (if --agent is set).')
|
||||||
@option('-t', '--telephone', help='The telephone of the agent (if --agent is set).')
|
@option('-t', '--telephone', help='The telephone of the agent (if --agent is set).')
|
||||||
@option('-t', '--tax-id', help='The tax id of the agent (if --agent is set).')
|
@option('-t', '--tax-id', help='The tax id of the agent (if --agent is set).')
|
||||||
|
@ -41,15 +42,16 @@ class UserDef(Resource):
|
||||||
country: str = None,
|
country: str = None,
|
||||||
telephone: str = None,
|
telephone: str = None,
|
||||||
tax_id: str = None) -> dict:
|
tax_id: str = None) -> dict:
|
||||||
"""Creates an user.
|
"""Create an user.
|
||||||
|
|
||||||
If ``--agent`` is passed, it creates an ``Individual`` agent
|
If ``--agent`` is passed, it creates too an ``Individual``
|
||||||
that represents the user.
|
agent that represents the user.
|
||||||
"""
|
"""
|
||||||
from ereuse_devicehub.resources.agent.models import Individual
|
from ereuse_devicehub.resources.agent.models import Individual
|
||||||
u = self.SCHEMA(only={'email', 'password'}, exclude=('token',)) \
|
u = self.SCHEMA(only={'email', 'password'}, exclude=('token',)) \
|
||||||
.load({'email': email, 'password': password})
|
.load({'email': email, 'password': password})
|
||||||
if inventory:
|
if inventory:
|
||||||
|
from ereuse_devicehub.resources.inventory import Inventory
|
||||||
inventory = Inventory.query.filter(Inventory.id.in_(inventory))
|
inventory = Inventory.query.filter(Inventory.id.in_(inventory))
|
||||||
user = User(**u, inventories=inventory)
|
user = User(**u, inventories=inventory)
|
||||||
agent = Individual(**current_app.resources[Individual.t].schema.load(
|
agent = Individual(**current_app.resources[Individual.t].schema.load(
|
||||||
|
|
|
@ -5,6 +5,7 @@ from sqlalchemy import Column
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy_utils import Password
|
from sqlalchemy_utils import Password
|
||||||
|
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
from ereuse_devicehub.resources.agent.models import Individual
|
from ereuse_devicehub.resources.agent.models import Individual
|
||||||
from ereuse_devicehub.resources.inventory import Inventory
|
from ereuse_devicehub.resources.inventory import Inventory
|
||||||
from ereuse_devicehub.resources.models import Thing
|
from ereuse_devicehub.resources.models import Thing
|
||||||
|
@ -30,3 +31,7 @@ class User(Thing):
|
||||||
@property
|
@property
|
||||||
def individual(self) -> Union[Individual, None]:
|
def individual(self) -> Union[Individual, None]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserInventory(db.Model):
|
||||||
|
pass
|
||||||
|
|
|
@ -5,7 +5,7 @@ click==6.7
|
||||||
click-spinner==0.1.8
|
click-spinner==0.1.8
|
||||||
colorama==0.3.9
|
colorama==0.3.9
|
||||||
colour==0.1.5
|
colour==0.1.5
|
||||||
ereuse-utils[naming, test, session, cli]==0.4.0b20
|
ereuse-utils[naming, test, session, cli]==0.4.0b21
|
||||||
Flask==1.0.2
|
Flask==1.0.2
|
||||||
Flask-Cors==3.0.6
|
Flask-Cors==3.0.6
|
||||||
Flask-SQLAlchemy==2.3.2
|
Flask-SQLAlchemy==2.3.2
|
||||||
|
@ -24,7 +24,7 @@ requests[security]==2.19.1
|
||||||
requests-mock==1.5.2
|
requests-mock==1.5.2
|
||||||
SQLAlchemy==1.2.17
|
SQLAlchemy==1.2.17
|
||||||
SQLAlchemy-Utils==0.33.11
|
SQLAlchemy-Utils==0.33.11
|
||||||
teal==0.2.0a35
|
teal==0.2.0a36
|
||||||
webargs==4.0.0
|
webargs==4.0.0
|
||||||
Werkzeug==0.14.1
|
Werkzeug==0.14.1
|
||||||
sqlalchemy-citext==1.3.post0
|
sqlalchemy-citext==1.3.post0
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -29,10 +29,10 @@ setup(
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type='text/markdown',
|
long_description_content_type='text/markdown',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'teal>=0.2.0a35', # teal always first
|
'teal>=0.2.0a36', # teal always first
|
||||||
'click',
|
'click',
|
||||||
'click-spinner',
|
'click-spinner',
|
||||||
'ereuse-utils[naming, test, session, cli]>=0.4b20',
|
'ereuse-utils[naming, test, session, cli]>=0.4b21',
|
||||||
'hashids',
|
'hashids',
|
||||||
'marshmallow_enum',
|
'marshmallow_enum',
|
||||||
'psycopg2-binary',
|
'psycopg2-binary',
|
||||||
|
|
|
@ -62,7 +62,7 @@ def app(request, _app: Devicehub) -> Devicehub:
|
||||||
try:
|
try:
|
||||||
with redirect_stdout(io.StringIO()):
|
with redirect_stdout(io.StringIO()):
|
||||||
_init()
|
_init()
|
||||||
except (ProgrammingError, IntegrityError):
|
except (ProgrammingError, IntegrityError, AssertionError):
|
||||||
print('Database was not correctly emptied. Re-empty and re-installing...')
|
print('Database was not correctly emptied. Re-empty and re-installing...')
|
||||||
_drop()
|
_drop()
|
||||||
_init()
|
_init()
|
||||||
|
|
|
@ -209,7 +209,7 @@ def test_device_search_regenerate_table(app: DeviceSearch, user: UserClient):
|
||||||
i, _ = user.get(res=Device, query=[('search', 'Desktop')])
|
i, _ = user.get(res=Device, query=[('search', 'Desktop')])
|
||||||
assert not i['items'], 'Truncate deleted all items'
|
assert not i['items'], 'Truncate deleted all items'
|
||||||
runner = app.test_cli_runner()
|
runner = app.test_cli_runner()
|
||||||
runner.invoke(args=['regenerate-search'], catch_exceptions=False)
|
runner.invoke('inv', 'search')
|
||||||
i, _ = user.get(res=Device, query=[('search', 'Desktop')])
|
i, _ = user.get(res=Device, query=[('search', 'Desktop')])
|
||||||
assert i['items'], 'Regenerated re-made the table'
|
assert i['items'], 'Regenerated re-made the table'
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ def noop():
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def dispatcher(app: Devicehub, config: TestConfig) -> PathDispatcher:
|
def dispatcher(app: Devicehub, config: TestConfig) -> PathDispatcher:
|
||||||
print('whoho')
|
|
||||||
PathDispatcher.call = Mock(side_effect=lambda *args: args[0])
|
PathDispatcher.call = Mock(side_effect=lambda *args: args[0])
|
||||||
return PathDispatcher(config_cls=config)
|
return PathDispatcher(config_cls=config)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,6 @@ from ereuse_devicehub.devicehub import Devicehub
|
||||||
def test_dummy(_app: Devicehub):
|
def test_dummy(_app: Devicehub):
|
||||||
"""Tests the dummy cli command."""
|
"""Tests the dummy cli command."""
|
||||||
runner = _app.test_cli_runner()
|
runner = _app.test_cli_runner()
|
||||||
runner.invoke(args=['dummy', '--yes'], catch_exceptions=False)
|
runner.invoke('dummy', '--yes')
|
||||||
with _app.app_context():
|
with _app.app_context():
|
||||||
_app.db.drop_all()
|
_app.db.drop_all()
|
||||||
|
|
|
@ -1,19 +1,147 @@
|
||||||
|
from typing import List
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
import click.testing
|
||||||
import pytest
|
import pytest
|
||||||
|
from boltons.urlutils import URL
|
||||||
|
|
||||||
|
import ereuse_devicehub.cli
|
||||||
|
from ereuse_devicehub.db import db
|
||||||
|
from ereuse_devicehub.devicehub import Devicehub
|
||||||
|
from ereuse_devicehub.resources.agent.models import Organization
|
||||||
|
from ereuse_devicehub.resources.inventory import Inventory
|
||||||
|
from ereuse_devicehub.resources.user import User
|
||||||
|
from tests.conftest import TestConfig
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='Test not developed')
|
|
||||||
def test_create_inventory():
|
|
||||||
"""Tests creating an inventory with an user."""
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='Test not developed')
|
|
||||||
def test_create_existing_inventory():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='Test not developed')
|
|
||||||
def test_delete_inventory():
|
|
||||||
"""Tests deleting an inventory without
|
|
||||||
disturbing other inventories (ex. keeping commmon db), and
|
|
||||||
removing its traces in common (no inventory row in inventory table).
|
|
||||||
"""
|
"""
|
||||||
|
Tests the management of inventories in a multi-inventory environment
|
||||||
|
(several Devicehub instances that point at different schemas).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class NoExcCliRunner(click.testing.CliRunner):
|
||||||
|
"""Runner that interfaces with the Devicehub CLI."""
|
||||||
|
|
||||||
|
def invoke(self, *args, input=None, env=None, catch_exceptions=False, color=False,
|
||||||
|
**extra):
|
||||||
|
r = super().invoke(ereuse_devicehub.cli.cli,
|
||||||
|
args, input, env, catch_exceptions, color, **extra)
|
||||||
|
assert r.exit_code == 0, 'CLI code {}: {}'.format(r.exit_code, r.output)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def inv(self, name: str):
|
||||||
|
"""Set an inventory as an environment variable."""
|
||||||
|
self.env = {'dhi': name}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def cli(config, _app):
|
||||||
|
"""Returns an interface for the dh CLI client,
|
||||||
|
cleaning the database afterwards.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def drop_schemas():
|
||||||
|
with _app.app_context():
|
||||||
|
_app.db.drop_schema(schema='tdb1')
|
||||||
|
_app.db.drop_schema(schema='tdb2')
|
||||||
|
_app.db.drop_schema(schema='common')
|
||||||
|
|
||||||
|
drop_schemas()
|
||||||
|
ereuse_devicehub.cli.DevicehubGroup.CONFIG = TestConfig
|
||||||
|
yield NoExcCliRunner()
|
||||||
|
drop_schemas()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def tdb1(config):
|
||||||
|
return Devicehub(inventory='tdb1', config=config, db=db)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def tdb2(config):
|
||||||
|
return Devicehub(inventory='tdb2', config=config, db=db)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inventory_create_delete_user(cli, tdb1, tdb2):
|
||||||
|
"""Tests creating two inventories with users, one user has
|
||||||
|
access to the first inventory and the other to both. Finally, deletes
|
||||||
|
the first inventory, deleting only the first user too.
|
||||||
|
"""
|
||||||
|
# Create first DB
|
||||||
|
cli.inv('tdb1')
|
||||||
|
cli.invoke('inv', 'add',
|
||||||
|
'-n', 'Test DB1',
|
||||||
|
'-on', 'ACME DB1',
|
||||||
|
'-oi', 'acme-id',
|
||||||
|
'-tu', 'https://example.com',
|
||||||
|
'-tt', '3c66a6ad-22de-4db6-ac46-d8982522ec40',
|
||||||
|
'--common')
|
||||||
|
|
||||||
|
# Create an user for first DB
|
||||||
|
cli.invoke('user', 'add', 'foo@foo.com', '-a', 'Foo', '-c', 'ES', '-p', 'Such password')
|
||||||
|
|
||||||
|
with tdb1.app_context():
|
||||||
|
# There is a row for the inventory
|
||||||
|
inv = Inventory.query.one() # type: Inventory
|
||||||
|
assert inv.id == 'tdb1'
|
||||||
|
assert inv.name == 'Test DB1'
|
||||||
|
assert inv.tag_provider == URL('https://example.com')
|
||||||
|
assert inv.tag_token == UUID('3c66a6ad-22de-4db6-ac46-d8982522ec40')
|
||||||
|
assert db.has_schema('tdb1')
|
||||||
|
org = Organization.query.one() # type: Organization
|
||||||
|
# assert inv.org_id == org.id
|
||||||
|
assert org.name == 'ACME DB1'
|
||||||
|
assert org.tax_id == 'acme-id'
|
||||||
|
user = User.query.one() # type: User
|
||||||
|
assert user.email == 'foo@foo.com'
|
||||||
|
|
||||||
|
cli.inv('tdb2')
|
||||||
|
# Create a second DB
|
||||||
|
# Note how we don't create common anymore
|
||||||
|
cli.invoke('inv', 'add',
|
||||||
|
'-n', 'Test DB2',
|
||||||
|
'-on', 'ACME DB2',
|
||||||
|
'-oi', 'acme-id-2',
|
||||||
|
'-tu', 'https://example.com',
|
||||||
|
'-tt', 'fbad1c08-ffdc-4a61-be49-464962c186a8')
|
||||||
|
# Create an user for with access for both DB
|
||||||
|
cli.invoke('user', 'add', 'bar@bar.com', '-a', 'Bar', '-p', 'Wow password')
|
||||||
|
|
||||||
|
with tdb2.app_context():
|
||||||
|
inventories = Inventory.query.all() # type: List[Inventory]
|
||||||
|
assert len(inventories) == 2
|
||||||
|
assert inventories[0].id == 'tdb1'
|
||||||
|
assert inventories[1].id == 'tdb2'
|
||||||
|
assert db.has_schema('tdb2')
|
||||||
|
org_db2 = Organization.query.one()
|
||||||
|
assert org_db2 != org
|
||||||
|
assert org_db2.name == 'ACME DB2'
|
||||||
|
users = User.query.all() # type: List[User]
|
||||||
|
assert users[0].email == 'foo@foo.com'
|
||||||
|
assert users[1].email == 'bar@bar.com'
|
||||||
|
|
||||||
|
# Delete tdb1
|
||||||
|
cli.inv('tdb1')
|
||||||
|
cli.invoke('inv', 'del', '--yes')
|
||||||
|
|
||||||
|
with tdb2.app_context():
|
||||||
|
# There is only tdb2 as inventory
|
||||||
|
inv = Inventory.query.one() # type: Inventory
|
||||||
|
assert inv.id == 'tdb2'
|
||||||
|
# User foo@foo.com is deleted because it only
|
||||||
|
# existed in tdb1, but not bar@bar.com which existed
|
||||||
|
# in another inventory too (tdb2)
|
||||||
|
user = User.query.one() # type: User
|
||||||
|
assert user.email == 'bar@bar.com'
|
||||||
|
assert not db.has_schema('tdb1')
|
||||||
|
assert db.has_schema('tdb2')
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_existing_inventory(cli, tdb1):
|
||||||
|
"""Tries to create twice the same inventory."""
|
||||||
|
cli.inv('tdb1')
|
||||||
|
cli.invoke('inv', 'add', '--common')
|
||||||
|
with tdb1.app_context():
|
||||||
|
assert db.has_schema('tdb1')
|
||||||
|
with pytest.raises(AssertionError, message='Schema tdb1 already exists.'):
|
||||||
|
cli.invoke('inv', 'add', '--common')
|
||||||
|
|
|
@ -137,7 +137,7 @@ def test_tag_get_device_from_tag_endpoint_multiple_tags(app: Devicehub, user: Us
|
||||||
def test_tag_create_tags_cli(app: Devicehub, user: UserClient):
|
def test_tag_create_tags_cli(app: Devicehub, user: UserClient):
|
||||||
"""Checks creating tags with the CLI endpoint."""
|
"""Checks creating tags with the CLI endpoint."""
|
||||||
runner = app.test_cli_runner()
|
runner = app.test_cli_runner()
|
||||||
runner.invoke(args=['create-tag', 'id1'], catch_exceptions=False)
|
runner.invoke('tag', 'add', 'id1')
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
tag = Tag.query.one() # type: Tag
|
tag = Tag.query.one() # type: Tag
|
||||||
assert tag.id == 'id1'
|
assert tag.id == 'id1'
|
||||||
|
@ -148,8 +148,7 @@ def test_tag_create_etags_cli(app: Devicehub, user: UserClient):
|
||||||
"""Creates an eTag through the CLI."""
|
"""Creates an eTag through the CLI."""
|
||||||
# todo what happens to organization?
|
# todo what happens to organization?
|
||||||
runner = app.test_cli_runner()
|
runner = app.test_cli_runner()
|
||||||
runner.invoke(args=['create-tag', '-p', 'https://t.ereuse.org', '-s', 'foo', 'DT-BARBAR'],
|
runner.invoke('tag', 'add', '-p', 'https://t.ereuse.org', '-s', 'foo', 'DT-BARBAR')
|
||||||
catch_exceptions=False)
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
tag = Tag.query.one() # type: Tag
|
tag = Tag.query.one() # type: Tag
|
||||||
assert tag.id == 'dt-barbar'
|
assert tag.id == 'dt-barbar'
|
||||||
|
@ -222,8 +221,7 @@ def test_tag_create_tags_cli_csv(app: Devicehub, user: UserClient):
|
||||||
"""Checks creating tags with the CLI endpoint using a CSV."""
|
"""Checks creating tags with the CLI endpoint using a CSV."""
|
||||||
csv = pathlib.Path(__file__).parent / 'files' / 'tags-cli.csv'
|
csv = pathlib.Path(__file__).parent / 'files' / 'tags-cli.csv'
|
||||||
runner = app.test_cli_runner()
|
runner = app.test_cli_runner()
|
||||||
runner.invoke(args=['create-tags-csv', str(csv)],
|
runner.invoke('tag', 'add-csv', str(csv))
|
||||||
catch_exceptions=False)
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
t1 = Tag.from_an_id('id1').one()
|
t1 = Tag.from_an_id('id1').one()
|
||||||
t2 = Tag.from_an_id('sec1').one()
|
t2 = Tag.from_an_id('sec1').one()
|
||||||
|
|
Reference in a new issue