diff --git a/ereuse_devicehub/cli.py b/ereuse_devicehub/cli.py index 01da3e47..993d00f3 100644 --- a/ereuse_devicehub/cli.py +++ b/ereuse_devicehub/cli.py @@ -1,6 +1,7 @@ import os import click.testing +import ereuse_utils import flask.cli from ereuse_devicehub.config import DevicehubConfig @@ -19,11 +20,35 @@ class DevicehubGroup(flask.cli.FlaskGroup): self.create_app = self.create_app_factory(inventory) return super().main(*args, **kwargs) - @staticmethod - def create_app_factory(inventory): - return lambda: Devicehub(inventory) + @classmethod + def create_app_factory(cls, 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(): pass diff --git a/ereuse_devicehub/devicehub.py b/ereuse_devicehub/devicehub.py index c3925ec8..4499268b 100644 --- a/ereuse_devicehub/devicehub.py +++ b/ereuse_devicehub/devicehub.py @@ -1,3 +1,4 @@ +import os import uuid from typing import Type @@ -42,12 +43,19 @@ class Devicehub(Teal): super().__init__(config, db, inventory, import_name, static_url_path, static_folder, static_host, host_matching, subdomain_matching, template_folder, instance_path, - instance_relative_config, root_path, Auth) + instance_relative_config, root_path, False, Auth) self.id = inventory """The Inventory ID of this instance. In Teal is the app.schema.""" 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) # noinspection PyMethodOverriding @@ -82,12 +90,21 @@ class Devicehub(Teal): tag_token: uuid.UUID, erase: 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.' print('Initializing database...'.ljust(30), end='') with click_spinner.spinner(): if erase: 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 self._init_db(exclude_schema=exclude_schema) InventoryDef.set_inventory_config(name, org_name, org_id, tag_url, tag_token) @@ -96,6 +113,20 @@ class Devicehub(Teal): self.db.session.commit() 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): """Re-creates from 0 all the search tables.""" DeviceSearch.regenerate_search_table(self.db.session) diff --git a/ereuse_devicehub/dummy/dummy.py b/ereuse_devicehub/dummy/dummy.py index 4495a821..97977cda 100644 --- a/ereuse_devicehub/dummy/dummy.py +++ b/ereuse_devicehub/dummy/dummy.py @@ -63,27 +63,21 @@ class Dummy: common=True) print('Creating stuff...'.ljust(30), end='') 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'] user = self.user_client('user@dhub.com', '1234') # todo put user's agent into Org for id in self.TAGS: user.post({'id': id}, res=Tag) for id, sec in self.ET: - runner.invoke(args=[ - 'create-tag', id, - '-p', 'https://t.devicetag.io', - '-s', sec, - '-o', org_id - ], - catch_exceptions=False) + runner.invoke('tag', 'add', id, + '-p', 'https://t.devicetag.io', + '-s', sec, + '-o', org_id) # create tag for pc-laudem - runner.invoke(args=[ - 'create-tag', 'tagA', - '-p', 'https://t.devicetag.io', - '-s', 'tagA-secondary' - ], - catch_exceptions=False) + runner.invoke('tag', 'add', 'tagA', + '-p', 'https://t.devicetag.io', + '-s', 'tagA-secondary') files = tuple(Path(__file__).parent.joinpath('files').iterdir()) print('done.') sample_pc = None # We treat this one as a special sample for demonstrations diff --git a/ereuse_devicehub/resources/agent/__init__.py b/ereuse_devicehub/resources/agent/__init__.py index f0b48d24..20d4945d 100644 --- a/ereuse_devicehub/resources/agent/__init__.py +++ b/ereuse_devicehub/resources/agent/__init__.py @@ -1,6 +1,7 @@ import json import click +from boltons.typeutils import classproperty from teal.resource import Converters, Resource from ereuse_devicehub.db import db @@ -22,7 +23,7 @@ class OrganizationDef(AgentDef): static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=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, url_prefix, subdomain, url_defaults, root_path, cli_commands) @@ -44,6 +45,10 @@ class OrganizationDef(AgentDef): print(json.dumps(o, indent=2)) return o + @classproperty + def cli_name(cls): + return 'org' + class Membership(Resource): SCHEMA = schemas.Membership diff --git a/ereuse_devicehub/resources/agent/models.py b/ereuse_devicehub/resources/agent/models.py index 930c39e9..78f4ac09 100644 --- a/ereuse_devicehub/resources/agent/models.py +++ b/ereuse_devicehub/resources/agent/models.py @@ -83,8 +83,13 @@ class Agent(Thing): class Organization(JoinedTableMixin, Agent): default_of = db.relationship(Inventory, - single_parent=True, 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) def __init__(self, name: str, **kwargs) -> None: diff --git a/ereuse_devicehub/resources/inventory/__init__.py b/ereuse_devicehub/resources/inventory/__init__.py index 28eed958..d9b7c374 100644 --- a/ereuse_devicehub/resources/inventory/__init__.py +++ b/ereuse_devicehub/resources/inventory/__init__.py @@ -1,8 +1,6 @@ import uuid import boltons.urlutils -import click -import ereuse_utils.cli from flask import current_app from teal.db import ResourceNotFound from teal.resource import Resource @@ -20,35 +18,8 @@ class InventoryDef(Resource): static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=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, - url_prefix, subdomain, url_defaults, root_path, cli_commands) - - @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() + url_prefix, subdomain, url_defaults, root_path) @classmethod def set_inventory_config(cls, @@ -72,8 +43,23 @@ class InventoryDef(Resource): except ResourceNotFound: org = Organization(tax_id=org_id, name=org_name) org.default_of = inventory - db.session.add(org) if tag_url: inventory.tag_provider = tag_url if 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) diff --git a/ereuse_devicehub/resources/inventory/model.py b/ereuse_devicehub/resources/inventory/model.py index 98a86c97..ef200603 100644 --- a/ereuse_devicehub/resources/inventory/model.py +++ b/ereuse_devicehub/resources/inventory/model.py @@ -13,7 +13,8 @@ class Inventory(Thing): tag_provider = db.Column(db.URL(), 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.""" - 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__ = ( db.Index('id_hash', id, postgresql_using='hash'), diff --git a/ereuse_devicehub/resources/tag/__init__.py b/ereuse_devicehub/resources/tag/__init__.py index db1e2752..816d89c9 100644 --- a/ereuse_devicehub/resources/tag/__init__.py +++ b/ereuse_devicehub/resources/tag/__init__.py @@ -29,8 +29,8 @@ class TagDef(Resource): template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, root_path=None): cli_commands = ( - (self.create_tag, 'create-tag'), - (self.create_tags_csv, 'create-tags-csv') + (self.create_tag, 'add'), + (self.create_tags_csv, 'add-csv') ) super().__init__(app, import_name, static_folder, static_url_path, template_folder, url_prefix, subdomain, url_defaults, root_path, cli_commands) diff --git a/ereuse_devicehub/resources/user/__init__.py b/ereuse_devicehub/resources/user/__init__.py index d749bb59..ec9eed78 100644 --- a/ereuse_devicehub/resources/user/__init__.py +++ b/ereuse_devicehub/resources/user/__init__.py @@ -5,7 +5,6 @@ from flask import current_app from teal.resource import Converters, Resource 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.models import User 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, static_url_path=None, template_folder=None, url_prefix=None, subdomain=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, url_prefix, subdomain, url_defaults, root_path, cli_commands) self.add_url_rule('/login/', view_func=login, methods={'POST'}) @@ -29,7 +28,9 @@ class UserDef(Resource): @option('-i', '--inventory', multiple=True, 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('-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).') @@ -41,15 +42,16 @@ class UserDef(Resource): country: str = None, telephone: str = None, tax_id: str = None) -> dict: - """Creates an user. + """Create an user. - If ``--agent`` is passed, it creates an ``Individual`` agent - that represents the user. + If ``--agent`` is passed, it creates too an ``Individual`` + agent that represents the user. """ from ereuse_devicehub.resources.agent.models import Individual u = self.SCHEMA(only={'email', 'password'}, exclude=('token',)) \ .load({'email': email, 'password': password}) if inventory: + from ereuse_devicehub.resources.inventory import Inventory inventory = Inventory.query.filter(Inventory.id.in_(inventory)) user = User(**u, inventories=inventory) agent = Individual(**current_app.resources[Individual.t].schema.load( diff --git a/ereuse_devicehub/resources/user/models.pyi b/ereuse_devicehub/resources/user/models.pyi index c6dd4754..6e8d03b9 100644 --- a/ereuse_devicehub/resources/user/models.pyi +++ b/ereuse_devicehub/resources/user/models.pyi @@ -5,6 +5,7 @@ from sqlalchemy import Column from sqlalchemy.orm import relationship from sqlalchemy_utils import Password +from ereuse_devicehub.db import db from ereuse_devicehub.resources.agent.models import Individual from ereuse_devicehub.resources.inventory import Inventory from ereuse_devicehub.resources.models import Thing @@ -30,3 +31,7 @@ class User(Thing): @property def individual(self) -> Union[Individual, None]: pass + + +class UserInventory(db.Model): + pass diff --git a/requirements.txt b/requirements.txt index aa8fb818..bf9d37e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ click==6.7 click-spinner==0.1.8 colorama==0.3.9 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-Cors==3.0.6 Flask-SQLAlchemy==2.3.2 @@ -24,7 +24,7 @@ requests[security]==2.19.1 requests-mock==1.5.2 SQLAlchemy==1.2.17 SQLAlchemy-Utils==0.33.11 -teal==0.2.0a35 +teal==0.2.0a36 webargs==4.0.0 Werkzeug==0.14.1 sqlalchemy-citext==1.3.post0 diff --git a/setup.py b/setup.py index 39d48f7f..fb53a90d 100644 --- a/setup.py +++ b/setup.py @@ -29,10 +29,10 @@ setup( long_description=long_description, long_description_content_type='text/markdown', install_requires=[ - 'teal>=0.2.0a35', # teal always first + 'teal>=0.2.0a36', # teal always first 'click', 'click-spinner', - 'ereuse-utils[naming, test, session, cli]>=0.4b20', + 'ereuse-utils[naming, test, session, cli]>=0.4b21', 'hashids', 'marshmallow_enum', 'psycopg2-binary', diff --git a/tests/conftest.py b/tests/conftest.py index 63005389..bd6209ed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,7 +62,7 @@ def app(request, _app: Devicehub) -> Devicehub: try: with redirect_stdout(io.StringIO()): _init() - except (ProgrammingError, IntegrityError): + except (ProgrammingError, IntegrityError, AssertionError): print('Database was not correctly emptied. Re-empty and re-installing...') _drop() _init() diff --git a/tests/test_device_find.py b/tests/test_device_find.py index e0765d2e..1d0a23ea 100644 --- a/tests/test_device_find.py +++ b/tests/test_device_find.py @@ -209,7 +209,7 @@ def test_device_search_regenerate_table(app: DeviceSearch, user: UserClient): i, _ = user.get(res=Device, query=[('search', 'Desktop')]) assert not i['items'], 'Truncate deleted all items' 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')]) assert i['items'], 'Regenerated re-made the table' diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index cf9a735e..6d210a4b 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -13,7 +13,6 @@ def noop(): @pytest.fixture() def dispatcher(app: Devicehub, config: TestConfig) -> PathDispatcher: - print('whoho') PathDispatcher.call = Mock(side_effect=lambda *args: args[0]) return PathDispatcher(config_cls=config) diff --git a/tests/test_dummy.py b/tests/test_dummy.py index 1c857242..dd5a7dc7 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -4,6 +4,6 @@ from ereuse_devicehub.devicehub import Devicehub def test_dummy(_app: Devicehub): """Tests the dummy cli command.""" runner = _app.test_cli_runner() - runner.invoke(args=['dummy', '--yes'], catch_exceptions=False) + runner.invoke('dummy', '--yes') with _app.app_context(): _app.db.drop_all() diff --git a/tests/test_inventory.py b/tests/test_inventory.py index b1c60f5c..e41b6857 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -1,19 +1,147 @@ +from typing import List +from uuid import UUID + +import click.testing 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 + +""" +Tests the management of inventories in a multi-inventory environment +(several Devicehub instances that point at different schemas). +""" -@pytest.mark.xfail(reason='Test not developed') -def test_create_inventory(): - """Tests creating an inventory with an user.""" +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.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). +@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') diff --git a/tests/test_tag.py b/tests/test_tag.py index 47bd2213..cb115ac9 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -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): """Checks creating tags with the CLI endpoint.""" runner = app.test_cli_runner() - runner.invoke(args=['create-tag', 'id1'], catch_exceptions=False) + runner.invoke('tag', 'add', 'id1') with app.app_context(): tag = Tag.query.one() # type: Tag assert tag.id == 'id1' @@ -148,8 +148,7 @@ def test_tag_create_etags_cli(app: Devicehub, user: UserClient): """Creates an eTag through the CLI.""" # todo what happens to organization? runner = app.test_cli_runner() - runner.invoke(args=['create-tag', '-p', 'https://t.ereuse.org', '-s', 'foo', 'DT-BARBAR'], - catch_exceptions=False) + runner.invoke('tag', 'add', '-p', 'https://t.ereuse.org', '-s', 'foo', 'DT-BARBAR') with app.app_context(): tag = Tag.query.one() # type: Tag 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.""" csv = pathlib.Path(__file__).parent / 'files' / 'tags-cli.csv' runner = app.test_cli_runner() - runner.invoke(args=['create-tags-csv', str(csv)], - catch_exceptions=False) + runner.invoke('tag', 'add-csv', str(csv)) with app.app_context(): t1 = Tag.from_an_id('id1').one() t2 = Tag.from_an_id('sec1').one()