mirror of https://gitlab.com/litecord/litecord.git
Merge branch 'admin-api-more-endpoints' into 'master'
Admin API improvements Closes #44 See merge request litecord/litecord!33
This commit is contained in:
commit
5dbf8ac8fd
|
|
@ -48,6 +48,26 @@ Output:
|
||||||
| old | user object | old user object pre-delete |
|
| old | user object | old user object pre-delete |
|
||||||
| new | user object | new user object post-delete |
|
| new | user object | new user object post-delete |
|
||||||
|
|
||||||
|
### PATCH `/users/<user_id>`
|
||||||
|
|
||||||
|
Update a single user's information.
|
||||||
|
|
||||||
|
Returns a user object on success.
|
||||||
|
|
||||||
|
**Note:** You can not change any user's staff badge state (neither adding
|
||||||
|
it or removing it) to not cause privilege escalation/de-escalation (where
|
||||||
|
a staff makes more staff or a staff removes staff privileges of someone else).
|
||||||
|
Keep in mind the staff badge is what grants access to the Admin API, so.
|
||||||
|
|
||||||
|
**Note:** Changing a user's nitro badge is not defined via the flags.
|
||||||
|
Plus that would require adding an interface to user payments
|
||||||
|
through the Admin API.
|
||||||
|
|
||||||
|
[UserFlags]: https://discordapp.com/developers/docs/resources/user#user-object-user-flags
|
||||||
|
|
||||||
|
| field | type | description |
|
||||||
|
| --: | :-- | :-- |
|
||||||
|
| flags | [UserFlags] | user flags/badges |
|
||||||
|
|
||||||
## Instance invites
|
## Instance invites
|
||||||
|
|
||||||
|
|
@ -130,7 +150,22 @@ Returns empty body with 204 status code on success.
|
||||||
|
|
||||||
### GET `/guilds/<guild_id>`
|
### GET `/guilds/<guild_id>`
|
||||||
|
|
||||||
Returns a partial guild object.
|
Returns a partial guild object. Gives a 404 when the guild is not found.
|
||||||
|
|
||||||
|
### PATCH `/guilds/<guild_id>`
|
||||||
|
|
||||||
|
Update a single guild.
|
||||||
|
Dispatches `GUILD_UPDATE` to subscribers of the guild.
|
||||||
|
|
||||||
|
Returns a guild object or an unavailable guild object on success.
|
||||||
|
|
||||||
|
| field | type | description |
|
||||||
|
| --: | :-- | :-- |
|
||||||
|
| unavailable | bool | if the guild is unavailable |
|
||||||
|
|
||||||
|
### DELETE `/guilds/<guild_id>`
|
||||||
|
|
||||||
|
Delete a single guild. Returns 204 on success.
|
||||||
|
|
||||||
## Guild features
|
## Guild features
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from litecord.enums import Feature
|
from litecord.enums import Feature, UserFlags
|
||||||
|
|
||||||
VOICE_SERVER = {
|
VOICE_SERVER = {
|
||||||
'hostname': {'type': 'string', 'maxlength': 255, 'required': True}
|
'hostname': {'type': 'string', 'maxlength': 255, 'required': True}
|
||||||
|
|
@ -50,3 +50,11 @@ USER_CREATE = {
|
||||||
INSTANCE_INVITE = {
|
INSTANCE_INVITE = {
|
||||||
'max_uses': {'type': 'integer', 'required': True}
|
'max_uses': {'type': 'integer', 'required': True}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GUILD_UPDATE = {
|
||||||
|
'unavailable': {'type': 'boolean', 'required': False}
|
||||||
|
}
|
||||||
|
|
||||||
|
USER_UPDATE = {
|
||||||
|
'flags': {'required': False, 'coerce': UserFlags.from_int}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from quart import Blueprint, jsonify, current_app as app
|
from quart import Blueprint, jsonify, current_app as app, request
|
||||||
|
|
||||||
from litecord.auth import admin_check
|
from litecord.auth import admin_check
|
||||||
|
from litecord.schemas import validate
|
||||||
|
from litecord.admin_schemas import GUILD_UPDATE
|
||||||
|
from litecord.blueprints.guilds import delete_guild
|
||||||
|
from litecord.errors import GuildNotFound
|
||||||
|
|
||||||
bp = Blueprint('guilds_admin', __name__)
|
bp = Blueprint('guilds_admin', __name__)
|
||||||
|
|
||||||
|
|
@ -28,6 +32,47 @@ async def get_guild(guild_id: int):
|
||||||
"""Get a basic guild payload."""
|
"""Get a basic guild payload."""
|
||||||
await admin_check()
|
await admin_check()
|
||||||
|
|
||||||
return jsonify(
|
guild = await app.storage.get_guild(guild_id)
|
||||||
await app.storage.get_guild(guild_id)
|
|
||||||
)
|
if not guild:
|
||||||
|
raise GuildNotFound()
|
||||||
|
|
||||||
|
return jsonify(guild)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:guild_id>', methods=['PATCH'])
|
||||||
|
async def update_guild(guild_id: int):
|
||||||
|
await admin_check()
|
||||||
|
|
||||||
|
j = validate(await request.get_json(), GUILD_UPDATE)
|
||||||
|
|
||||||
|
# TODO: what happens to the other guild attributes when its
|
||||||
|
# unavailable? do they vanish?
|
||||||
|
old_unavailable = app.guild_store.get(guild_id, 'unavailable')
|
||||||
|
new_unavailable = j.get('unavailable', old_unavailable)
|
||||||
|
|
||||||
|
# always set unavailable status since new_unavailable will be
|
||||||
|
# old_unavailable when not provided, so we don't need to check if
|
||||||
|
# j.unavailable is there
|
||||||
|
app.guild_store.set(guild_id, 'unavailable', j['unavailable'])
|
||||||
|
|
||||||
|
guild = await app.storage.get_guild(guild_id)
|
||||||
|
|
||||||
|
# TODO: maybe we can just check guild['unavailable']...?
|
||||||
|
|
||||||
|
if old_unavailable and not new_unavailable:
|
||||||
|
# guild became available
|
||||||
|
await app.dispatcher.dispatch_guild(guild_id, 'GUILD_CREATE', guild)
|
||||||
|
else:
|
||||||
|
# guild became unavailable
|
||||||
|
await app.dispatcher.dispatch_guild(guild_id, 'GUILD_DELETE', guild)
|
||||||
|
|
||||||
|
return jsonify(guild)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:guild_id>', methods=['DELETE'])
|
||||||
|
async def delete_guild_as_admin(guild_id):
|
||||||
|
"""Delete a single guild via the admin API without ownership checks."""
|
||||||
|
await admin_check()
|
||||||
|
await delete_guild(guild_id)
|
||||||
|
return '', 204
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,13 @@ from quart import Blueprint, jsonify, current_app as app, request
|
||||||
from litecord.auth import admin_check
|
from litecord.auth import admin_check
|
||||||
from litecord.blueprints.auth import create_user
|
from litecord.blueprints.auth import create_user
|
||||||
from litecord.schemas import validate
|
from litecord.schemas import validate
|
||||||
from litecord.admin_schemas import USER_CREATE
|
from litecord.admin_schemas import USER_CREATE, USER_UPDATE
|
||||||
from litecord.errors import BadRequest
|
from litecord.errors import BadRequest, Forbidden
|
||||||
from litecord.utils import async_map
|
from litecord.utils import async_map
|
||||||
from litecord.blueprints.users import delete_user, user_disconnect
|
from litecord.blueprints.users import (
|
||||||
|
delete_user, user_disconnect, mass_user_update
|
||||||
|
)
|
||||||
|
from litecord.enums import UserFlags
|
||||||
|
|
||||||
bp = Blueprint('users_admin', __name__)
|
bp = Blueprint('users_admin', __name__)
|
||||||
|
|
||||||
|
|
@ -116,3 +119,31 @@ async def _delete_single_user(user_id: int):
|
||||||
'old': old_user,
|
'old': old_user,
|
||||||
'new': new_user
|
'new': new_user
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:user_id>', methods=['PATCH'])
|
||||||
|
async def patch_user(user_id: int):
|
||||||
|
await admin_check()
|
||||||
|
|
||||||
|
j = validate(await request.get_json(), USER_UPDATE)
|
||||||
|
|
||||||
|
# get the original user for flags checking
|
||||||
|
user = await app.storage.get_user(user_id)
|
||||||
|
old_flags = UserFlags.from_int(user['flags'])
|
||||||
|
|
||||||
|
# j.flags is already a UserFlags since we coerce it.
|
||||||
|
if 'flags' in j:
|
||||||
|
new_flags = j['flags']
|
||||||
|
|
||||||
|
# disallow any changes to the staff badge
|
||||||
|
if new_flags.is_staff != old_flags.is_staff:
|
||||||
|
raise Forbidden('you can not change a users staff badge')
|
||||||
|
|
||||||
|
await app.db.execute("""
|
||||||
|
UPDATE users
|
||||||
|
SET flags = $1
|
||||||
|
WHERE id = $2
|
||||||
|
""", new_flags.value, user_id)
|
||||||
|
|
||||||
|
public_user, _ = await mass_user_update(user_id, app)
|
||||||
|
return jsonify(public_user)
|
||||||
|
|
|
||||||
|
|
@ -350,21 +350,17 @@ async def _update_guild(guild_id):
|
||||||
return jsonify(guild)
|
return jsonify(guild)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<int:guild_id>', methods=['DELETE'])
|
async def delete_guild(guild_id: int, *, app_=None):
|
||||||
# this endpoint is not documented, but used by the official client.
|
"""Delete a single guild."""
|
||||||
@bp.route('/<int:guild_id>/delete', methods=['POST'])
|
app_ = app_ or app
|
||||||
async def delete_guild(guild_id):
|
|
||||||
"""Delete a guild."""
|
|
||||||
user_id = await token_check()
|
|
||||||
await guild_owner_check(user_id, guild_id)
|
|
||||||
|
|
||||||
await app.db.execute("""
|
await app_.db.execute("""
|
||||||
DELETE FROM guilds
|
DELETE FROM guilds
|
||||||
WHERE guilds.id = $1
|
WHERE guilds.id = $1
|
||||||
""", guild_id)
|
""", guild_id)
|
||||||
|
|
||||||
# Discord's client expects IDs being string
|
# Discord's client expects IDs being string
|
||||||
await app.dispatcher.dispatch('guild', guild_id, 'GUILD_DELETE', {
|
await app_.dispatcher.dispatch('guild', guild_id, 'GUILD_DELETE', {
|
||||||
'guild_id': str(guild_id),
|
'guild_id': str(guild_id),
|
||||||
'id': str(guild_id),
|
'id': str(guild_id),
|
||||||
# 'unavailable': False,
|
# 'unavailable': False,
|
||||||
|
|
@ -373,8 +369,17 @@ async def delete_guild(guild_id):
|
||||||
# remove from the dispatcher so nobody
|
# remove from the dispatcher so nobody
|
||||||
# becomes the little memer that tries to fuck up with
|
# becomes the little memer that tries to fuck up with
|
||||||
# everybody's gateway
|
# everybody's gateway
|
||||||
await app.dispatcher.remove('guild', guild_id)
|
await app_.dispatcher.remove('guild', guild_id)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:guild_id>', methods=['DELETE'])
|
||||||
|
# this endpoint is not documented, but used by the official client.
|
||||||
|
@bp.route('/<int:guild_id>/delete', methods=['POST'])
|
||||||
|
async def delete_guild_handler(guild_id):
|
||||||
|
"""Delete a guild."""
|
||||||
|
user_id = await token_check()
|
||||||
|
await guild_owner_check(user_id, guild_id)
|
||||||
|
await delete_guild(guild_id)
|
||||||
return '', 204
|
return '', 204
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ async def mass_user_update(user_id, app_=None):
|
||||||
'lazy_guild', guild_ids, 'update_user', user_id
|
'lazy_guild', guild_ids, 'update_user', user_id
|
||||||
)
|
)
|
||||||
|
|
||||||
return private_user
|
return public_user, private_user
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/@me', methods=['GET'])
|
@bp.route('/@me', methods=['GET'])
|
||||||
|
|
@ -257,7 +257,7 @@ async def patch_me():
|
||||||
|
|
||||||
user.pop('password_hash')
|
user.pop('password_hash')
|
||||||
|
|
||||||
private_user = await mass_user_update(user_id, app)
|
_, private_user = await mass_user_update(user_id, app)
|
||||||
return jsonify(private_user)
|
return jsonify(private_user)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ class Flags:
|
||||||
def _make_int(value):
|
def _make_int(value):
|
||||||
res = Flags()
|
res = Flags()
|
||||||
|
|
||||||
|
setattr(res, 'value', value)
|
||||||
|
|
||||||
for attr, val in attrs:
|
for attr, val in attrs:
|
||||||
# get only the ones that represent a field in the
|
# get only the ones that represent a field in the
|
||||||
# number's bits
|
# number's bits
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
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/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
class GuildMemoryStore:
|
||||||
|
"""Store in-memory properties about guilds.
|
||||||
|
|
||||||
|
I could have just used Redis... probably too overkill to add
|
||||||
|
aioredis to the already long depedency list, plus, I don't need
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._store = {}
|
||||||
|
|
||||||
|
def get(self, guild_id: int, attribute: str, default=None):
|
||||||
|
"""get a key"""
|
||||||
|
return self._store.get(f'{guild_id}:{attribute}', default)
|
||||||
|
|
||||||
|
def set(self, guild_id: int, attribute: str, value):
|
||||||
|
"""set a key"""
|
||||||
|
self._store[f'{guild_id}:{attribute}'] = value
|
||||||
|
|
@ -180,6 +180,18 @@ class Storage:
|
||||||
|
|
||||||
drow = dict(row)
|
drow = dict(row)
|
||||||
|
|
||||||
|
# a guild's unavailable state is kept in memory, and we remove every
|
||||||
|
# other guild related field when its unavailable.
|
||||||
|
drow['unavailable'] = self.app.guild_store.get(
|
||||||
|
guild_id, 'unavailable', False)
|
||||||
|
|
||||||
|
if drow['unavailable']:
|
||||||
|
drow = {
|
||||||
|
'id': drow['id'],
|
||||||
|
'unavailable': True
|
||||||
|
}
|
||||||
|
|
||||||
|
# guild.owner is dependant of the user doing the get_guild call.
|
||||||
if user_id:
|
if user_id:
|
||||||
drow['owner'] = drow['owner_id'] == str(user_id)
|
drow['owner'] = drow['owner_id'] == str(user_id)
|
||||||
|
|
||||||
|
|
@ -663,6 +675,9 @@ class Storage:
|
||||||
if guild is None:
|
if guild is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if guild['unavailable']:
|
||||||
|
return guild
|
||||||
|
|
||||||
extra = await self.get_guild_extra(guild_id, user_id, large_count)
|
extra = await self.get_guild_extra(guild_id, user_id, large_count)
|
||||||
|
|
||||||
return {**guild, **extra}
|
return {**guild, **extra}
|
||||||
|
|
|
||||||
2
run.py
2
run.py
|
|
@ -75,6 +75,7 @@ from litecord.presence import PresenceManager
|
||||||
from litecord.images import IconManager
|
from litecord.images import IconManager
|
||||||
from litecord.jobs import JobManager
|
from litecord.jobs import JobManager
|
||||||
from litecord.voice.manager import VoiceManager
|
from litecord.voice.manager import VoiceManager
|
||||||
|
from litecord.guild_memory_store import GuildMemoryStore
|
||||||
|
|
||||||
from litecord.gateway.gateway import websocket_handler
|
from litecord.gateway.gateway import websocket_handler
|
||||||
|
|
||||||
|
|
@ -246,6 +247,7 @@ def init_app_managers(app_):
|
||||||
app_.storage.presence = app_.presence
|
app_.storage.presence = app_.presence
|
||||||
|
|
||||||
app_.voice = VoiceManager(app_)
|
app_.voice = VoiceManager(app_)
|
||||||
|
app_.guild_store = GuildMemoryStore()
|
||||||
|
|
||||||
|
|
||||||
async def api_index(app_):
|
async def api_index(app_):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
from litecord.blueprints.guilds import delete_guild
|
||||||
|
from litecord.errors import GuildNotFound
|
||||||
|
|
||||||
|
async def _create_guild(test_cli, *, token=None):
|
||||||
|
token = token or await login('admin', test_cli)
|
||||||
|
|
||||||
|
genned_name = secrets.token_hex(6)
|
||||||
|
|
||||||
|
resp = await test_cli.post('/api/v6/guilds', headers={
|
||||||
|
'Authorization': token
|
||||||
|
}, json={
|
||||||
|
'name': genned_name,
|
||||||
|
'region': None
|
||||||
|
})
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
rjson = await resp.json
|
||||||
|
assert isinstance(rjson, dict)
|
||||||
|
assert rjson['name'] == genned_name
|
||||||
|
|
||||||
|
return rjson
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_guild(test_cli, guild_id, *, token=None, ret_early=False):
|
||||||
|
token = token or await login('admin', test_cli)
|
||||||
|
|
||||||
|
resp = await test_cli.get(f'/api/v6/admin/guilds/{guild_id}', headers={
|
||||||
|
'Authorization': token
|
||||||
|
})
|
||||||
|
|
||||||
|
if ret_early:
|
||||||
|
return resp
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
rjson = await resp.json
|
||||||
|
assert isinstance(rjson, dict)
|
||||||
|
assert rjson['id'] == guild_id
|
||||||
|
|
||||||
|
return rjson
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_guild_fetch(test_cli):
|
||||||
|
"""Test the creation and fetching of a guild via the Admin API."""
|
||||||
|
token = await login('admin', test_cli)
|
||||||
|
rjson = await _create_guild(test_cli, token=token)
|
||||||
|
guild_id = rjson['id']
|
||||||
|
|
||||||
|
try:
|
||||||
|
await _fetch_guild(test_cli, guild_id)
|
||||||
|
finally:
|
||||||
|
await delete_guild(int(guild_id), app_=test_cli.app)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_guild_update(test_cli):
|
||||||
|
"""Test the update of a guild via the Admin API."""
|
||||||
|
token = await login('admin', test_cli)
|
||||||
|
rjson = await _create_guild(test_cli, token=token)
|
||||||
|
guild_id = rjson['id']
|
||||||
|
assert not rjson['unavailable']
|
||||||
|
|
||||||
|
try:
|
||||||
|
# I believe setting up an entire gateway client registered to the guild
|
||||||
|
# would be overkill to test the side-effects, so... I'm not
|
||||||
|
# testing them. Yes, I know its a bad idea, but if someone has an easier
|
||||||
|
# way to write that, do send an MR.
|
||||||
|
resp = await test_cli.patch(
|
||||||
|
f'/api/v6/admin/guilds/{guild_id}',
|
||||||
|
headers={
|
||||||
|
'Authorization': token
|
||||||
|
}, json={
|
||||||
|
'unavailable': True
|
||||||
|
})
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
rjson = await resp.json
|
||||||
|
assert isinstance(rjson, dict)
|
||||||
|
assert rjson['id'] == guild_id
|
||||||
|
assert rjson['unavailable']
|
||||||
|
|
||||||
|
rjson = await _fetch_guild(test_cli, guild_id, token=token)
|
||||||
|
assert rjson['unavailable']
|
||||||
|
finally:
|
||||||
|
await delete_guild(int(guild_id), app_=test_cli.app)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_guild_delete(test_cli):
|
||||||
|
"""Test the update of a guild via the Admin API."""
|
||||||
|
token = await login('admin', test_cli)
|
||||||
|
rjson = await _create_guild(test_cli, token=token)
|
||||||
|
guild_id = rjson['id']
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = await test_cli.delete(
|
||||||
|
f'/api/v6/admin/guilds/{guild_id}',
|
||||||
|
headers={
|
||||||
|
'Authorization': token
|
||||||
|
})
|
||||||
|
|
||||||
|
assert resp.status_code == 204
|
||||||
|
|
||||||
|
resp = await _fetch_guild(test_cli, guild_id, token=token,
|
||||||
|
ret_early=True)
|
||||||
|
|
||||||
|
assert resp.status_code == 404
|
||||||
|
rjson = await resp.json
|
||||||
|
assert isinstance(rjson, dict)
|
||||||
|
assert rjson['error']
|
||||||
|
assert rjson['code'] == GuildNotFound.error_code
|
||||||
|
finally:
|
||||||
|
await delete_guild(int(guild_id), app_=test_cli.app)
|
||||||
|
|
@ -22,10 +22,12 @@ import secrets
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.common import login
|
from tests.common import login
|
||||||
|
from tests.credentials import CREDS
|
||||||
|
from litecord.enums import UserFlags
|
||||||
|
|
||||||
|
|
||||||
async def _search(test_cli, *, username='', discrim='', token=None):
|
async def _search(test_cli, *, username='', discrim='', token=None):
|
||||||
if token is None:
|
token = token or await login('admin', test_cli)
|
||||||
token = await login('admin', test_cli)
|
|
||||||
|
|
||||||
query_string = {
|
query_string = {
|
||||||
'username': username,
|
'username': username,
|
||||||
|
|
@ -40,8 +42,7 @@ async def _search(test_cli, *, username='', discrim='', token=None):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_list_users(test_cli):
|
async def test_list_users(test_cli):
|
||||||
"""Try to list as many users as possible."""
|
"""Try to list as many users as possible."""
|
||||||
# NOTE: replace here if admin username changes
|
resp = await _search(test_cli, username=CREDS['admin']['username'])
|
||||||
resp = await _search(test_cli, username='big_girl')
|
|
||||||
|
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
rjson = await resp.json
|
rjson = await resp.json
|
||||||
|
|
@ -49,11 +50,8 @@ async def test_list_users(test_cli):
|
||||||
assert rjson
|
assert rjson
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
async def _setup_user(test_cli, *, token=None) -> dict:
|
||||||
async def test_create_delete(test_cli):
|
token = token or await login('admin', test_cli)
|
||||||
"""Create a user. Then delete them."""
|
|
||||||
token = await login('admin', test_cli)
|
|
||||||
|
|
||||||
genned = secrets.token_hex(7)
|
genned = secrets.token_hex(7)
|
||||||
|
|
||||||
resp = await test_cli.post('/api/v6/admin/users', headers={
|
resp = await test_cli.post('/api/v6/admin/users', headers={
|
||||||
|
|
@ -69,23 +67,73 @@ async def test_create_delete(test_cli):
|
||||||
assert isinstance(rjson, dict)
|
assert isinstance(rjson, dict)
|
||||||
assert rjson['username'] == genned
|
assert rjson['username'] == genned
|
||||||
|
|
||||||
genned_uid = rjson['id']
|
return rjson
|
||||||
|
|
||||||
# check if side-effects went through with a search
|
|
||||||
resp = await _search(test_cli, username=genned, token=token)
|
|
||||||
|
|
||||||
assert resp.status_code == 200
|
async def _del_user(test_cli, user_id, *, token=None):
|
||||||
rjson = await resp.json
|
"""Delete a user."""
|
||||||
assert isinstance(rjson, list)
|
token = token or await login('admin', test_cli)
|
||||||
assert rjson[0]['id'] == genned_uid
|
|
||||||
|
|
||||||
# delete
|
resp = await test_cli.delete(f'/api/v6/admin/users/{user_id}', headers={
|
||||||
resp = await test_cli.delete(f'/api/v6/admin/users/{genned_uid}', headers={
|
|
||||||
'Authorization': token
|
'Authorization': token
|
||||||
})
|
})
|
||||||
|
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
rjson = await resp.json
|
rjson = await resp.json
|
||||||
assert isinstance(rjson, dict)
|
assert isinstance(rjson, dict)
|
||||||
assert rjson['new']['id'] == genned_uid
|
assert rjson['new']['id'] == user_id
|
||||||
assert rjson['old']['id'] == rjson['new']['id']
|
assert rjson['old']['id'] == rjson['new']['id']
|
||||||
|
|
||||||
|
# TODO: remove from database at this point? it'll just keep being
|
||||||
|
# filled up every time we run a test..
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_delete(test_cli):
|
||||||
|
"""Create a user. Then delete them."""
|
||||||
|
token = await login('admin', test_cli)
|
||||||
|
|
||||||
|
rjson = await _setup_user(test_cli, token=token)
|
||||||
|
|
||||||
|
genned = rjson['username']
|
||||||
|
genned_uid = rjson['id']
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 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
|
||||||
|
finally:
|
||||||
|
await _del_user(test_cli, genned_uid, token=token)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_user_update(test_cli):
|
||||||
|
"""Test user update."""
|
||||||
|
token = await login('admin', test_cli)
|
||||||
|
rjson = await _setup_user(test_cli, token=token)
|
||||||
|
|
||||||
|
user_id = rjson['id']
|
||||||
|
|
||||||
|
# test update
|
||||||
|
|
||||||
|
try:
|
||||||
|
# set them as partner flag
|
||||||
|
resp = await test_cli.patch(f'/api/v6/admin/users/{user_id}', headers={
|
||||||
|
'Authorization': token
|
||||||
|
}, json={
|
||||||
|
'flags': UserFlags.partner,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
rjson = await resp.json
|
||||||
|
assert rjson['id'] == user_id
|
||||||
|
assert rjson['flags'] == UserFlags.partner
|
||||||
|
|
||||||
|
# TODO: maybe we can check for side effects by fetching the
|
||||||
|
# user manually too...
|
||||||
|
finally:
|
||||||
|
await _del_user(test_cli, user_id, token=token)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue