From 7b98b2332f6f675131b90e2f245e49764b535055 Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 14:04:59 -0300 Subject: [PATCH 01/35] tests: add test resource awareness --- tests/common.py | 53 +++++++++++++++++++++++++++++++++++++++++++++-- tests/conftest.py | 8 +++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/tests/common.py b/tests/common.py index 4e573a7..ddb4ac2 100644 --- a/tests/common.py +++ b/tests/common.py @@ -18,24 +18,73 @@ along with this program. If not, see . """ import secrets +from typing import Optional +from dataclasses import dataclass + +from litecord.common.users import create_user, delete_user +from litecord.blueprints.auth import make_token def email() -> str: return f"{secrets.token_hex(5)}@{secrets.token_hex(5)}.com" +@dataclass +class WrappedUser: + test_cli: "TestClient" + id: int + name: str + email: str + password: str + token: str + + async def refetch(self): + async with self.test_cli.app.app_context(): + return await self.test_cli.app.storage.get_user(self.id) + + async def delete(self): + async with self.test_cli.app.app_context(): + return await delete_user(self.id) + + class TestClient: - """Test client that wraps pytest-sanic's TestClient and a test - user and adds authorization headers to test requests.""" + """Test client wrapper class. Adds Authorization headers to all requests + and manages test resource setup and destruction.""" def __init__(self, test_cli, test_user): self.cli = test_cli self.app = test_cli.app self.user = test_user + self.resources = [] def __getitem__(self, key): return self.user[key] + def add_resource(self, resource): + self.resources.append(resource) + return resource + + async def cleanup(self): + for resource in self.resources: + await resource.delete() + + async def create_user( + self, + *, + username: str, + email: str, + password: Optional[str] = None, + ) -> WrappedUser: + password = password or secrets.token_hex(6) + + async with self.app.app_context(): + user_id, password_hash = await create_user(username, email, password) + user_token = make_token(user_id, password_hash) + + return self.add_resource( + WrappedUser(self, user_id, username, email, password, user_token) + ) + def _inject_auth(self, kwargs: dict) -> list: """Inject the test user's API key into the test request before passing the request on to the underlying TestClient.""" diff --git a/tests/conftest.py b/tests/conftest.py index f0444c9..57df793 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -107,7 +107,9 @@ async def test_user_fixture(app): async def test_cli_user(test_cli, test_user): """Yield a TestClient instance that contains a randomly generated user.""" - yield TestClient(test_cli, test_user) + client = TestClient(test_cli, test_user) + yield client + await client.cleanup() @pytest.fixture @@ -138,5 +140,7 @@ async def test_cli_staff(test_cli): user_id, ) - yield TestClient(test_cli, test_user) + client = TestClient(test_cli, test_user) + yield client + await client.cleanup() await _user_fixture_teardown(test_cli.app, test_user) From ddf27ace1a6b2ec494f506d761f8b1141321f670 Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 14:05:11 -0300 Subject: [PATCH 02/35] tests: add test_find_single_user with test resources --- tests/test_admin_api/test_users.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_admin_api/test_users.py b/tests/test_admin_api/test_users.py index 8b2e738..b8bbf19 100644 --- a/tests/test_admin_api/test_users.py +++ b/tests/test_admin_api/test_users.py @@ -21,6 +21,7 @@ import secrets import pytest +from tests.common import email from litecord.enums import UserFlags @@ -41,6 +42,20 @@ async def test_list_users(test_cli_staff): assert rjson +@pytest.mark.asyncio +async def test_find_single_user(test_cli_staff): + user = await test_cli_staff.create_user( + username="test_user" + secrets.token_hex(2), email=email() + ) + resp = await _search(test_cli_staff, username=user.name) + + assert resp.status_code == 200 + rjson = await resp.json + assert isinstance(rjson, list) + fetched_user = rjson[0] + assert fetched_user["id"] == str(user.id) + + async def _setup_user(test_cli) -> dict: genned = secrets.token_hex(7) From d01fde15da5ea11eb1bc32685f5e112910899f56 Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 15:50:11 -0300 Subject: [PATCH 03/35] storage: fix typo --- litecord/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litecord/storage.py b/litecord/storage.py index 1b3b9d8..65367dc 100644 --- a/litecord/storage.py +++ b/litecord/storage.py @@ -231,7 +231,7 @@ class Storage: drow["max_members"] = 100000 # used by guilds with DISCOVERABLE feature - drow["preffered_locale"] = "en-US" + drow["preferred_locale"] = "en-US" # feature won't be impl'd drow["guild_scheduled_events"] = [] From 494a6b5c63b927a95898262d16b0b1161c8fbb3e Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 15:50:27 -0300 Subject: [PATCH 04/35] tests: add support for test guild resources --- tests/common.py | 113 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 4 deletions(-) diff --git a/tests/common.py b/tests/common.py index ddb4ac2..7f00fc7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -22,7 +22,9 @@ from typing import Optional from dataclasses import dataclass from litecord.common.users import create_user, delete_user +from litecord.common.guilds import delete_guild from litecord.blueprints.auth import make_token +from litecord.storage import int_ def email() -> str: @@ -38,13 +40,92 @@ class WrappedUser: password: str token: str - async def refetch(self): + async def refetch(self) -> dict: async with self.test_cli.app.app_context(): return await self.test_cli.app.storage.get_user(self.id) async def delete(self): + return await delete_user(self.id) + + +@dataclass +class WrappedGuild: + test_cli: "TestClient" + id: int + owner: bool # value depends on the user that fetched guild + owner_id: int + name: str + unavailable: bool + icon: Optional[str] + splash: Optional[str] + region: Optional[str] + afk_timeout: int + afk_channel_id: Optional[str] + afk_timeout: int + verification_level: int + default_message_notifications: int + explicit_content_filter: int + mfa_level: int + embed_enabled: bool + embed_channel_id: int + widget_enabled: bool + widget_channel_id: int + system_channel_id: int + rules_channel_id: int + public_updates_channel_id: int + features: str + features: str + banner: Optional[str] + description: Optional[str] + preferred_locale: Optional[str] + discovery_splash: Optional[str] + + vanity_url_code: Optional[str] + max_presences: int + max_members: int + guild_scheduled_events: list + + joined_at: str # value depends on the user that fetched the guild + + member_count: int + members: list + channels: list + roles: list + presences: list + emojis: list + voice_states: list + + large: Optional[bool] = None + + async def delete(self): + await delete_guild(self.id) + + async def refetch(self) -> "WrappedGuild": async with self.test_cli.app.app_context(): - return await delete_user(self.id) + guild = await self.test_cli.app.storage.get_guild_full(self.id) + return WrappedGuild.from_json(self.test_cli, guild) + + @classmethod + def from_json(cls, test_cli, rjson): + return cls( + test_cli, + **{ + **rjson, + **{ + "id": int(rjson["id"]), + "owner_id": int(rjson["owner_id"]), + "afk_channel_id": int_(rjson["afk_channel_id"]), + "embed_channel_id": int_(rjson["embed_channel_id"]), + "widget_enabled": int_(rjson["widget_enabled"]), + "widget_channel_id": int_(rjson["widget_channel_id"]), + "system_channel_id": int_(rjson["system_channel_id"]), + "rules_channel_id": int_(rjson["rules_channel_id"]), + "public_updates_channel_id": int_( + rjson["public_updates_channel_id"] + ), + }, + }, + ) class TestClient: @@ -66,7 +147,8 @@ class TestClient: async def cleanup(self): for resource in self.resources: - await resource.delete() + async with self.app.app_context(): + await resource.delete() async def create_user( self, @@ -85,11 +167,34 @@ class TestClient: WrappedUser(self, user_id, username, email, password, user_token) ) + async def create_guild( + self, + *, + name: Optional[str] = None, + region: Optional[str] = None, + owner: Optional["WrappedUser"] = None, + ) -> WrappedGuild: + name = name or secrets.token_hex(6) + owner_token = owner.token if owner else self.user["token"] + + async with self.app.app_context(): + # TODO move guild creation logic to litecord.common.guild + # TODO make tests use aiosqlite on memory for db + resp = await self.post( + "/api/v6/guilds", + json={"name": name, "region": region}, + headers={"authorization": owner_token}, + ) + rjson = await resp.json + + return self.add_resource(WrappedGuild.from_json(self, rjson)) + def _inject_auth(self, kwargs: dict) -> list: """Inject the test user's API key into the test request before passing the request on to the underlying TestClient.""" headers = kwargs.get("headers", {}) - headers["authorization"] = self.user["token"] + if "authorization" not in headers: + headers["authorization"] = self.user["token"] return headers async def get(self, *args, **kwargs): From 60e21a173334e5eac34f1ccbaf21ea62cb3a1421 Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 15:55:20 -0300 Subject: [PATCH 05/35] tests: use test guild resource for admin api test_guilds --- tests/test_admin_api/test_guilds.py | 111 ++++++++++------------------ 1 file changed, 37 insertions(+), 74 deletions(-) diff --git a/tests/test_admin_api/test_guilds.py b/tests/test_admin_api/test_guilds.py index 125193c..2ad03c5 100644 --- a/tests/test_admin_api/test_guilds.py +++ b/tests/test_admin_api/test_guilds.py @@ -25,26 +25,10 @@ from litecord.blueprints.guilds import delete_guild from litecord.errors import GuildNotFound -async def _create_guild(test_cli_staff, *, region=None) -> dict: - genned_name = secrets.token_hex(6) - - async with test_cli_staff.app.app_context(): - resp = await test_cli_staff.post( - "/api/v6/guilds", json={"name": genned_name, "region": region} - ) - - 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_staff, guild_id, *, ret_early=False): +async def _fetch_guild(test_cli_staff, guild_id: str, *, return_early: bool = False): resp = await test_cli_staff.get(f"/api/v6/admin/guilds/{guild_id}") - if ret_early: + if return_early: return resp assert resp.status_code == 200 @@ -55,73 +39,54 @@ async def _fetch_guild(test_cli_staff, guild_id, *, ret_early=False): return rjson -async def _delete_guild(test_cli, guild_id: int): - async with test_cli.app.app_context(): - await delete_guild(int(guild_id)) - - @pytest.mark.asyncio async def test_guild_fetch(test_cli_staff): """Test the creation and fetching of a guild via the Admin API.""" - async with test_cli_staff.app.app_context(): - rjson = await _create_guild(test_cli_staff) - guild_id = rjson["id"] - try: - await _fetch_guild(test_cli_staff, guild_id) - finally: - await _delete_guild(test_cli_staff, int(guild_id)) + guild = await test_cli_staff.create_guild() + await _fetch_guild(test_cli_staff, str(guild.id)) @pytest.mark.asyncio async def test_guild_update(test_cli_staff): """Test the update of a guild via the Admin API.""" - async with test_cli_staff.app.app_context(): - rjson = await _create_guild(test_cli_staff) - guild_id = rjson["id"] - assert not rjson["unavailable"] + guild = await test_cli_staff.create_guild() + guild_id = str(guild.id) - 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_staff.patch( - f"/api/v6/admin/guilds/{guild_id}", json={"unavailable": True} - ) + # 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_staff.patch( + f"/api/v6/admin/guilds/{guild_id}", json={"unavailable": True} + ) - assert resp.status_code == 200 - rjson = await resp.json - assert isinstance(rjson, dict) - assert rjson["id"] == guild_id - assert rjson["unavailable"] + 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_staff, guild_id) - assert rjson["unavailable"] - finally: - await _delete_guild(test_cli_staff, int(guild_id)) + rjson = await _fetch_guild(test_cli_staff, guild_id) + assert rjson["id"] == guild_id + assert rjson["unavailable"] @pytest.mark.asyncio async def test_guild_delete(test_cli_staff): """Test the update of a guild via the Admin API.""" - async with test_cli_staff.app.app_context(): - rjson = await _create_guild(test_cli_staff) - guild_id = rjson["id"] + guild = await test_cli_staff.create_guild() + guild_id = str(guild.id) - try: - resp = await test_cli_staff.delete(f"/api/v6/admin/guilds/{guild_id}") + resp = await test_cli_staff.delete(f"/api/v6/admin/guilds/{guild_id}") + assert resp.status_code == 204 - assert resp.status_code == 204 + resp = await _fetch_guild(test_cli_staff, guild_id, return_early=True) + assert resp.status_code == 404 - resp = await _fetch_guild(test_cli_staff, guild_id, 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(test_cli_staff, int(guild_id)) + rjson = await resp.json + assert isinstance(rjson, dict) + assert rjson["error"] + assert rjson["code"] == GuildNotFound.error_code @pytest.mark.asyncio @@ -132,17 +97,15 @@ async def test_guild_create_voice(test_cli_staff): "/api/v6/admin/voice/regions", json={"id": region_id, "name": region_name} ) assert resp.status_code == 200 - guild_id = None + rjson = await resp.json + assert isinstance(rjson, list) + assert region_id in [r["id"] for r in rjson] + # This test is basically creating the guild with a self-selected region + # then deleting the guild afterwards on test resource cleanup try: - rjson = await resp.json - assert isinstance(rjson, list) - assert region_id in [r["id"] for r in rjson] - guild_id = await _create_guild(test_cli_staff, region=region_id) + await test_cli_staff.create_guild(region=region_id) finally: - if guild_id: - await _delete_guild(test_cli_staff, int(guild_id["id"])) - await test_cli_staff.app.db.execute( """ DELETE FROM voice_regions From de4c5a5e75b7455ed469c54ef5d898ff3213650e Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 15:56:04 -0300 Subject: [PATCH 06/35] remove unused import --- tests/test_admin_api/test_guilds.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_admin_api/test_guilds.py b/tests/test_admin_api/test_guilds.py index 2ad03c5..cdb4110 100644 --- a/tests/test_admin_api/test_guilds.py +++ b/tests/test_admin_api/test_guilds.py @@ -21,7 +21,6 @@ import secrets import pytest -from litecord.blueprints.guilds import delete_guild from litecord.errors import GuildNotFound From 948627efd18c5c7075f6625fb69cceb480c204a3 Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 18:29:03 -0300 Subject: [PATCH 07/35] tests: make test app fixture async --- tests/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 57df793..66d7528 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,7 +36,7 @@ from litecord.blueprints.auth import make_token @pytest.fixture(name="app") -def _test_app(unused_tcp_port, event_loop): +async def _test_app(unused_tcp_port): set_blueprints(main_app) main_app.config["_testing"] = True @@ -53,13 +53,13 @@ def _test_app(unused_tcp_port, event_loop): main_app.config["REGISTRATIONS"] = True # make sure we're calling the before_serving hooks - event_loop.run_until_complete(main_app.startup()) + await main_app.startup() # 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()) + await main_app.shutdown() @pytest.fixture(name="test_cli") From 9d81bef527d9e3cf4a8b8178262071b1c40e0f6b Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 23:28:54 -0300 Subject: [PATCH 08/35] channels: handle when chan_type is None --- litecord/blueprints/channels.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/litecord/blueprints/channels.py b/litecord/blueprints/channels.py index ae61820..aa38898 100644 --- a/litecord/blueprints/channels.py +++ b/litecord/blueprints/channels.py @@ -194,6 +194,9 @@ async def close_channel(channel_id): user_id = await token_check() chan_type = await app.storage.get_chan_type(channel_id) + if chan_type is None: + raise ChannelNotFound("Channel not found") + ctype = ChannelType(chan_type) if ctype in GUILD_CHANS: From 0a0d0603dd7f1d29e17b605c1b71ad96a124b1cd Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 23:29:07 -0300 Subject: [PATCH 09/35] channels: don't ignore unknown channel types --- litecord/blueprints/channels.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/litecord/blueprints/channels.py b/litecord/blueprints/channels.py index aa38898..f077a4a 100644 --- a/litecord/blueprints/channels.py +++ b/litecord/blueprints/channels.py @@ -256,8 +256,7 @@ async def close_channel(channel_id): await app.dispatcher.guild.dispatch(guild_id, ("CHANNEL_DELETE", chan)) await app.dispatcher.channel.drop(channel_id) return jsonify(chan) - - if ctype == ChannelType.DM: + elif ctype == ChannelType.DM: chan = await app.storage.get_channel(channel_id) # we don't ever actually delete DM channels off the database. @@ -278,8 +277,7 @@ async def close_channel(channel_id): await dispatch_user(user_id, ("CHANNEL_DELETE", chan)) return jsonify(chan) - - if ctype == ChannelType.GROUP_DM: + elif ctype == ChannelType.GROUP_DM: await gdm_remove_recipient(channel_id, user_id) gdm_count = await app.db.fetchval( @@ -294,8 +292,8 @@ async def close_channel(channel_id): if gdm_count == 0: # destroy dm await gdm_destroy(channel_id) - - raise ChannelNotFound() + else: + raise RuntimeError(f"Data inconsistency: Unknown channel type {ctype}") async def _update_pos(channel_id, pos: int): From 1d36038469910bc012822fffa56ac2034aba141c Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 23:29:19 -0300 Subject: [PATCH 10/35] checks: assert guild channel has correlated guild id --- litecord/blueprints/checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litecord/blueprints/checks.py b/litecord/blueprints/checks.py index 90d67e7..aa126b8 100644 --- a/litecord/blueprints/checks.py +++ b/litecord/blueprints/checks.py @@ -92,7 +92,7 @@ async def channel_check( """, channel_id, ) - + assert guild_id is not None await guild_check(user_id, guild_id) return ctype, guild_id From 7e79abf34454f95c31fa5d35fe571ee5c6d078af Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 23:29:35 -0300 Subject: [PATCH 11/35] guilds: properly delete child channels on guild delete --- litecord/common/guilds.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/litecord/common/guilds.py b/litecord/common/guilds.py index c458f6e..a9f3b88 100644 --- a/litecord/common/guilds.py +++ b/litecord/common/guilds.py @@ -232,6 +232,22 @@ async def _del_from_table(table: str, user_id: int): async def delete_guild(guild_id: int): """Delete a single guild.""" await _del_from_table("vanity_invites", guild_id) + + # while most guild channel tables have 'ON DELETE CASCADE', this + # must not be true to the channels table, which is generic for any channel. + # + # the drawback is that this causes breakdown on the data's semantics as + # we get a channel with a type of GUILD_TEXT/GUILD_VOICE but without any + # entry on the guild_channels table, causing errors. + for channel_id in await app.storage.get_channel_ids(guild_id): + await app.db.execute( + """ + DELETE FROM channels + WHERE channels.id = $1 + """, + channel_id, + ) + await app.db.execute( """ DELETE FROM guilds From ddf78d94e4c0368ecca328fb464c7dcf01ecafed Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 23:30:19 -0300 Subject: [PATCH 12/35] storage: fix typing for get_chan_type --- litecord/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litecord/storage.py b/litecord/storage.py index 65367dc..3f548b5 100644 --- a/litecord/storage.py +++ b/litecord/storage.py @@ -447,7 +447,7 @@ class Storage: # this only exists to trick mypy. this codepath is unreachable raise RuntimeError("Unreachable code path.") - async def get_chan_type(self, channel_id: int) -> int: + async def get_chan_type(self, channel_id: int) -> Optional[int]: """Get the channel type integer, given channel ID.""" return await self.db.fetchval( """ From 7ac38212a3e072fb1396fee41ad285a168107fda Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 23:30:41 -0300 Subject: [PATCH 13/35] storage: assert channel type is valid on more places --- litecord/storage.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/litecord/storage.py b/litecord/storage.py index 3f548b5..be70cd2 100644 --- a/litecord/storage.py +++ b/litecord/storage.py @@ -431,8 +431,7 @@ class Storage: drow["last_message_id"] = last_msg return {**row, **drow} - - if chan_type == ChannelType.GUILD_VOICE: + elif chan_type == ChannelType.GUILD_VOICE: vrow = await self.db.fetchrow( """ SELECT bitrate, user_limit @@ -443,9 +442,9 @@ class Storage: ) return {**row, **dict(vrow)} - - # this only exists to trick mypy. this codepath is unreachable - raise RuntimeError("Unreachable code path.") + else: + # this only exists to trick mypy. this codepath is unreachable + raise AssertionError("Unreachable code path.") async def get_chan_type(self, channel_id: int) -> Optional[int]: """Get the channel type integer, given channel ID.""" @@ -603,7 +602,9 @@ class Storage: drow["last_message_id"] = await self.chan_last_message_str(channel_id) return drow - return None + raise RuntimeError( + f"Data Inconsistency: Channel type {ctype} is not properly handled" + ) async def get_channel_ids(self, guild_id: int) -> List[int]: """Get all channel IDs in a guild.""" From 5bb9d107a6fa2edcc67a1e3a54f05a78f508300c Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 23:30:55 -0300 Subject: [PATCH 14/35] storage: properly handle unknown channels on get_channel --- litecord/storage.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/litecord/storage.py b/litecord/storage.py index be70cd2..eca2481 100644 --- a/litecord/storage.py +++ b/litecord/storage.py @@ -532,6 +532,9 @@ class Storage: async def get_channel(self, channel_id: int, **kwargs) -> Optional[Dict[str, Any]]: """Fetch a single channel's information.""" chan_type = await self.get_chan_type(channel_id) + if chan_type is None: + return None + ctype = ChannelType(chan_type) if ctype in ( From 4fa8fd9d39c5aaefb2ff6df2c9c2fc383a274c2f Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 23:58:08 -0300 Subject: [PATCH 15/35] tests: add support for guild channels and messages --- tests/common.py | 172 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 170 insertions(+), 2 deletions(-) diff --git a/tests/common.py b/tests/common.py index 7f00fc7..636f620 100644 --- a/tests/common.py +++ b/tests/common.py @@ -22,9 +22,12 @@ from typing import Optional from dataclasses import dataclass from litecord.common.users import create_user, delete_user -from litecord.common.guilds import delete_guild +from litecord.common.guilds import delete_guild, create_guild_channel +from litecord.blueprints.channel.messages import create_message from litecord.blueprints.auth import make_token from litecord.storage import int_ +from litecord.enums import ChannelType +from litecord.errors import ChannelNotFound, MessageNotFound def email() -> str: @@ -102,7 +105,9 @@ class WrappedGuild: async def refetch(self) -> "WrappedGuild": async with self.test_cli.app.app_context(): - guild = await self.test_cli.app.storage.get_guild_full(self.id) + guild = await self.test_cli.app.storage.get_guild_full( + self.id, user_id=self.test_cli.user["id"] + ) return WrappedGuild.from_json(self.test_cli, guild) @classmethod @@ -128,6 +133,119 @@ class WrappedGuild: ) +@dataclass +class WrappedGuildChannel: + test_cli: "TestClient" + id: int + type: int + guild_id: int + parent_id: Optional[int] + name: str + position: int + nsfw: bool + topic: str + rate_limit_per_user: int + last_message_id: int + permission_overwrites: list + + async def delete(self): + async with self.test_cli.app.app_context(): + resp = await self.test_cli.delete( + f"/api/v6/channels/{self.id}", + ) + rjson = await resp.json + + if resp.status_code == 404 and rjson["code"] == ChannelNotFound.error_code: + return + + assert resp.status_code == 200 + assert rjson["id"] == str(self.id) + + async def refetch(self) -> dict: + async with self.test_cli.app.app_context(): + channel_data = await self.test_cli.app.storage.get_channel(self.id) + return WrappedGuildChannel.from_json(self.test_cli, channel_data) + + @classmethod + def from_json(cls, test_cli, rjson): + return cls( + test_cli, + **{ + **rjson, + **{ + "id": int(rjson["id"]), + "guild_id": int(rjson["guild_id"]), + "parent_id": int_(rjson["parent_id"]), + }, + }, + ) + + +@dataclass +class WrappedMessage: + test_cli: "TestClient" + + id: int + channel_id: int + author: dict + + type: int + content: str + + timestamp: str + edited_timestamp: str + + tts: bool + mention_everyone: bool + nonce: str + embeds: list + mentions: list + mention_roles: list + reactions: list + attachments: list + pinned: bool + message_reference: Optional[dict] + allowed_mentions: Optional[dict] + member: Optional[dict] = None + flags: Optional[int] = None + guild_id: Optional[int] = None + + async def delete(self): + async with self.test_cli.app.app_context(): + resp = await self.test_cli.delete( + f"/api/v6/channels/{self.channel_id}/messages/{self.id}", + ) + rjson = await resp.json + + if resp.status_code == 404 and rjson["code"] in ( + ChannelNotFound.error_code, + MessageNotFound.error_code, + ): + return + + assert resp.status_code == 200 + assert rjson["id"] == str(self.id) + + async def refetch(self) -> dict: + async with self.test_cli.app.app_context(): + message_data = await self.test_cli.app.storage.get_message(self.id) + return WrappedMessage.from_json(self.test_cli, message_data) + + @classmethod + def from_json(cls, test_cli, rjson): + return cls( + test_cli, + **{ + **rjson, + **{ + "id": int(rjson["id"]), + "channel_id": int(rjson["channel_id"]), + "guild_id": int_(rjson["guild_id"]), + }, + }, + ) + + class TestClient: """Test client wrapper class. Adds Authorization headers to all requests and manages test resource setup and destruction.""" @@ -189,6 +307,56 @@ class TestClient: return self.add_resource(WrappedGuild.from_json(self, rjson)) + async def create_guild_channel( + self, + *, + guild_id: int, + name: Optional[str] = None, + type: ChannelType = ChannelType.GUILD_TEXT, + **kwargs, + ) -> WrappedGuild: + name = name or secrets.token_hex(6) + channel_id = self.app.winter_factory.snowflake() + + async with self.app.app_context(): + await create_guild_channel( + guild_id, channel_id, type, **{**{"name": name}, **kwargs} + ) + channel_data = await self.app.storage.get_channel(channel_id) + + return self.add_resource(WrappedGuildChannel.from_json(self, channel_data)) + + async def create_message( + self, + *, + guild_id: int, + channel_id: int, + content: Optional[str] = None, + author_id: Optional[int] = None, + ) -> WrappedGuild: + content = content or secrets.token_hex(6) + author_id = author_id or self.user["id"] + + async with self.app.app_context(): + message_id = await create_message( + channel_id, + guild_id, + author_id, + { + "content": content, + "tts": False, + "nonce": 0, + "everyone_mention": False, + "embeds": [], + "message_reference": None, + "allowed_mentions": None, + }, + ) + + message_data = await self.app.storage.get_message(message_id) + + return self.add_resource(WrappedMessage.from_json(self, message_data)) + def _inject_auth(self, kwargs: dict) -> list: """Inject the test user's API key into the test request before passing the request on to the underlying TestClient.""" From d7abc1e7000cb3d05aaad241bf58f271f89e0c6e Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Sep 2021 23:58:24 -0300 Subject: [PATCH 16/35] add test_channels --- tests/test_channels.py | 86 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tests/test_channels.py diff --git a/tests/test_channels.py b/tests/test_channels.py new file mode 100644 index 0000000..df6d1d1 --- /dev/null +++ b/tests/test_channels.py @@ -0,0 +1,86 @@ +""" + +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 + +pytestmark = pytest.mark.asyncio + + +async def test_channel_create(test_cli_user): + guild = await test_cli_user.create_guild() + + # guild test object teardown should destroy the channel as well! + resp = await test_cli_user.post( + f"/api/v6/guilds/{guild.id}/channels", + json={ + "name": "hello-world", + }, + ) + assert resp.status_code == 200 + rjson = await resp.json + assert rjson["name"] == "hello-world" + + refetched_guild = await guild.refetch() + assert len(refetched_guild.channels) == 2 + + +async def test_channel_message_send(test_cli_user): + guild = await test_cli_user.create_guild() + channel = guild.channels[0] + resp = await test_cli_user.post( + f'/api/v6/channels/{channel["id"]}/messages', + json={ + "content": "hello world", + }, + ) + assert resp.status_code == 200 + rjson = await resp.json + assert rjson["content"] == "hello world" + + +async def test_channel_message_send_on_new_channel(test_cli_user): + guild = await test_cli_user.create_guild() + channel = await test_cli_user.create_guild_channel(guild_id=guild.id) + assert channel.guild_id == guild.id + + refetched_guild = await guild.refetch() + assert len(refetched_guild.channels) == 2 + + resp = await test_cli_user.post( + f"/api/v6/channels/{channel.id}/messages", + json={ + "content": "hello world", + }, + ) + assert resp.status_code == 200 + rjson = await resp.json + assert rjson["content"] == "hello world" + + +async def test_channel_message_delete(test_cli_user): + guild = await test_cli_user.create_guild() + channel = await test_cli_user.create_guild_channel(guild_id=guild.id) + message = await test_cli_user.create_message( + guild_id=guild.id, channel_id=channel.id + ) + + resp = await test_cli_user.delete( + f"/api/v6/channels/{channel.id}/messages/{message.id}", + ) + assert resp.status_code == 204 From e5691e1362aee6e9f943eb2710dfb6bf73b6a054 Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 6 Sep 2021 00:14:28 -0300 Subject: [PATCH 17/35] tests: make all parameters optional on create_user --- tests/common.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/common.py b/tests/common.py index 636f620..c92b072 100644 --- a/tests/common.py +++ b/tests/common.py @@ -34,6 +34,15 @@ def email() -> str: return f"{secrets.token_hex(5)}@{secrets.token_hex(5)}.com" +def random_email() -> str: + # TODO: move everyone who uses email() to random_email() + return email() + + +def random_username() -> str: + return secrets.token_hex(10) + + @dataclass class WrappedUser: test_cli: "TestClient" @@ -271,11 +280,13 @@ class TestClient: async def create_user( self, *, - username: str, - email: str, + username: Optional[str] = None, + email: Optional[str] = None, password: Optional[str] = None, ) -> WrappedUser: - password = password or secrets.token_hex(6) + username = username or random_username() + email = email or random_email() + password = password or random_username() async with self.app.app_context(): user_id, password_hash = await create_user(username, email, password) From 866accb45f75662a2f5caed856d7f63123bf6670 Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 6 Sep 2021 00:14:43 -0300 Subject: [PATCH 18/35] tests: add test for different member deleting own message --- tests/test_channels.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_channels.py b/tests/test_channels.py index df6d1d1..808a250 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -18,6 +18,7 @@ along with this program. If not, see . """ import pytest +from litecord.common.guilds import add_member pytestmark = pytest.mark.asyncio @@ -84,3 +85,21 @@ async def test_channel_message_delete(test_cli_user): f"/api/v6/channels/{channel.id}/messages/{message.id}", ) assert resp.status_code == 204 + + +async def test_channel_message_delete_different_author(test_cli_user): + guild = await test_cli_user.create_guild() + channel = await test_cli_user.create_guild_channel(guild_id=guild.id) + user = await test_cli_user.create_user() + async with test_cli_user.app.app_context(): + await add_member(guild.id, user.id) + + message = await test_cli_user.create_message( + guild_id=guild.id, channel_id=channel.id, author_id=user.id + ) + + resp = await test_cli_user.delete( + f"/api/v6/channels/{channel.id}/messages/{message.id}", + headers={"authorization": user.token}, + ) + assert resp.status_code == 204 From 829767318ed3306158fbc2a048ff27732b8260bd Mon Sep 17 00:00:00 2001 From: Luna Date: Tue, 7 Sep 2021 00:05:17 -0300 Subject: [PATCH 19/35] tests: add more fields to WrappedUser --- tests/common.py | 55 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/tests/common.py b/tests/common.py index c92b072..580133e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -26,7 +26,7 @@ from litecord.common.guilds import delete_guild, create_guild_channel from litecord.blueprints.channel.messages import create_message from litecord.blueprints.auth import make_token from litecord.storage import int_ -from litecord.enums import ChannelType +from litecord.enums import ChannelType, UserFlags from litecord.errors import ChannelNotFound, MessageNotFound @@ -48,17 +48,52 @@ class WrappedUser: test_cli: "TestClient" id: int name: str + discriminator: str + avatar: Optional[str] + flags: UserFlags + public_flags: UserFlags + bot: bool + premium: bool + bio: str + accent_color: Optional[int] + + # secure fields email: str - password: str - token: str + verified: str + + # extra-secure tokens (not here by default) + password: Optional[str] = None + password_hash: Optional[str] = None + token: Optional[str] = None + + # not there by default + premium_type: Optional[str] = None + mobile: Optional[bool] = None + phone: Optional[bool] = None + mfa_enabled: Optional[bool] = None async def refetch(self) -> dict: async with self.test_cli.app.app_context(): - return await self.test_cli.app.storage.get_user(self.id) + rjson = await self.test_cli.app.storage.get_user(self.id, secure=True) + return WrappedUser.from_json(self.test_cli, rjson) async def delete(self): return await delete_user(self.id) + @classmethod + def from_json(cls, test_cli, data_not_owned): + data = dict(data_not_owned) # take ownership of data via copy + data["name"] = data.pop("username") + return cls( + test_cli, + **{ + **data, + **{ + "id": int(data["id"]), + }, + }, + ) + @dataclass class WrappedGuild: @@ -291,9 +326,19 @@ class TestClient: async with self.app.app_context(): user_id, password_hash = await create_user(username, email, password) user_token = make_token(user_id, password_hash) + full_user_object = await self.app.storage.get_user(user_id, secure=True) return self.add_resource( - WrappedUser(self, user_id, username, email, password, user_token) + WrappedUser.from_json( + self, + { + **full_user_object, + **{ + "token": user_token, + "password_hash": password_hash, + }, + }, + ) ) async def create_guild( From 3d27a30339cbe23b72ce775be1189b4d7446db56 Mon Sep 17 00:00:00 2001 From: Luna Date: Tue, 7 Sep 2021 00:07:36 -0300 Subject: [PATCH 20/35] tests: use test user on test_user_update --- tests/test_admin_api/test_users.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/tests/test_admin_api/test_users.py b/tests/test_admin_api/test_users.py index b8bbf19..814cd29 100644 --- a/tests/test_admin_api/test_users.py +++ b/tests/test_admin_api/test_users.py @@ -120,24 +120,17 @@ async def test_create_delete(test_cli_staff): @pytest.mark.asyncio async def test_user_update(test_cli_staff): """Test user update.""" - rjson = await _setup_user(test_cli_staff) + user = await test_cli_staff.create_user() - user_id = rjson["id"] + # set them as partner flag + resp = await test_cli_staff.patch( + f"/api/v6/admin/users/{user.id}", json={"flags": UserFlags.partner} + ) - # test update + assert resp.status_code == 200 + rjson = await resp.json + assert rjson["id"] == str(user.id) + assert rjson["flags"] == UserFlags.partner - try: - # set them as partner flag - resp = await test_cli_staff.patch( - f"/api/v6/admin/users/{user_id}", 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_staff, user_id) + refetched = await user.refetch() + assert refetched.flags == UserFlags.partner From 2f9fea27403cd7da9c4210bc1e72aa9753b7f02e Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 13 Sep 2021 22:20:12 -0300 Subject: [PATCH 21/35] test_channels: validate new channels can be fetched on api --- tests/test_channels.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_channels.py b/tests/test_channels.py index 808a250..625b752 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -35,10 +35,17 @@ async def test_channel_create(test_cli_user): ) assert resp.status_code == 200 rjson = await resp.json + channel_id: str = rjson["id"] assert rjson["name"] == "hello-world" refetched_guild = await guild.refetch() assert len(refetched_guild.channels) == 2 + assert channel_id in (channel["id"] for channel in refetched_guild.channels) + + resp = await test_cli_user.get(f"/api/v6/channels/{channel_id}") + assert resp.status_code == 200 + rjson = await resp.json + assert rjson["id"] == channel_id async def test_channel_message_send(test_cli_user): From 415f1fa3be17cebc7e0742edbbf6bbc49c2abde9 Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 13 Sep 2021 22:54:10 -0300 Subject: [PATCH 22/35] tests: add test for channel delete --- tests/test_channels.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_channels.py b/tests/test_channels.py index 625b752..c6a98d4 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -48,6 +48,18 @@ async def test_channel_create(test_cli_user): assert rjson["id"] == channel_id +async def test_channel_delete(test_cli_user): + guild = await test_cli_user.create_guild() + channel = await test_cli_user.create_guild_channel(guild_id=guild.id) + + resp = await test_cli_user.delete( + f"/api/v6/channels/{channel.id}", + ) + assert resp.status_code == 200 + rjson = await resp.json + assert rjson["id"] == str(channel.id) + + async def test_channel_message_send(test_cli_user): guild = await test_cli_user.create_guild() channel = guild.channels[0] From 8da480db88959e8928c990366b41a4198d4f771f Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 13 Sep 2021 23:24:05 -0300 Subject: [PATCH 23/35] tests: add test for bulk deletes --- tests/common.py | 4 +++- tests/test_channels.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/common.py b/tests/common.py index 580133e..4a24beb 100644 --- a/tests/common.py +++ b/tests/common.py @@ -270,9 +270,11 @@ class WrappedMessage: assert resp.status_code == 200 assert rjson["id"] == str(self.id) - async def refetch(self) -> dict: + async def refetch(self) -> Optional["WrappedMessage"]: async with self.test_cli.app.app_context(): message_data = await self.test_cli.app.storage.get_message(self.id) + if message_data is None: + return None return WrappedMessage.from_json(self.test_cli, message_data) @classmethod diff --git a/tests/test_channels.py b/tests/test_channels.py index c6a98d4..ccbe84a 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -122,3 +122,23 @@ async def test_channel_message_delete_different_author(test_cli_user): headers={"authorization": user.token}, ) assert resp.status_code == 204 + + +async def test_channel_message_bulk_delete(test_cli_user): + guild = await test_cli_user.create_guild() + channel = await test_cli_user.create_guild_channel(guild_id=guild.id) + messages = [] + for _ in range(10): + messages.append( + await test_cli_user.create_message(guild_id=guild.id, channel_id=channel.id) + ) + + resp = await test_cli_user.post( + f"/api/v6/channels/{channel.id}/messages/bulk-delete", + json={"messages": [message.id for message in messages]}, + ) + assert resp.status_code == 204 + + # assert everyone cant be refetched + for message in messages: + assert (await message.refetch()) is None From 9861823434219b302956f1a9ba30bc0d52c05a5b Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 13 Sep 2021 23:25:05 -0300 Subject: [PATCH 24/35] tests: assert deleted message is actually deleted --- tests/test_channels.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_channels.py b/tests/test_channels.py index ccbe84a..47d33b4 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -105,6 +105,8 @@ async def test_channel_message_delete(test_cli_user): ) assert resp.status_code == 204 + assert (await message.refetch()) is None + async def test_channel_message_delete_different_author(test_cli_user): guild = await test_cli_user.create_guild() From 3d79163ef98092d24f7fba939c97525107339e3e Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 13 Sep 2021 23:34:38 -0300 Subject: [PATCH 25/35] add test for message listing in 'around' param --- tests/test_messages.py | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/test_messages.py diff --git a/tests/test_messages.py b/tests/test_messages.py new file mode 100644 index 0000000..535eadb --- /dev/null +++ b/tests/test_messages.py @@ -0,0 +1,47 @@ +""" + +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 + +pytestmark = pytest.mark.asyncio + + +async def test_message_listing(test_cli_user): + guild = await test_cli_user.create_guild() + channel = await test_cli_user.create_guild_channel(guild_id=guild.id) + messages = [] + for _ in range(10): + messages.append( + await test_cli_user.create_message(guild_id=guild.id, channel_id=channel.id) + ) + + # assert all messages we just created can be refetched if we give the + # middle message to the 'around' parameter + middle_message_id = messages[5].id + + resp = await test_cli_user.get( + f"/api/v6/channels/{channel.id}/messages", + query_string={"around": middle_message_id}, + ) + assert resp.status_code == 200 + rjson = await resp.json + + fetched_ids = [m["id"] for m in rjson] + for message in messages: + assert str(message.id) in fetched_ids From ea06ea88e2b746c3db12a6c3ebae136ce184bef5 Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 13 Sep 2021 23:52:27 -0300 Subject: [PATCH 26/35] tests: add test for 'before' and 'after' params --- tests/test_messages.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_messages.py b/tests/test_messages.py index 535eadb..4494c18 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -45,3 +45,26 @@ async def test_message_listing(test_cli_user): fetched_ids = [m["id"] for m in rjson] for message in messages: assert str(message.id) in fetched_ids + + # assert all messages are below given id if its on 'before' param + + resp = await test_cli_user.get( + f"/api/v6/channels/{channel.id}/messages", + query_string={"before": middle_message_id}, + ) + assert resp.status_code == 200 + rjson = await resp.json + + for message_json in rjson: + assert int(message_json["id"]) <= middle_message_id + + # assert all message are above given id if its on 'after' param + resp = await test_cli_user.get( + f"/api/v6/channels/{channel.id}/messages", + query_string={"after": middle_message_id}, + ) + assert resp.status_code == 200 + rjson = await resp.json + + for message_json in rjson: + assert int(message_json["id"]) >= middle_message_id From 9088ac8fd3feba6fce7d057ee1831e07a6864446 Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 13 Sep 2021 23:52:38 -0300 Subject: [PATCH 27/35] utils: fix bug on not catching 'after' param properly --- litecord/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litecord/utils.py b/litecord/utils.py index 338a5b6..355bebf 100644 --- a/litecord/utils.py +++ b/litecord/utils.py @@ -279,7 +279,7 @@ def query_tuple_from_args(args: dict, limit: int) -> tuple: if "before" in args: before = int(args["before"]) elif "after" in args: - before = int(args["after"]) + after = int(args["after"]) return before, after From 1b294c68a4bf7166e338a2be4fe6003ca51f7bbc Mon Sep 17 00:00:00 2001 From: Luna Date: Tue, 14 Sep 2021 23:48:28 -0300 Subject: [PATCH 28/35] pins: fix unpinning the only pinned message in a channel --- litecord/blueprints/channel/pins.py | 75 +++++++++++------------------ 1 file changed, 29 insertions(+), 46 deletions(-) diff --git a/litecord/blueprints/channel/pins.py b/litecord/blueprints/channel/pins.py index 3e5e9d2..6cb5522 100644 --- a/litecord/blueprints/channel/pins.py +++ b/litecord/blueprints/channel/pins.py @@ -37,6 +37,33 @@ class SysMsgInvalidAction(BadRequest): error_code = 50021 +async def _dispatch_pins_update(channel_id: int) -> None: + message_id = await app.db.fetchval( + """ + SELECT message_id + FROM channel_pins + WHERE channel_id = $1 + ORDER BY message_id ASC + LIMIT 1 + """, + channel_id, + ) + + timestamp = ( + app.winter_factory.to_datetime(message_id) if message_id is not None else None + ) + await app.dispatcher.channel.dispatch( + channel_id, + ( + "CHANNEL_PINS_UPDATE", + { + "channel_id": str(channel_id), + "last_pin_timestamp": timestamp_(timestamp), + }, + ), + ) + + @bp.route("//pins", methods=["GET"]) async def get_pins(channel_id): """Get the pins for a channel""" @@ -93,29 +120,7 @@ async def add_pin(channel_id, message_id): message_id, ) - row = await app.db.fetchrow( - """ - SELECT message_id - FROM channel_pins - WHERE channel_id = $1 - ORDER BY message_id ASC - LIMIT 1 - """, - channel_id, - ) - - timestamp = app.winter_factory.to_datetime(row["message_id"]) - - await app.dispatcher.channel.dispatch( - channel_id, - ( - "CHANNEL_PINS_UPDATE", - { - "channel_id": str(channel_id), - "last_pin_timestamp": timestamp_(timestamp), - }, - ), - ) + await _dispatch_pins_update(channel_id) await send_sys_message( channel_id, MessageType.CHANNEL_PINNED_MESSAGE, message_id, user_id @@ -140,28 +145,6 @@ async def delete_pin(channel_id, message_id): message_id, ) - row = await app.db.fetchrow( - """ - SELECT message_id - FROM channel_pins - WHERE channel_id = $1 - ORDER BY message_id ASC - LIMIT 1 - """, - channel_id, - ) - - timestamp = app.winter_factory.to_datetime(row["message_id"]) - - await app.dispatcher.channel.dispatch( - channel_id, - ( - "CHANNEL_PINS_UPDATE", - { - "channel_id": str(channel_id), - "last_pin_timestamp": timestamp.isoformat(), - }, - ), - ) + await _dispatch_pins_update(channel_id) return "", 204 From 83bf048604cba4c9851f6cb8f44ef9c8f99f8259 Mon Sep 17 00:00:00 2001 From: Luna Date: Tue, 14 Sep 2021 23:48:58 -0300 Subject: [PATCH 29/35] pins: update system message error message --- litecord/blueprints/channel/pins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litecord/blueprints/channel/pins.py b/litecord/blueprints/channel/pins.py index 6cb5522..477d543 100644 --- a/litecord/blueprints/channel/pins.py +++ b/litecord/blueprints/channel/pins.py @@ -109,7 +109,7 @@ async def add_pin(channel_id, message_id): ) if mtype in SYS_MESSAGES: - raise SysMsgInvalidAction("Cannot execute action on a system message") + raise SysMsgInvalidAction("Cannot pin a system message") await app.db.execute( """ From c5af9cacada9c8826134306b3c6137a711797c99 Mon Sep 17 00:00:00 2001 From: Luna Date: Tue, 14 Sep 2021 23:49:21 -0300 Subject: [PATCH 30/35] add test for pinned messages --- tests/test_messages.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_messages.py b/tests/test_messages.py index 4494c18..5c2dc3f 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -68,3 +68,30 @@ async def test_message_listing(test_cli_user): for message_json in rjson: assert int(message_json["id"]) >= middle_message_id + + +async def test_message_pinning(test_cli_user): + guild = await test_cli_user.create_guild() + channel = await test_cli_user.create_guild_channel(guild_id=guild.id) + message = await test_cli_user.create_message( + guild_id=guild.id, channel_id=channel.id + ) + + resp = await test_cli_user.put(f"/api/v6/channels/{channel.id}/pins/{message.id}") + assert resp.status_code == 204 + + resp = await test_cli_user.get(f"/api/v6/channels/{channel.id}/pins") + assert resp.status_code == 200 + rjson = await resp.json + assert len(rjson) == 1 + assert rjson[0]["id"] == str(message.id) + + resp = await test_cli_user.delete( + f"/api/v6/channels/{channel.id}/pins/{message.id}" + ) + assert resp.status_code == 204 + + resp = await test_cli_user.get(f"/api/v6/channels/{channel.id}/pins") + assert resp.status_code == 200 + rjson = await resp.json + assert len(rjson) == 0 From 8f3fa52fa8d3db5c9adcd626e6c1bb9108497979 Mon Sep 17 00:00:00 2001 From: Luna Date: Tue, 14 Sep 2021 23:49:28 -0300 Subject: [PATCH 31/35] add test for updating messages --- tests/test_messages.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_messages.py b/tests/test_messages.py index 5c2dc3f..a92b58d 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -70,6 +70,27 @@ async def test_message_listing(test_cli_user): assert int(message_json["id"]) >= middle_message_id +async def test_message_update(test_cli_user): + guild = await test_cli_user.create_guild() + channel = await test_cli_user.create_guild_channel(guild_id=guild.id) + message = await test_cli_user.create_message( + guild_id=guild.id, channel_id=channel.id + ) + + resp = await test_cli_user.patch( + f"/api/v6/channels/{channel.id}/messages/{message.id}", + json={"content": "awooga"}, + ) + assert resp.status_code == 200 + rjson = await resp.json + + assert rjson["id"] == str(message.id) + assert rjson["content"] == "awooga" + + refetched = await message.refetch() + assert refetched.content == "awooga" + + async def test_message_pinning(test_cli_user): guild = await test_cli_user.create_guild() channel = await test_cli_user.create_guild_channel(guild_id=guild.id) From 53c0b8578d8838a8bd9f342dbfce2be986f8de33 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 15 Sep 2021 22:57:13 -0300 Subject: [PATCH 32/35] add test for webhook creation --- tests/test_webhooks.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/test_webhooks.py diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py new file mode 100644 index 0000000..8193d78 --- /dev/null +++ b/tests/test_webhooks.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 . + +""" + +import pytest + +pytestmark = pytest.mark.asyncio + + +async def test_webhook_flow(test_cli_user): + guild = await test_cli_user.create_guild() + channel = await test_cli_user.create_guild_channel(guild_id=guild.id) + + resp = await test_cli_user.post( + f"/api/v6/channels/{channel.id}/webhooks", json={"name": "awooga"} + ) + assert resp.status_code == 200 + rjson = await resp.json + assert rjson["channel_id"] == str(channel.id) + assert rjson["guild_id"] == str(guild.id) + assert rjson["name"] == "awooga" From 7503b60bc845729532c41d27822ada738d5467b1 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 15 Sep 2021 23:39:13 -0300 Subject: [PATCH 33/35] do not continuously set app blueprints --- tests/conftest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 66d7528..e625dba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,7 +28,7 @@ sys.path.append(os.getcwd()) from tests.common import email, TestClient -from run import app as main_app, set_blueprints +from run import app as main_app from litecord.common.users import create_user, delete_user from litecord.enums import UserFlags @@ -37,7 +37,6 @@ from litecord.blueprints.auth import make_token @pytest.fixture(name="app") async def _test_app(unused_tcp_port): - set_blueprints(main_app) main_app.config["_testing"] = True # reassign an unused tcp port for websockets From c79b4df252377e5a2b398162934d31fd985ea5ef Mon Sep 17 00:00:00 2001 From: Luna Date: Thu, 16 Sep 2021 21:57:33 -0300 Subject: [PATCH 34/35] add some int fields to WrappedGuildChannel --- tests/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/common.py b/tests/common.py index 4a24beb..6f13cff 100644 --- a/tests/common.py +++ b/tests/common.py @@ -220,6 +220,8 @@ class WrappedGuildChannel: "id": int(rjson["id"]), "guild_id": int(rjson["guild_id"]), "parent_id": int_(rjson["parent_id"]), + "last_message_id": int_(rjson["last_message_id"]), + "rate_limit_per_user": int_(rjson["rate_limit_per_user"]), }, }, ) From 63d451f2d0b4b58aada3878f496df70945ceb2c8 Mon Sep 17 00:00:00 2001 From: Luna Date: Thu, 16 Sep 2021 21:57:49 -0300 Subject: [PATCH 35/35] add test for webhook execution --- tests/test_webhooks.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py index 8193d78..e73c615 100644 --- a/tests/test_webhooks.py +++ b/tests/test_webhooks.py @@ -34,3 +34,19 @@ async def test_webhook_flow(test_cli_user): assert rjson["channel_id"] == str(channel.id) assert rjson["guild_id"] == str(guild.id) assert rjson["name"] == "awooga" + + webhook_id = rjson["id"] + webhook_token = rjson["token"] + + resp = await test_cli_user.post( + f"/api/v6/webhooks/{webhook_id}/{webhook_token}", + json={"content": "test_message"}, + headers={"authorization": ""}, + ) + assert resp.status_code == 204 + + refetched_channel = await channel.refetch() + message = await test_cli_user.app.storage.get_message( + refetched_channel.last_message_id + ) + assert message["author"]["id"] == webhook_id