Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ 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 }}
uses: actions/setup-python@v6
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 pytest-cov
- name: Test with pytest
run: pytest
13 changes: 3 additions & 10 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -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
exclude LICENSE
exclude ISSUE_TEMPLATE.md
exclude PULL_REQUEST_TEMPLATE.md
187 changes: 92 additions & 95 deletions tests/test_autopost.py
Original file line number Diff line number Diff line change
@@ -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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we testing 500s? and if we do, could we please not send them to the actual API, it pollutes my observability. mock an error instead.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been here in upstream. To my knowledge, the tests do not send anything to the actual API. (note the mock API tokens.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to verify, the api token is mocked, not the client

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the MOCK_TOKEN = ".eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=." constant in the test files. The client objects are created from this mock token:

# test_autoposter.py
@pytest.fixture
def autopost(session: ClientSession) -> AutoPoster:
    return AutoPoster(DBLClient(MOCK_TOKEN, session=session))

# test_client.py
@pytest.fixture
def client(session: ClientSession) -> topgg.DBLClient:
    return topgg.DBLClient(MOCK_TOKEN, session=session)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClientSession was the thing to point out I suppose, I don't know enough about how the http module works in python, but it makes sense that if that's mocked it should be fine.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClientSession is an aiohttp object that lets you send HTTP requests via session.get, session.post, etc.


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
43 changes: 27 additions & 16 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,65 @@
import mock
import pytest
from aiohttp import ClientSession

import topgg


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()
9 changes: 3 additions & 6 deletions tests/test_data_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 28 additions & 28 deletions tests/test_ratelimiter.py
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading