From 76048f3abce253705863a104d723a44e8b84fac9 Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 16 Mar 2019 16:00:21 -0300 Subject: [PATCH 01/10] docs/admin_api.md: add draft for upcoming endpoints --- docs/admin_api.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/admin_api.md b/docs/admin_api.md index 4dc8863..ab28751 100644 --- a/docs/admin_api.md +++ b/docs/admin_api.md @@ -2,6 +2,43 @@ the base path is `/api/v6/admin`. +## User management + +### `PUT /users` + +Create a user. +Returns a user object. + +| field | type | description | +| --: | :-- | :-- | +| username | string | username | +| email | email | the email of the new user | +| password | str | password for the new user | + +### `GET /users` + +Search users. + +**TODO: query args** + +### `DELETE /users/` + +**TODO** + +## Instance invites + +### `GET /instance/invites` + +**TODO** + +### `PUT /instance/invites` + +**TODO** + +### `DELETE /instance/invites/` + +**TODO** + ## Voice ### GET `/voice/regions/` From 5fb27c04a2e18d31377ae564229668170e31f200 Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 16 Mar 2019 16:45:13 -0300 Subject: [PATCH 02/10] add admin_api.{users, instance_invites} blueprints - admin_schemas: add USER_CREATE --- litecord/admin_schemas.py | 6 +++ litecord/blueprints/admin_api/__init__.py | 4 +- .../blueprints/admin_api/instance_invites.py | 36 +++++++++++++++++ litecord/blueprints/admin_api/users.py | 40 +++++++++++++++++++ manage/cmd/migration/__init__.py | 2 + manage/cmd/users.py | 3 -- run.py | 6 ++- 7 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 litecord/blueprints/admin_api/instance_invites.py create mode 100644 litecord/blueprints/admin_api/users.py diff --git a/litecord/admin_schemas.py b/litecord/admin_schemas.py index 18d0e50..8f2a001 100644 --- a/litecord/admin_schemas.py +++ b/litecord/admin_schemas.py @@ -40,3 +40,9 @@ FEATURES = { 'schema': {'coerce': lambda x: Feature(x)} } } + +USER_CREATE = { + 'username': {'type': 'username', 'required': True}, + 'email': {'type': 'email', 'required': True}, + 'password': {'type': 'string', 'minlength': 5, 'required': True}, +} diff --git a/litecord/blueprints/admin_api/__init__.py b/litecord/blueprints/admin_api/__init__.py index 32710ce..d27cc52 100644 --- a/litecord/blueprints/admin_api/__init__.py +++ b/litecord/blueprints/admin_api/__init__.py @@ -20,5 +20,7 @@ along with this program. If not, see . from .voice import bp as voice from .features import bp as features from .guilds import bp as guilds +from .users import bp as users +from .instance_invites import bp as instance_invites -__all__ = ['voice', 'features', 'guilds'] +__all__ = ['voice', 'features', 'guilds', 'users', 'instance_invites'] diff --git a/litecord/blueprints/admin_api/instance_invites.py b/litecord/blueprints/admin_api/instance_invites.py new file mode 100644 index 0000000..70c6106 --- /dev/null +++ b/litecord/blueprints/admin_api/instance_invites.py @@ -0,0 +1,36 @@ +""" + +Litecord +Copyright (C) 2018-2019 Luna Mendes + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +""" + +from quart import Blueprint, jsonify, current_app as app + +from litecord.auth import admin_check + +bp = Blueprint('instance_invites', __name__) + + +@bp.route('', methods=['GET']) +async def _all_instance_invites(): + await admin_check() + + rows = await app.db.fetch(""" + SELECT code, created_at, uses, max_uses + FROM instance_invites + """) + + return jsonify([dict(row) for row in rows]) diff --git a/litecord/blueprints/admin_api/users.py b/litecord/blueprints/admin_api/users.py new file mode 100644 index 0000000..67690c8 --- /dev/null +++ b/litecord/blueprints/admin_api/users.py @@ -0,0 +1,40 @@ +""" + +Litecord +Copyright (C) 2018-2019 Luna Mendes + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +""" + +from quart import Blueprint, jsonify, current_app as app, request + +from litecord.auth import admin_check +from litecord.blueprints.auth import create_user +from litecord.schemas import validate +from litecord.admin_schemas import USER_CREATE + +bp = Blueprint('users_admin', __name__) + +@bp.route('', methods=['POST']) +async def _create_user(): + await admin_check() + + j = validate(await request.get_json(), USER_CREATE) + + user_id = await create_user(j['username'], j['email'], j['password']) + + return jsonify( + await app.storage.get_user(user_id) + ) + diff --git a/manage/cmd/migration/__init__.py b/manage/cmd/migration/__init__.py index e2363a2..d685676 100644 --- a/manage/cmd/migration/__init__.py +++ b/manage/cmd/migration/__init__.py @@ -18,3 +18,5 @@ along with this program. If not, see . """ from .command import setup as migration + +__all__ = ['migration'] diff --git a/manage/cmd/users.py b/manage/cmd/users.py index 1516673..4d37757 100644 --- a/manage/cmd/users.py +++ b/manage/cmd/users.py @@ -17,9 +17,6 @@ along with this program. If not, see . """ -import base64 -import itsdangerous -import bcrypt from litecord.blueprints.auth import create_user, make_token from litecord.blueprints.users import delete_user from litecord.enums import UserFlags diff --git a/run.py b/run.py index 15ce2b4..f8bf1c7 100644 --- a/run.py +++ b/run.py @@ -58,7 +58,7 @@ from litecord.blueprints.user.billing_job import payment_job from litecord.blueprints.admin_api import ( voice as voice_admin, features as features_admin, - guilds as guilds_admin + guilds as guilds_admin, users as users_admin, instance_invites ) from litecord.blueprints.admin_api.voice import guild_region_check @@ -146,7 +146,9 @@ def set_blueprints(app_): voice_admin: '/admin/voice', features_admin: '/admin/guilds', - guilds_admin: '/admin/guilds' + guilds_admin: '/admin/guilds', + users_admin: '/admin/users', + instance_invites: '/admin/instance/invites' } for bp, suffix in bps.items(): From 78aba9432e1f6b89098ba59259febc7430fd5117 Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 16 Mar 2019 17:23:00 -0300 Subject: [PATCH 03/10] admin_api.instance_invites add impls for other endpoints - docs/admin_api.md: document them - docs/admin_api.md: add basic usage of the api --- docs/admin_api.md | 34 +++++++- litecord/admin_schemas.py | 4 + .../blueprints/admin_api/instance_invites.py | 77 ++++++++++++++++++- 3 files changed, 109 insertions(+), 6 deletions(-) diff --git a/docs/admin_api.md b/docs/admin_api.md index ab28751..2a5f7c4 100644 --- a/docs/admin_api.md +++ b/docs/admin_api.md @@ -1,6 +1,13 @@ # Litecord Admin API -the base path is `/api/v6/admin`. +Litecord's Admin API uses the same authentication methods as Discord, +it's the same `Authorization` header, and the same token. + +Only users who have the staff flag set can use the Admin API. Instance +owners can use the `./manage.py make_staff` manage task to set someone +as a staff user, granting them access over the administration functions. + +The base path is `/api/v6/admin`. ## User management @@ -27,17 +34,36 @@ Search users. ## Instance invites +Instance invites are used for instances that do not have open +registrations but want to let some people in regardless. Users +go to the `/invite_register.html` page in the instance and put +their data in. + +### Instance Invite object + +| field | type | description | +| --: | :-- | :-- | +| code | string | instance invite code | +| created\_at | ISO8907 timestamp | when the invite was created | +| max\_uses | integer | maximum amount of uses | +| uses | integer | how many times has the invite been used | + ### `GET /instance/invites` -**TODO** +Get a list of instance invites. ### `PUT /instance/invites` -**TODO** +Create an instance invite. Receives only the `max_uses` +field from the instance invites object. Returns a full +instance invite object. ### `DELETE /instance/invites/` -**TODO** +Delete an invite. Does not have any input, only the instance invite's `code` +as the `` parameter in the URL. + +Returns empty body 204 on success, 404 on invite not found. ## Voice diff --git a/litecord/admin_schemas.py b/litecord/admin_schemas.py index 8f2a001..d4dba63 100644 --- a/litecord/admin_schemas.py +++ b/litecord/admin_schemas.py @@ -46,3 +46,7 @@ USER_CREATE = { 'email': {'type': 'email', 'required': True}, 'password': {'type': 'string', 'minlength': 5, 'required': True}, } + +INSTANCE_INVITE = { + 'max_uses': {'type': 'integer', 'required': True} +} diff --git a/litecord/blueprints/admin_api/instance_invites.py b/litecord/blueprints/admin_api/instance_invites.py index 70c6106..87279c1 100644 --- a/litecord/blueprints/admin_api/instance_invites.py +++ b/litecord/blueprints/admin_api/instance_invites.py @@ -17,11 +17,40 @@ along with this program. If not, see . """ -from quart import Blueprint, jsonify, current_app as app +import string +from random import choice + +from quart import Blueprint, jsonify, current_app as app, request from litecord.auth import admin_check +from litecord.types import timestamp_ +from litecord.schemas import validate +from litecord.admin_schemas import INSTANCE_INVITE bp = Blueprint('instance_invites', __name__) +ALPHABET = string.ascii_lowercase + string.ascii_uppercase + string.digits + + +async def _gen_inv() -> str: + """Generate an invite code""" + return ''.join(choice(ALPHABET) for _ in range(6)) + + +async def gen_inv(ctx) -> str: + """Generate an invite.""" + for _ in range(10): + possible_inv = await _gen_inv() + + created_at = await ctx.db.fetchval(""" + SELECT created_at + FROM instance_invites + WHERE code = $1 + """, possible_inv) + + if created_at is None: + return possible_inv + + return None @bp.route('', methods=['GET']) @@ -33,4 +62,48 @@ async def _all_instance_invites(): FROM instance_invites """) - return jsonify([dict(row) for row in rows]) + rows = [dict(row) for row in rows] + + for row in rows: + row['timestamp'] = timestamp_(row['timestamp']) + + return jsonify(rows) + + +@bp.route('', methods=['PUT']) +async def _create_invite(): + await admin_check() + + code = await gen_inv(app) + if not code: + return 'failed to make invite', 500 + + j = validate(await request.get_json(), INSTANCE_INVITE) + + await app.db.execute(""" + INSERT INTO instance_invites (code, max_uses) + VALUES ($1, $2) + """, code, j['max_uses']) + + inv = dict(await app.db.fetchrow(""" + SELECT code, created_at, uses, max_uses + FROM instance_invites + WHERE code = $1 + """, code)) + + return jsonify(dict(inv)) + + +@bp.route('/', methods=['DELETE']) +async def _del_invite(invite: str): + await admin_check() + + res = await app.db.execute(""" + DELETE FROM instance_invites + WHERE code = $1 + """, invite) + + if res.lower() == 'update 0': + return 'invite not found', 404 + + return '', 204 From ddb56891d1802bff3d59758fd6e88778ffb7d77f Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 16 Mar 2019 17:38:22 -0300 Subject: [PATCH 04/10] add tests.test_admin_api.test_instance_invites - admin_api.instance_invites: fixes --- .../blueprints/admin_api/instance_invites.py | 6 +- tests/test_admin_api/test_instance_invites.py | 87 +++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 tests/test_admin_api/test_instance_invites.py diff --git a/litecord/blueprints/admin_api/instance_invites.py b/litecord/blueprints/admin_api/instance_invites.py index 87279c1..dbd6c78 100644 --- a/litecord/blueprints/admin_api/instance_invites.py +++ b/litecord/blueprints/admin_api/instance_invites.py @@ -65,7 +65,7 @@ async def _all_instance_invites(): rows = [dict(row) for row in rows] for row in rows: - row['timestamp'] = timestamp_(row['timestamp']) + row['created_at'] = timestamp_(row['created_at']) return jsonify(rows) @@ -75,7 +75,7 @@ async def _create_invite(): await admin_check() code = await gen_inv(app) - if not code: + if code is None: return 'failed to make invite', 500 j = validate(await request.get_json(), INSTANCE_INVITE) @@ -103,7 +103,7 @@ async def _del_invite(invite: str): WHERE code = $1 """, invite) - if res.lower() == 'update 0': + if res.lower() == 'delete 0': return 'invite not found', 404 return '', 204 diff --git a/tests/test_admin_api/test_instance_invites.py b/tests/test_admin_api/test_instance_invites.py new file mode 100644 index 0000000..aef9fb8 --- /dev/null +++ b/tests/test_admin_api/test_instance_invites.py @@ -0,0 +1,87 @@ +""" + +Litecord +Copyright (C) 2018-2019 Luna Mendes + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +""" + +import pytest + +from tests.common import login + +async def _get_invs(test_cli, token=None): + if token is None: + token = await login('admin', test_cli) + + resp = await test_cli.get('/api/v6/admin/instance/invites', headers={ + 'Authorization': token + }) + + assert resp.status_code == 200 + rjson = await resp.json + assert isinstance(rjson, list) + return rjson + + +@pytest.mark.asyncio +async def test_get_invites(test_cli): + """Test the listing of instance invites.""" + await _get_invs(test_cli) + + +@pytest.mark.asyncio +async def test_inv_delete_invalid(test_cli): + """Test errors happen when trying to delete a + non-existing instance invite.""" + token = await login('admin', test_cli) + resp = await test_cli.delete( + '/api/v6/admin/instance/invites/aaaaaaaaaa', + headers={ + 'Authorization': token + } + ) + + assert resp.status_code == 404 + + +@pytest.mark.asyncio +async def test_create_invite(test_cli): + """Test the creation of an instance invite, then listing it, + then deleting it.""" + token = await login('admin', test_cli) + resp = await test_cli.put('/api/v6/admin/instance/invites', headers={ + 'Authorization': token + }, json={ + 'max_uses': 1 + }) + + assert resp.status_code == 200 + rjson = await resp.json + assert isinstance(rjson, dict) + code = rjson['code'] + + # assert that the invite is in the list + invites = await _get_invs(test_cli, token) + assert any(inv['code'] == code for inv in invites) + + # delete it, and assert it worked + resp = await test_cli.delete( + f'/api/v6/admin/instance/invites/{code}', + headers={ + 'Authorization': token + } + ) + + assert resp.status_code == 204 From e60b396e19eab6379e4fc8893562c8d672ec3f49 Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 16 Mar 2019 18:18:02 -0300 Subject: [PATCH 05/10] admin_api.users: add user search endpoint --- docs/admin_api.md | 9 ++++- litecord/blueprints/admin_api/users.py | 55 ++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/docs/admin_api.md b/docs/admin_api.md index 2a5f7c4..ff17d9f 100644 --- a/docs/admin_api.md +++ b/docs/admin_api.md @@ -24,9 +24,14 @@ Returns a user object. ### `GET /users` -Search users. +Search users. Input is query arguments with the search parameters. +Returns a list of users -**TODO: query args** +| field | type | description | +| --: | :-- | :-- | +| username | string | username | +| discriminator | string | discriminator | +| page | integer | page | ### `DELETE /users/` diff --git a/litecord/blueprints/admin_api/users.py b/litecord/blueprints/admin_api/users.py index 67690c8..35374f7 100644 --- a/litecord/blueprints/admin_api/users.py +++ b/litecord/blueprints/admin_api/users.py @@ -23,6 +23,7 @@ from litecord.auth import admin_check from litecord.blueprints.auth import create_user from litecord.schemas import validate from litecord.admin_schemas import USER_CREATE +from litecord.errors import BadRequest bp = Blueprint('users_admin', __name__) @@ -38,3 +39,57 @@ async def _create_user(): await app.storage.get_user(user_id) ) + +def args_try(args: dict, typ, field: str, default): + """Try to fetch a value from the request arguments, + given a type.""" + try: + return typ(args.get(field, default)) + except (TypeError, ValueError): + raise BadRequest(f'invalid {field} value') + + +@bp.route('', methods=['GET']) +async def _search_users(): + await admin_check() + + args = request.args + + username, discrim = args.get('username'), args.get('discriminator') + + per_page = args_try(args, int, 'per_page', 20) + page = args_try(args, int, 'page', 0) + + if page < 0: + raise BadRequest('invalid page number') + + if per_page > 50: + raise BadRequest('invalid per page number') + + # any of those must be available. + if not any((username, discrim)): + raise BadRequest('must insert username or discrim') + + wheres, args = [], [] + + if username: + wheres.append("username LIKE '%' || $2 || '%'") + args.append(username) + + if discrim: + wheres.append(f'discriminator = ${len(args) + 2}') + args.append(discrim) + + where_tot = 'WHERE ' if args else '' + where_tot += ' AND '.join(wheres) + + rows = await app.db.fetch(f""" + SELECT id + FROM users + {where_tot} + ORDER BY id ASC + LIMIT {per_page} + OFFSET ($1 * {per_page}) + """, page, *args) + + return jsonify([dict(r) for r in rows]) From efebd21cfb9091000b53e8e89d4810a3d45d525a Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 16 Mar 2019 18:23:37 -0300 Subject: [PATCH 06/10] docs/admin_api.md: add per_page arg - admin_api.users: properly return user objects --- docs/admin_api.md | 5 +++-- litecord/blueprints/admin_api/users.py | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/admin_api.md b/docs/admin_api.md index ff17d9f..53833e3 100644 --- a/docs/admin_api.md +++ b/docs/admin_api.md @@ -25,13 +25,14 @@ Returns a user object. ### `GET /users` Search users. Input is query arguments with the search parameters. -Returns a list of users +Returns a list of user objects. | field | type | description | | --: | :-- | :-- | | username | string | username | | discriminator | string | discriminator | -| page | integer | page | +| page | Optional[integer] | page, default 0 | +| per\_page | Optional[integer] | users per page, default 20, max 50 | ### `DELETE /users/` diff --git a/litecord/blueprints/admin_api/users.py b/litecord/blueprints/admin_api/users.py index 35374f7..713e065 100644 --- a/litecord/blueprints/admin_api/users.py +++ b/litecord/blueprints/admin_api/users.py @@ -24,6 +24,7 @@ from litecord.blueprints.auth import create_user from litecord.schemas import validate from litecord.admin_schemas import USER_CREATE from litecord.errors import BadRequest +from litecord.utils import async_map bp = Blueprint('users_admin', __name__) @@ -92,4 +93,8 @@ async def _search_users(): OFFSET ($1 * {per_page}) """, page, *args) - return jsonify([dict(r) for r in rows]) + rows = [r['id'] for r in rows] + + return jsonify( + await async_map(app.storage.get_user, rows) + ) From 700e590e8b6e7b469c88a47ce8ccbb00054f8971 Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 16 Mar 2019 18:44:02 -0300 Subject: [PATCH 07/10] admin_api.users: add user deletion - users: expose _force_disconnect as user_disconnect --- docs/admin_api.md | 13 ++++++++++++- litecord/blueprints/admin_api/users.py | 18 ++++++++++++++++++ litecord/blueprints/users.py | 5 +++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/admin_api.md b/docs/admin_api.md index 53833e3..d19658f 100644 --- a/docs/admin_api.md +++ b/docs/admin_api.md @@ -36,7 +36,18 @@ Returns a list of user objects. ### `DELETE /users/` -**TODO** +Delete a single user. Does not *actually* remove the user from the users row, +it changes the username to `Deleted User `, etc. + +Also disconnects all of the users' devices from the gateway. + +Output: + +| field | type | description | +| --: | :-- | :-- | +| old | user object | old user object pre-delete | +| new | user object | new user object post-delete | + ## Instance invites diff --git a/litecord/blueprints/admin_api/users.py b/litecord/blueprints/admin_api/users.py index 713e065..cc60a69 100644 --- a/litecord/blueprints/admin_api/users.py +++ b/litecord/blueprints/admin_api/users.py @@ -25,6 +25,7 @@ from litecord.schemas import validate from litecord.admin_schemas import USER_CREATE from litecord.errors import BadRequest from litecord.utils import async_map +from litecord.blueprints.users import delete_user, user_disconnect bp = Blueprint('users_admin', __name__) @@ -98,3 +99,20 @@ async def _search_users(): return jsonify( await async_map(app.storage.get_user, rows) ) + + +@bp.route('/', methods=['DELETE']) +async def _delete_single_user(user_id: int): + await admin_check() + + old_user = await app.storage.get_user(user_id) + + await delete_user(user_id) + await user_disconnect(user_id) + + new_user = await app.storage.get_user(user_id) + + return jsonify({ + 'old': old_user, + 'new': new_user + }) diff --git a/litecord/blueprints/users.py b/litecord/blueprints/users.py index 1145b8d..3789eda 100644 --- a/litecord/blueprints/users.py +++ b/litecord/blueprints/users.py @@ -545,7 +545,8 @@ async def delete_user(user_id, *, db=None): await _del_from_table(db, 'channel_overwrites', user_id) -async def _force_disconnect(user_id): +async def user_disconnect(user_id): + """Disconnects the given user's devices.""" # after removing the user from all tables, we need to force # all known user states to reconnect, causing the user to not # be online anymore. @@ -597,6 +598,6 @@ async def delete_account(): raise Unauthorized('password does not match') await delete_user(user_id) - await _force_disconnect(user_id) + await user_disconnect(user_id) return '', 204 From 2dda7c1bf914744969743e7aa25a386460199200 Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 16 Mar 2019 19:17:12 -0300 Subject: [PATCH 08/10] add tests.test_admin_api.test_users --- litecord/blueprints/admin_api/users.py | 2 +- tests/test_admin_api/test_users.py | 37 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tests/test_admin_api/test_users.py diff --git a/litecord/blueprints/admin_api/users.py b/litecord/blueprints/admin_api/users.py index cc60a69..ee8843d 100644 --- a/litecord/blueprints/admin_api/users.py +++ b/litecord/blueprints/admin_api/users.py @@ -101,7 +101,7 @@ async def _search_users(): ) -@bp.route('/', methods=['DELETE']) +@bp.route('/', methods=['DELETE']) async def _delete_single_user(user_id: int): await admin_check() diff --git a/tests/test_admin_api/test_users.py b/tests/test_admin_api/test_users.py new file mode 100644 index 0000000..dd300f5 --- /dev/null +++ b/tests/test_admin_api/test_users.py @@ -0,0 +1,37 @@ +""" + +Litecord +Copyright (C) 2018-2019 Luna Mendes + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +""" + +import pytest + +from tests.common import login + +@pytest.mark.asyncio +async def test_list_users(test_cli): + """Try to list as many users as possible.""" + token = await login('admin', test_cli) + + # NOTE: replace here if admin username changes + resp = await test_cli.get('/api/v6/admin/users?username=big_girl', headers={ + 'Authorization': token + }) + + assert resp.status_code == 200 + rjson = await resp.json + assert isinstance(rjson, list) + assert rjson From 169d22ad72651f7a65909719ef7f7cf216dcdd63 Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 16 Mar 2019 20:31:28 -0300 Subject: [PATCH 09/10] test_admin_api.test_users: add impl for test_create_delete - admin_api.users: fix bug --- litecord/blueprints/admin_api/users.py | 2 +- tests/test_admin_api/test_users.py | 64 ++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/litecord/blueprints/admin_api/users.py b/litecord/blueprints/admin_api/users.py index ee8843d..6f309d6 100644 --- a/litecord/blueprints/admin_api/users.py +++ b/litecord/blueprints/admin_api/users.py @@ -35,7 +35,7 @@ async def _create_user(): j = validate(await request.get_json(), USER_CREATE) - user_id = await create_user(j['username'], j['email'], j['password']) + user_id, _ = await create_user(j['username'], j['email'], j['password']) return jsonify( await app.storage.get_user(user_id) diff --git a/tests/test_admin_api/test_users.py b/tests/test_admin_api/test_users.py index dd300f5..3ad51f7 100644 --- a/tests/test_admin_api/test_users.py +++ b/tests/test_admin_api/test_users.py @@ -17,21 +17,75 @@ along with this program. If not, see . """ +import secrets + import pytest from tests.common import login +async def _search(test_cli, *, username='', discrim='', token=None): + if token is None: + token = await login('admin', test_cli) + + query_string = { + 'username': username, + 'discriminator': discrim + } + + return await test_cli.get('/api/v6/admin/users', headers={ + 'Authorization': token + }, query_string=query_string) + + @pytest.mark.asyncio async def test_list_users(test_cli): """Try to list as many users as possible.""" - token = await login('admin', test_cli) - # NOTE: replace here if admin username changes - resp = await test_cli.get('/api/v6/admin/users?username=big_girl', headers={ - 'Authorization': token - }) + resp = await _search(test_cli, username='big_girl') assert resp.status_code == 200 rjson = await resp.json assert isinstance(rjson, list) assert rjson + + +@pytest.mark.asyncio +async def test_create_delete(test_cli): + """Create a user. Then delete them.""" + token = await login('admin', test_cli) + + genned = secrets.token_hex(7) + + resp = await test_cli.post('/api/v6/admin/users', headers={ + 'Authorization': token + }, json={ + 'username': genned, + 'email': f'{genned}@{genned}.com', + 'password': genned, + }) + + assert resp.status_code == 200 + rjson = await resp.json + assert isinstance(rjson, dict) + assert rjson['username'] == genned + + genned_uid = rjson['id'] + + # check if side-effects went through with a search + resp = await _search(test_cli, username=genned, token=token) + + assert resp.status_code == 200 + rjson = await resp.json + assert isinstance(rjson, list) + assert rjson[0]['id'] == genned_uid + + # delete + resp = await test_cli.delete(f'/api/v6/admin/users/{genned_uid}', headers={ + 'Authorization': token + }) + + assert resp.status_code == 200 + rjson = await resp.json + assert isinstance(rjson, dict) + assert rjson['new']['id'] == genned_uid + assert rjson['old']['id'] == rjson['new']['id'] From cc379df861b57550986a7feb75f7e3faee21174d Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 16 Mar 2019 21:13:10 -0300 Subject: [PATCH 10/10] tests.conftest: add teardown to prevent overuse of resources --- tests/conftest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 66c5e6f..5917a75 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,7 +45,11 @@ def _test_app(unused_tcp_port, event_loop): # make sure we're calling the before_serving hooks event_loop.run_until_complete(main_app.startup()) - return main_app + # https://docs.pytest.org/en/latest/fixture.html#fixture-finalization-executing-teardown-code + yield main_app + + # properly teardown + event_loop.run_until_complete(main_app.shutdown()) @pytest.fixture(name='test_cli')