mirror of https://gitlab.com/litecord/litecord.git
Merge branch 'admin-api-conversion' into 'master'
Admin api conversion Closes #38 See merge request litecord/litecord!26
This commit is contained in:
commit
67acf7e22a
|
|
@ -1,6 +1,86 @@
|
||||||
# Litecord Admin API
|
# 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
|
||||||
|
|
||||||
|
### `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. Input is query arguments with the search parameters.
|
||||||
|
Returns a list of user objects.
|
||||||
|
|
||||||
|
| field | type | description |
|
||||||
|
| --: | :-- | :-- |
|
||||||
|
| username | string | username |
|
||||||
|
| discriminator | string | discriminator |
|
||||||
|
| page | Optional[integer] | page, default 0 |
|
||||||
|
| per\_page | Optional[integer] | users per page, default 20, max 50 |
|
||||||
|
|
||||||
|
### `DELETE /users/<user_id>`
|
||||||
|
|
||||||
|
Delete a single user. Does not *actually* remove the user from the users row,
|
||||||
|
it changes the username to `Deleted User <random hex>`, 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
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
Get a list of instance invites.
|
||||||
|
|
||||||
|
### `PUT /instance/invites`
|
||||||
|
|
||||||
|
Create an instance invite. Receives only the `max_uses`
|
||||||
|
field from the instance invites object. Returns a full
|
||||||
|
instance invite object.
|
||||||
|
|
||||||
|
### `DELETE /instance/invites/<invite>`
|
||||||
|
|
||||||
|
Delete an invite. Does not have any input, only the instance invite's `code`
|
||||||
|
as the `<invite>` parameter in the URL.
|
||||||
|
|
||||||
|
Returns empty body 204 on success, 404 on invite not found.
|
||||||
|
|
||||||
## Voice
|
## Voice
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,13 @@ FEATURES = {
|
||||||
'schema': {'coerce': lambda x: Feature(x)}
|
'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},
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTANCE_INVITE = {
|
||||||
|
'max_uses': {'type': 'integer', 'required': True}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
from .voice import bp as voice
|
from .voice import bp as voice
|
||||||
from .features import bp as features
|
from .features import bp as features
|
||||||
from .guilds import bp as guilds
|
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']
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
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'])
|
||||||
|
async def _all_instance_invites():
|
||||||
|
await admin_check()
|
||||||
|
|
||||||
|
rows = await app.db.fetch("""
|
||||||
|
SELECT code, created_at, uses, max_uses
|
||||||
|
FROM instance_invites
|
||||||
|
""")
|
||||||
|
|
||||||
|
rows = [dict(row) for row in rows]
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
row['created_at'] = timestamp_(row['created_at'])
|
||||||
|
|
||||||
|
return jsonify(rows)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('', methods=['PUT'])
|
||||||
|
async def _create_invite():
|
||||||
|
await admin_check()
|
||||||
|
|
||||||
|
code = await gen_inv(app)
|
||||||
|
if code is None:
|
||||||
|
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('/<invite>', 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() == 'delete 0':
|
||||||
|
return 'invite not found', 404
|
||||||
|
|
||||||
|
return '', 204
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
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__)
|
||||||
|
|
||||||
|
@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)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
rows = [r['id'] for r in rows]
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
await async_map(app.storage.get_user, rows)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:user_id>', 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
|
||||||
|
})
|
||||||
|
|
@ -545,7 +545,8 @@ async def delete_user(user_id, *, db=None):
|
||||||
await _del_from_table(db, 'channel_overwrites', user_id)
|
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
|
# after removing the user from all tables, we need to force
|
||||||
# all known user states to reconnect, causing the user to not
|
# all known user states to reconnect, causing the user to not
|
||||||
# be online anymore.
|
# be online anymore.
|
||||||
|
|
@ -597,6 +598,6 @@ async def delete_account():
|
||||||
raise Unauthorized('password does not match')
|
raise Unauthorized('password does not match')
|
||||||
|
|
||||||
await delete_user(user_id)
|
await delete_user(user_id)
|
||||||
await _force_disconnect(user_id)
|
await user_disconnect(user_id)
|
||||||
|
|
||||||
return '', 204
|
return '', 204
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,5 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .command import setup as migration
|
from .command import setup as migration
|
||||||
|
|
||||||
|
__all__ = ['migration']
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
|
||||||
import itsdangerous
|
|
||||||
import bcrypt
|
|
||||||
from litecord.blueprints.auth import create_user, make_token
|
from litecord.blueprints.auth import create_user, make_token
|
||||||
from litecord.blueprints.users import delete_user
|
from litecord.blueprints.users import delete_user
|
||||||
from litecord.enums import UserFlags
|
from litecord.enums import UserFlags
|
||||||
|
|
|
||||||
6
run.py
6
run.py
|
|
@ -58,7 +58,7 @@ from litecord.blueprints.user.billing_job import payment_job
|
||||||
|
|
||||||
from litecord.blueprints.admin_api import (
|
from litecord.blueprints.admin_api import (
|
||||||
voice as voice_admin, features as features_admin,
|
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
|
from litecord.blueprints.admin_api.voice import guild_region_check
|
||||||
|
|
@ -146,7 +146,9 @@ def set_blueprints(app_):
|
||||||
|
|
||||||
voice_admin: '/admin/voice',
|
voice_admin: '/admin/voice',
|
||||||
features_admin: '/admin/guilds',
|
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():
|
for bp, suffix in bps.items():
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,11 @@ def _test_app(unused_tcp_port, event_loop):
|
||||||
# make sure we're calling the before_serving hooks
|
# make sure we're calling the before_serving hooks
|
||||||
event_loop.run_until_complete(main_app.startup())
|
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')
|
@pytest.fixture(name='test_cli')
|
||||||
|
|
|
||||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
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."""
|
||||||
|
# NOTE: replace here if admin username changes
|
||||||
|
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']
|
||||||
Loading…
Reference in New Issue