From 531a14604b0f32547b65cba1ee9205fe3778411a Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 21 Oct 2025 16:40:54 +0700 Subject: [PATCH 1/2] feat: apply changes related to tests from #87 --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 4 +- MANIFEST.in | 13 +-- tests/test_autopost.py | 187 +++++++++++++++++----------------- tests/test_client.py | 43 +++++--- tests/test_data_container.py | 9 +- tests/test_ratelimiter.py | 56 +++++----- tests/test_type.py | 155 ++++++++-------------------- tests/test_webhook.py | 160 ++++++++++++++--------------- 9 files changed, 279 insertions(+), 350 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6f637a26..1284d783 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: - python-version: 3.13 + python-version: 3.14 - name: Install dependencies run: python3 -m pip install build twine - name: Build and publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c077d633..784f0b5e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ 3.9, '3.10', 3.11, 3.12, 3.13 ] + python-version: [ '3.10', 3.11, 3.12, 3.13, 3.14 ] steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} @@ -17,6 +17,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install - run: python -m pip install .[dev] + run: python -m pip install . pytest mock pytest-mock pytest-asyncio - name: Test with pytest run: pytest diff --git a/MANIFEST.in b/MANIFEST.in index a0e44e5b..3bd177d4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,18 +1,11 @@ prune .github -prune .pytest_cache prune .ruff_cache -prune build prune docs prune examples -prune scripts prune tests - -exclude .coverage exclude .gitignore exclude .readthedocs.yml -exclude ISSUE_TEMPLATE.md -exclude mypy.ini -exclude PULL_REQUEST_TEMPLATE.md -exclude pytest.ini exclude ruff.toml -exclude LICENSE \ No newline at end of file +exclude LICENSE +exclude ISSUE_TEMPLATE.md +exclude PULL_REQUEST_TEMPLATE.md \ No newline at end of file diff --git a/tests/test_autopost.py b/tests/test_autopost.py index b83a0cc0..aee465de 100644 --- a/tests/test_autopost.py +++ b/tests/test_autopost.py @@ -1,95 +1,92 @@ -import datetime - -import mock -import pytest -from aiohttp import ClientSession -from pytest_mock import MockerFixture - -from topgg import DBLClient -from topgg.autopost import AutoPoster -from topgg.errors import HTTPException, TopGGException - - -MOCK_TOKEN = ".eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=." - - -@pytest.fixture -def session() -> ClientSession: - return mock.Mock(ClientSession) - - -@pytest.fixture -def autopost(session: ClientSession) -> AutoPoster: - return AutoPoster(DBLClient(MOCK_TOKEN, session=session)) - - -@pytest.mark.asyncio -async def test_AutoPoster_breaks_autopost_loop_on_401( - mocker: MockerFixture, session: ClientSession -) -> None: - response = mock.Mock("reason, status") - response.reason = "Unauthorized" - response.status = 401 - - mocker.patch( - "topgg.DBLClient.post_guild_count", side_effect=HTTPException(response, {}) - ) - - callback = mock.Mock() - autopost = DBLClient(MOCK_TOKEN, session=session).autopost().stats(callback) - assert isinstance(autopost, AutoPoster) - assert not isinstance(autopost.stats()(callback), AutoPoster) - - with pytest.raises(HTTPException): - await autopost.start() - - callback.assert_called_once() - assert not autopost.is_running - - -@pytest.mark.asyncio -async def test_AutoPoster_raises_missing_stats(autopost: AutoPoster) -> None: - with pytest.raises( - TopGGException, match="you must provide a callback that returns the stats." - ): - await autopost.start() - - -@pytest.mark.asyncio -async def test_AutoPoster_raises_already_running(autopost: AutoPoster) -> None: - autopost.stats(mock.Mock()).start() - with pytest.raises(TopGGException, match="the autopost is already running."): - await autopost.start() - - -@pytest.mark.asyncio -async def test_AutoPoster_interval_too_short(autopost: AutoPoster) -> None: - with pytest.raises(ValueError, match="interval must be greated than 900 seconds."): - autopost.set_interval(50) - - -@pytest.mark.asyncio -async def test_AutoPoster_error_callback( - mocker: MockerFixture, autopost: AutoPoster -) -> None: - error_callback = mock.Mock() - response = mock.Mock("reason, status") - response.reason = "Internal Server Error" - response.status = 500 - side_effect = HTTPException(response, {}) - - mocker.patch("topgg.DBLClient.post_guild_count", side_effect=side_effect) - task = autopost.on_error(error_callback).stats(mock.Mock()).start() - autopost.stop() - await task - error_callback.assert_called_once_with(side_effect) - - -def test_AutoPoster_interval(autopost: AutoPoster): - assert autopost.interval == 900 - autopost.set_interval(datetime.timedelta(hours=1)) - assert autopost.interval == 3600 - autopost.interval = datetime.timedelta(hours=2) - assert autopost.interval == 7200 - autopost.interval = 3600 - assert autopost.interval == 3600 +import datetime + +import mock +import pytest +from aiohttp import ClientSession +from pytest_mock import MockerFixture + +from topgg import DBLClient +from topgg.autopost import AutoPoster +from topgg.errors import HTTPException, TopGGException + + +MOCK_TOKEN = ".eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=." + + +@pytest.fixture +def session() -> ClientSession: + return mock.Mock(ClientSession) + + +@pytest.fixture +def autopost(session: ClientSession) -> AutoPoster: + return AutoPoster(DBLClient(MOCK_TOKEN, session=session)) + + +@pytest.mark.asyncio +async def test_AutoPoster_breaks_autopost_loop_on_401( + mocker: MockerFixture, session: ClientSession +) -> None: + mocker.patch( + "topgg.DBLClient.post_guild_count", + side_effect=HTTPException("Unauthorized", 401), + ) + + callback = mock.Mock() + autopost = DBLClient(MOCK_TOKEN, session=session).autopost().stats(callback) + + assert isinstance(autopost, AutoPoster) + assert not isinstance(autopost.stats()(callback), AutoPoster) + + autopost._interval = 1 + + with pytest.raises(HTTPException): + await autopost.start() + + callback.assert_called_once() + assert not autopost.is_running + + +@pytest.mark.asyncio +async def test_AutoPoster_raises_missing_stats(autopost: AutoPoster) -> None: + with pytest.raises( + TopGGException, match="You must provide a callback that returns the stats." + ): + await autopost.start() + + +@pytest.mark.asyncio +async def test_AutoPoster_raises_already_running(autopost: AutoPoster) -> None: + autopost.stats(mock.Mock()).start() + with pytest.raises(TopGGException, match="The autoposter is already running."): + await autopost.start() + + +@pytest.mark.asyncio +async def test_AutoPoster_interval_too_short(autopost: AutoPoster) -> None: + with pytest.raises(ValueError, match="interval must be greater than 900 seconds."): + autopost.set_interval(50) + + +@pytest.mark.asyncio +async def test_AutoPoster_error_callback( + mocker: MockerFixture, autopost: AutoPoster +) -> None: + error_callback = mock.Mock() + side_effect = HTTPException("Internal Server Error", 500) + + mocker.patch("topgg.DBLClient.post_guild_count", side_effect=side_effect) + task = autopost.on_error(error_callback).stats(mock.Mock()).start() + autopost.stop() + await task + error_callback.assert_called_once_with(side_effect) + + +def test_AutoPoster_interval(autopost: AutoPoster): + assert autopost.interval == 900 + autopost.set_interval(datetime.timedelta(hours=1)) + assert autopost.interval == 3600 + autopost.interval = datetime.timedelta(hours=2) + assert autopost.interval == 7200 + autopost.interval = 3600 + assert autopost.interval == 3600 diff --git a/tests/test_client.py b/tests/test_client.py index f0a9c456..f26895a0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,5 +1,6 @@ import mock import pytest +from aiohttp import ClientSession import topgg @@ -7,48 +8,58 @@ MOCK_TOKEN = ".eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=." +@pytest.fixture +def session() -> ClientSession: + return mock.Mock(ClientSession) + + +@pytest.fixture +def client(session: ClientSession) -> topgg.DBLClient: + return topgg.DBLClient(MOCK_TOKEN, session=session) + + @pytest.mark.asyncio -async def test_DBLClient_post_guild_count_with_no_args(): - client = topgg.DBLClient(MOCK_TOKEN) - with pytest.raises(TypeError, match="stats or guild_count must be provided."): +async def test_DBLClient_post_guild_count_with_no_args(client: topgg.DBLClient): + with pytest.raises(ValueError, match="Got an invalid server count. Got None."): await client.post_guild_count() @pytest.mark.asyncio -async def test_DBLClient_get_weekend_status(monkeypatch): - client = topgg.DBLClient(MOCK_TOKEN) +async def test_DBLClient_get_weekend_status(monkeypatch, client: topgg.DBLClient): monkeypatch.setattr("topgg.DBLClient._DBLClient__request", mock.AsyncMock()) await client.get_weekend_status() client._DBLClient__request.assert_called_once() @pytest.mark.asyncio -async def test_DBLClient_post_guild_count(monkeypatch): - client = topgg.DBLClient(MOCK_TOKEN) +async def test_DBLClient_post_guild_count(monkeypatch, client: topgg.DBLClient): monkeypatch.setattr("topgg.DBLClient._DBLClient__request", mock.AsyncMock()) await client.post_guild_count(guild_count=123) client._DBLClient__request.assert_called_once() @pytest.mark.asyncio -async def test_DBLClient_get_guild_count(monkeypatch): - client = topgg.DBLClient(MOCK_TOKEN) - monkeypatch.setattr("topgg.DBLClient._DBLClient__request", mock.AsyncMock(return_value={})) +async def test_DBLClient_get_guild_count(monkeypatch, client: topgg.DBLClient): + monkeypatch.setattr( + "topgg.DBLClient._DBLClient__request", mock.AsyncMock(return_value={}) + ) await client.get_guild_count() client._DBLClient__request.assert_called_once() @pytest.mark.asyncio -async def test_DBLClient_get_bot_votes(monkeypatch): - client = topgg.DBLClient(MOCK_TOKEN) - monkeypatch.setattr("topgg.DBLClient._DBLClient__request", mock.AsyncMock(return_value=[])) +async def test_DBLClient_get_bot_votes(monkeypatch, client: topgg.DBLClient): + monkeypatch.setattr( + "topgg.DBLClient._DBLClient__request", mock.AsyncMock(return_value=[]) + ) await client.get_bot_votes() client._DBLClient__request.assert_called_once() @pytest.mark.asyncio -async def test_DBLClient_get_user_vote(monkeypatch): - client = topgg.DBLClient(MOCK_TOKEN) - monkeypatch.setattr("topgg.DBLClient._DBLClient__request", mock.AsyncMock(return_value={"voted": 1})) +async def test_DBLClient_get_user_vote(monkeypatch, client: topgg.DBLClient): + monkeypatch.setattr( + "topgg.DBLClient._DBLClient__request", mock.AsyncMock(return_value={"voted": 1}) + ) await client.get_user_vote(1234) client._DBLClient__request.assert_called_once() diff --git a/tests/test_data_container.py b/tests/test_data_container.py index 978574fb..0fd1bede 100644 --- a/tests/test_data_container.py +++ b/tests/test_data_container.py @@ -15,18 +15,15 @@ def data_container() -> DataContainerMixin: async def _async_callback( text: str = data(str), number: int = data(int), mapping: dict = data(dict) -): - ... +): ... def _sync_callback( text: str = data(str), number: int = data(int), mapping: dict = data(dict) -): - ... +): ... -def _invalid_callback(number: float = data(float)): - ... +def _invalid_callback(number: float = data(float)): ... @pytest.mark.asyncio diff --git a/tests/test_ratelimiter.py b/tests/test_ratelimiter.py index 998a7357..53692fe4 100644 --- a/tests/test_ratelimiter.py +++ b/tests/test_ratelimiter.py @@ -1,28 +1,28 @@ -import pytest - -from topgg.ratelimiter import Ratelimiter - -n = period = 10 - - -@pytest.fixture -def limiter() -> Ratelimiter: - return Ratelimiter(max_calls=n, period=period) - - -@pytest.mark.asyncio -async def test_AsyncRateLimiter_calls(limiter: Ratelimiter) -> None: - for _ in range(n): - async with limiter: - pass - - assert len(limiter._Ratelimiter__calls) == limiter._Ratelimiter__max_calls == n - - -@pytest.mark.asyncio -async def test_AsyncRateLimiter_timespan_property(limiter: Ratelimiter) -> None: - for _ in range(n): - async with limiter: - pass - - assert limiter._timespan < period +import pytest + +from topgg.ratelimiter import Ratelimiter + +n = period = 10 + + +@pytest.fixture +def limiter() -> Ratelimiter: + return Ratelimiter(max_calls=n, period=period) + + +@pytest.mark.asyncio +async def test_AsyncRateLimiter_calls(limiter: Ratelimiter) -> None: + for _ in range(n): + async with limiter: + pass + + assert len(limiter._Ratelimiter__calls) == limiter._Ratelimiter__max_calls == n + + +@pytest.mark.asyncio +async def test_AsyncRateLimiter_timespan_property(limiter: Ratelimiter) -> None: + for _ in range(n): + async with limiter: + pass + + assert limiter._timespan < period diff --git a/tests/test_type.py b/tests/test_type.py index caec363c..b30bfcf1 100644 --- a/tests/test_type.py +++ b/tests/test_type.py @@ -3,35 +3,28 @@ from topgg import types d: dict = { - "defAvatar": "6debd47ed13483642cf09e832ed0bc1b", - "invite": "", - "website": "https://top.gg", - "support": "KYZsaFb", - "github": "https://github.com/top-gg/Luca", - "longdesc": "Luca only works in the **Discord Bot List** server. \nPrepend commands with the prefix `-` or " - "`@Luca#1375`. \n**Please refrain from using these commands in non testing channels.**\n- `botinfo " - "@bot` Shows bot info, title redirects to site listing.\n- `bots @user`* Shows all bots of that user, " - "includes bots in the queue.\n- `owner / -owners @bot`* Shows all owners of that bot.\n- `prefix " - "@bot`* Shows the prefix of that bot.\n* Mobile friendly version exists. Just add `noembed` to the " - "end of the command.\n", - "shortdesc": "Luca is a bot for managing and informing members of the server", - "prefix": "- or @Luca#1375", - "lib": None, - "clientid": "264811613708746752", - "avatar": "7edcc4c6fbb0b23762455ca139f0e1c9", - "id": "264811613708746752", - "discriminator": "1375", - "username": "Luca", - "date": "2017-04-26T18:08:17.125Z", + "invite": "https://top.gg/discord", + "support": "https://discord.gg/dbl", + "github": "https://github.com/top-gg", + "longdesc": "A bot to grant API access to our Library Developers on the Top.gg site without them needing to submit a bot to pass verification just to be able to access the API.\n\nThis is not a real bot, so if you happen to find this page, do not try to invite it. It will not work.\n\nAccess to this bot's team can be requested by contacting a Community Manager in [our Discord server](https://top.gg/discord).", + "shortdesc": "API access for Top.gg Library Developers", + "prefix": "/", + "lib": "", + "clientid": "1026525568344264724", + "avatar": "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png", + "id": "1026525568344264724", + "username": "Top.gg Lib Dev API Access", + "date": "2022-10-03T16:08:55.292Z", "server_count": 2, - "guilds": ["417723229721853963", "264445053596991498"], + "shard_count": 0, + "guilds": [], "shards": [], - "monthlyPoints": 19, - "points": 397, + "monthlyPoints": 2, + "points": 28, "certifiedBot": False, - "owners": ["129908908096487424"], - "tags": ["Moderation", "Role Management", "Logging"], - "donatebotguildid": "", + "owners": ["121919449996460033"], + "tags": ["api", "library", "topgg"], + "reviews": {"averageScore": 5, "count": 2}, } query_dict = {"qwe": "1", "rty": "2", "uio": "3"} @@ -47,6 +40,7 @@ "user": "3", "type": "test", "query": "?" + "&".join(f"{k}={v}" for k, v in query_dict.items()), + "isWeekend": False, } server_vote_dict = { @@ -56,147 +50,84 @@ "query": "?" + "&".join(f"{k}={v}" for k, v in query_dict.items()), } -user_data_dict = { - "discriminator": "0001", - "avatar": "a_1241439d430def25c100dd28add2d42f", - "id": "140862798832861184", - "username": "Xetera", - "defAvatar": "322c936a8c8be1b803cd94861bdfa868", - "admin": True, - "webMod": True, - "mod": True, - "certifiedDev": False, - "supporter": False, - "social": {}, -} - -bot_stats_dict = {"shards": [1, 5, 8]} - - -@pytest.fixture -def data_dict() -> types.DataDict: - return types.DataDict(**d) +bot_stats_dict = {"server_count": 2, "shards": [], "shard_count": 0} @pytest.fixture def bot_data() -> types.BotData: - return types.BotData(**d) - - -@pytest.fixture -def user_data() -> types.UserData: - return types.UserData(**user_data_dict) + return types.BotData(d) @pytest.fixture def widget_options() -> types.WidgetOptions: - return types.WidgetOptions(id=int(d["id"])) + return types.WidgetOptions( + id=int(d["id"]), + project_type=types.WidgetProjectType.DISCORD_BOT, + type=types.WidgetType.LARGE, + ) @pytest.fixture def vote_data() -> types.VoteDataDict: - return types.VoteDataDict(**vote_data_dict) + return types.VoteDataDict(vote_data_dict) @pytest.fixture def bot_vote_data() -> types.BotVoteData: - return types.BotVoteData(**bot_vote_dict) + return types.BotVoteData(bot_vote_dict) @pytest.fixture def server_vote_data() -> types.GuildVoteData: - return types.GuildVoteData(**server_vote_dict) + return types.GuildVoteData(server_vote_dict) @pytest.fixture def bot_stats_data() -> types.BotStatsData: - return types.BotStatsData(**bot_stats_dict) - - -def test_data_dict_fields(data_dict: types.DataDict) -> None: - for attr in data_dict: - if "id" in attr.lower(): - assert isinstance(data_dict[attr], int) or data_dict[attr] is None - assert data_dict.get(attr) == data_dict[attr] == getattr(data_dict, attr) + return types.BotStatsData(bot_stats_dict) def test_bot_data_fields(bot_data: types.BotData) -> None: bot_data.github = "I'm a GitHub link!" bot_data.support = "Support has arrived!" - for attr in bot_data: + for attr in bot_data.__slots__: if "id" in attr.lower(): - assert isinstance(bot_data[attr], int) or bot_data[attr] is None + value = getattr(bot_data, attr) + + assert isinstance(value, int) or value is None elif attr in ("owners", "guilds"): - for item in bot_data[attr]: + for item in getattr(bot_data, attr): assert isinstance(item, int) - assert bot_data.get(attr) == bot_data[attr] == getattr(bot_data, attr) - - -def test_widget_options_fields(widget_options: types.WidgetOptions) -> None: - assert widget_options["colors"] == widget_options["colours"] - - widget_options.colours = {"background": 0} - widget_options["colours"]["text"] = 255 - assert widget_options.colours == widget_options["colors"] - - for attr in widget_options: - if "id" in attr.lower(): - assert isinstance(widget_options[attr], int) or widget_options[attr] is None - assert ( - widget_options.get(attr) - == widget_options[attr] - == widget_options[attr] - == getattr(widget_options, attr) - ) def test_vote_data_fields(vote_data: types.VoteDataDict) -> None: assert isinstance(vote_data.query, dict) vote_data.type = "upvote" - for attr in vote_data: - assert getattr(vote_data, attr) == vote_data.get(attr) == vote_data[attr] - def test_bot_vote_data_fields(bot_vote_data: types.BotVoteData) -> None: assert isinstance(bot_vote_data.query, dict) bot_vote_data.type = "upvote" - assert isinstance(bot_vote_data["bot"], int) - for attr in bot_vote_data: - assert ( - getattr(bot_vote_data, attr) - == bot_vote_data.get(attr) - == bot_vote_data[attr] - ) + assert isinstance(bot_vote_data.bot, int) def test_server_vote_data_fields(server_vote_data: types.BotVoteData) -> None: assert isinstance(server_vote_data.query, dict) server_vote_data.type = "upvote" - assert isinstance(server_vote_data["guild"], int) - for attr in server_vote_data: - assert ( - getattr(server_vote_data, attr) - == server_vote_data.get(attr) - == server_vote_data[attr] - ) + assert isinstance(server_vote_data.guild, int) def test_bot_stats_data_attrs(bot_stats_data: types.BotStatsData) -> None: for count in ("server_count", "shard_count"): - assert isinstance(bot_stats_data[count], int) or bot_stats_data[count] is None + value = getattr(bot_stats_data, count) + + assert isinstance(value, int) or value is None + assert isinstance(bot_stats_data.shards, list) + if bot_stats_data.shards: for shard in bot_stats_data.shards: assert isinstance(shard, int) - - -def test_user_data_attrs(user_data: types.UserData) -> None: - assert isinstance(user_data.social, types.SocialData) - for attr in user_data: - if "id" in attr.lower(): - assert isinstance(user_data[attr], int) or user_data[attr] is None - assert user_data[attr] == getattr(user_data, attr) == user_data.get(attr) diff --git a/tests/test_webhook.py b/tests/test_webhook.py index 8ef3c71d..863fd627 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -1,80 +1,80 @@ -import typing as t - -import aiohttp -import mock -import pytest - -from topgg import WebhookManager, WebhookType -from topgg.errors import TopGGException - -auth = "youshallnotpass" - - -@pytest.fixture -def webhook_manager() -> WebhookManager: - return ( - WebhookManager() - .endpoint() - .type(WebhookType.BOT) - .auth(auth) - .route("/dbl") - .callback(print) - .add_to_manager() - .endpoint() - .type(WebhookType.GUILD) - .auth(auth) - .route("/dsl") - .callback(print) - .add_to_manager() - ) - - -def test_WebhookManager_routes(webhook_manager: WebhookManager) -> None: - assert len(webhook_manager.app.router.routes()) == 2 - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "headers, result, state", - [({"authorization": auth}, 200, True), ({}, 401, False)], -) -async def test_WebhookManager_validates_auth( - webhook_manager: WebhookManager, headers: t.Dict[str, str], result: int, state: bool -) -> None: - await webhook_manager.start(5000) - - try: - for path in ("dbl", "dsl"): - async with aiohttp.request( - "POST", f"http://localhost:5000/{path}", headers=headers, json={} - ) as r: - assert r.status == result - finally: - await webhook_manager.close() - assert not webhook_manager.is_running - - -def test_WebhookEndpoint_callback_unset(webhook_manager: WebhookManager): - with pytest.raises( - TopGGException, - match="endpoint missing callback.", - ): - webhook_manager.endpoint().add_to_manager() - - -def test_WebhookEndpoint_route_unset(webhook_manager: WebhookManager): - with pytest.raises( - TopGGException, - match="endpoint missing type.", - ): - webhook_manager.endpoint().callback(mock.Mock()).add_to_manager() - - -def test_WebhookEndpoint_type_unset(webhook_manager: WebhookManager): - with pytest.raises( - TopGGException, - match="endpoint missing route.", - ): - webhook_manager.endpoint().callback(mock.Mock()).type( - WebhookType.BOT - ).add_to_manager() +import typing as t + +import aiohttp +import mock +import pytest + +from topgg import WebhookManager, WebhookType +from topgg.errors import TopGGException + +auth = "youshallnotpass" + + +@pytest.fixture +def webhook_manager() -> WebhookManager: + return ( + WebhookManager() + .endpoint() + .type(WebhookType.BOT) + .auth(auth) + .route("/dbl") + .callback(print) + .add_to_manager() + .endpoint() + .type(WebhookType.GUILD) + .auth(auth) + .route("/dsl") + .callback(print) + .add_to_manager() + ) + + +def test_WebhookManager_routes(webhook_manager: WebhookManager) -> None: + assert len(webhook_manager.app.router.routes()) == 2 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "headers, result, state", + [({"authorization": auth}, 204, True), ({}, 401, False)], +) +async def test_WebhookManager_validates_auth( + webhook_manager: WebhookManager, headers: t.Dict[str, str], result: int, state: bool +) -> None: + await webhook_manager.start(5000) + + try: + for path in ("dbl", "dsl"): + async with aiohttp.request( + "POST", f"http://localhost:5000/{path}", headers=headers, json={} + ) as r: + assert r.status == result + finally: + await webhook_manager.close() + assert not webhook_manager.is_running + + +def test_WebhookEndpoint_callback_unset(webhook_manager: WebhookManager): + with pytest.raises( + TopGGException, + match="endpoint missing callback.", + ): + webhook_manager.endpoint().add_to_manager() + + +def test_WebhookEndpoint_route_unset(webhook_manager: WebhookManager): + with pytest.raises( + TopGGException, + match="endpoint missing type.", + ): + webhook_manager.endpoint().callback(mock.Mock()).add_to_manager() + + +def test_WebhookEndpoint_type_unset(webhook_manager: WebhookManager): + with pytest.raises( + TopGGException, + match="endpoint missing route.", + ): + webhook_manager.endpoint().callback(mock.Mock()).type( + WebhookType.BOT + ).add_to_manager() From 26fd1534e45444fdcbfcf0cbbc80b75443bd449a Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 22 Oct 2025 21:32:21 +0700 Subject: [PATCH 2/2] fix: also install pytest-cov --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 784f0b5e..ef8d641f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install - run: python -m pip install . pytest mock pytest-mock pytest-asyncio + run: python -m pip install . pytest mock pytest-mock pytest-asyncio pytest-cov - name: Test with pytest run: pytest