Skip to content

Commit e4c8578

Browse files
committed
ci: add more tests
1 parent 42f1efe commit e4c8578

File tree

6 files changed

+281
-16
lines changed

6 files changed

+281
-16
lines changed

tests/mocks/compare_bots.json

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
"data": [
3+
{
4+
"id": "432610292342587392",
5+
"topGGId": "432610292342587392",
6+
"owners": [
7+
"275748765166469120"
8+
],
9+
"deleted": false,
10+
"name": "Mudae",
11+
"def_avatar": "1.png",
12+
"short_desc": "Database of 135,000 anime/game characters: make and customize the best collection in your server. 400 commands, multiplayer games and more!",
13+
"prefix": "$",
14+
"approved_at": "2018-04-21T21:17:57.793Z",
15+
"monthly_votes": 876511,
16+
"server_count": 3371839,
17+
"total_votes": 243100324,
18+
"review_count": 13805,
19+
"monthly_votes_rank": 1,
20+
"server_count_rank": 15,
21+
"total_votes_rank": 1,
22+
"timestamp": "2025-12-11T19:00:00Z",
23+
"unix_timestamp": "1765479600000",
24+
"tags": [
25+
"anime",
26+
"database",
27+
"fun",
28+
"game"
29+
],
30+
"avg_review_rating": 4.505686345526983,
31+
"percentage_changes": {
32+
"daily": 0.32,
33+
"monthly": 0.89
34+
},
35+
"certified": false,
36+
"avatar": "https://cdn.discordapp.com/avatars/432610292342587392/29cb28fbf65a3958105026ab03abd306.webp?size=256",
37+
"lib": "",
38+
"website": "https://www.patreon.com/mudae"
39+
},
40+
{
41+
"id": "1026525568344264724",
42+
"topGGId": "1026525568344264724",
43+
"owners": [
44+
"121919449996460033"
45+
],
46+
"deleted": false,
47+
"name": "Top.gg Lib Dev API Access",
48+
"def_avatar": "1.png",
49+
"short_desc": "API access for Top.gg Library Developers",
50+
"prefix": "/",
51+
"approved_at": "2022-10-03T16:08:55.292Z",
52+
"monthly_votes": 0,
53+
"server_count": 2,
54+
"total_votes": 28,
55+
"review_count": 2,
56+
"monthly_votes_rank": 3036,
57+
"server_count_rank": 9615,
58+
"total_votes_rank": 16004,
59+
"timestamp": "2025-12-11T19:00:00Z",
60+
"unix_timestamp": "1765479600000",
61+
"tags": [
62+
"api",
63+
"library",
64+
"topgg"
65+
],
66+
"avg_review_rating": 5,
67+
"percentage_changes": {
68+
"daily": 0,
69+
"monthly": 0
70+
},
71+
"certified": false,
72+
"avatar": "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png",
73+
"lib": "",
74+
"website": ""
75+
}
76+
]
77+
}

tests/mocks/get_users_bot.json

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
{
2+
"bots": [
3+
{
4+
"id": "896713090782068749",
5+
"owners": [
6+
"121919449996460033"
7+
],
8+
"deleted": false,
9+
"name": "Partake Bot",
10+
"avatar": "https://external-cdn.top.gg/discord/bots/896713090782068749/07517480ffe63b53ca5d75d0ee1b28f4.webp",
11+
"short_desc": "Find your virtual place to be - Experience the true end-game with the easiest way to discover and plan social events for Final Fantasy XIV",
12+
"lib": "",
13+
"prefix": "pv!",
14+
"website": "https://partyverse.app/",
15+
"approved_at": "2022-02-20T03:58:18.086Z",
16+
"monthly_votes": 0,
17+
"server_count": 0,
18+
"total_votes": 17,
19+
"monthly_votes_rank": 40750,
20+
"server_count_rank": 41342,
21+
"total_votes_rank": 20188,
22+
"timestamp": "2025-12-11T19:00:00Z",
23+
"unix_timestamp": "1765479600000"
24+
},
25+
{
26+
"id": "422087909634736160",
27+
"owners": [
28+
"121919449996460033"
29+
],
30+
"deleted": false,
31+
"name": "Top.gg",
32+
"avatar": "https://cdn.discordapp.com/avatars/422087909634736160/d41e1166aadbba1fd62f6c43e2a15777.webp?size=256",
33+
"short_desc": "The official Discord bot for Top.gg!",
34+
"lib": "",
35+
"prefix": "N/A",
36+
"website": "https://top.gg/discord/servers",
37+
"approved_at": "2018-03-31T09:45:37.000Z",
38+
"monthly_votes": 3,
39+
"server_count": 260000,
40+
"total_votes": 8604,
41+
"monthly_votes_rank": 1480,
42+
"server_count_rank": 105,
43+
"total_votes_rank": 742,
44+
"timestamp": "2025-12-11T19:00:00Z",
45+
"unix_timestamp": "1765479600000"
46+
},
47+
{
48+
"id": "1027977768350257173",
49+
"owners": [
50+
"121919449996460033"
51+
],
52+
"deleted": true,
53+
"name": "Miki Anime",
54+
"avatar": "https://cdn.discordapp.com/avatars/1027977768350257173/287aec0ad8a65ce3ca23400571401560.png",
55+
"short_desc": "1027977768350257173",
56+
"lib": "",
57+
"prefix": "2",
58+
"website": "",
59+
"approved_at": "2025-02-25T03:02:25.000Z",
60+
"monthly_votes": 0,
61+
"server_count": 0,
62+
"total_votes": 0,
63+
"monthly_votes_rank": 5064,
64+
"server_count_rank": 10395,
65+
"total_votes_rank": 41297,
66+
"timestamp": "2025-02-25T21:00:00.000Z",
67+
"unix_timestamp": "1740517200000"
68+
},
69+
{
70+
"id": "1026525568344264724",
71+
"owners": [
72+
"121919449996460033"
73+
],
74+
"deleted": false,
75+
"name": "Top.gg Lib Dev API Access",
76+
"avatar": "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png",
77+
"short_desc": "API access for Top.gg Library Developers",
78+
"lib": "",
79+
"prefix": "/",
80+
"website": "",
81+
"approved_at": "2022-10-03T16:08:55.292Z",
82+
"monthly_votes": 0,
83+
"server_count": 2,
84+
"total_votes": 28,
85+
"monthly_votes_rank": 3036,
86+
"server_count_rank": 9615,
87+
"total_votes_rank": 16004,
88+
"timestamp": "2025-12-11T19:00:00Z",
89+
"unix_timestamp": "1765479600000"
90+
},
91+
{
92+
"id": "160105994217586689",
93+
"owners": [
94+
"121919449996460033"
95+
],
96+
"deleted": false,
97+
"name": "Miki",
98+
"avatar": "https://cdn.discordapp.com/avatars/160105994217586689/c49b5a905659a3793965a97cc6a6e6e9.webp?size=256",
99+
"short_desc": "A great bot with tons of features! language | admin | cards | fun | levels | roles | marriage | currency | custom commands! ",
100+
"lib": "",
101+
"prefix": "/ (or @mention) ",
102+
"website": "http://miki.bot",
103+
"approved_at": "2017-04-26T00:06:40.000Z",
104+
"monthly_votes": 1344,
105+
"server_count": 290000,
106+
"total_votes": 313613,
107+
"monthly_votes_rank": 123,
108+
"server_count_rank": 93,
109+
"total_votes_rank": 131,
110+
"timestamp": "2025-12-11T19:00:00Z",
111+
"unix_timestamp": "1765479600000"
112+
}
113+
]
114+
}

tests/test_client.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ def test_Client_attributes_work(client: topstats.Client) -> None:
3636
_test_attributes(client)
3737

3838

39+
@pytest.mark.asyncio
40+
async def test_Client_error_handling_works() -> None:
41+
with pytest.raises(TypeError, match='^An API token is required to use this API.$'):
42+
async with topstats.Client(''):
43+
pass
44+
45+
with pytest.raises(topstats.Error, match='^Client session is already closed.$'):
46+
token = getenv('TOPSTATS_TOKEN')
47+
48+
if TYPE_CHECKING:
49+
assert token is not None
50+
51+
test_client = topstats.Client(token)
52+
53+
await test_client.close()
54+
await test_client.get_bot(432610292342587392)
55+
56+
3957
@pytest.mark.asyncio
4058
async def test_Client_get_bot_works(
4159
monkeypatch: pytest.MonkeyPatch,
@@ -67,6 +85,22 @@ async def test_Client_get_top_bots_works(
6785
request.assert_called_once()
6886

6987

88+
@pytest.mark.asyncio
89+
async def test_Client_get_users_bot_works(
90+
monkeypatch: pytest.MonkeyPatch,
91+
client: topstats.Client,
92+
) -> None:
93+
with RequestMock(200, 'OK', 'mocks/get_users_bot.json') as request:
94+
monkeypatch.setattr('aiohttp.ClientSession.get', request)
95+
96+
bots = await client.get_users_bot(121919449996460033)
97+
98+
for bot in bots:
99+
_test_attributes(bot)
100+
101+
request.assert_called_once()
102+
103+
70104
@pytest.mark.asyncio
71105
async def test_Client_get_recent_bot_stats_works(
72106
monkeypatch: pytest.MonkeyPatch,
@@ -103,6 +137,11 @@ async def test_Client_search_bots_by_tag_works(
103137
monkeypatch: pytest.MonkeyPatch,
104138
client: topstats.Client,
105139
) -> None:
140+
with pytest.raises(
141+
topstats.Error, match='^Either a bot name or tag must be specified.$'
142+
):
143+
await client.search_bots()
144+
106145
with RequestMock(200, 'OK', 'mocks/search_bots_by_tag.json') as request:
107146
monkeypatch.setattr('aiohttp.ClientSession.get', request)
108147

@@ -133,11 +172,36 @@ async def test_Client_get_historical_bot_works(
133172
request.assert_called_once()
134173

135174

175+
@pytest.mark.asyncio
176+
async def test_Client_compare_bot_works(
177+
monkeypatch: pytest.MonkeyPatch, client: topstats.Client
178+
) -> None:
179+
with pytest.raises(
180+
IndexError, match='^Expected 2 to 4 unique bot IDs to compare, but got '
181+
):
182+
await client.compare_bots()
183+
184+
with pytest.raises(
185+
IndexError, match='^Expected 2 to 4 unique bot IDs to compare, but got '
186+
):
187+
await client.compare_bots(1026525568344264724)
188+
189+
with RequestMock(200, 'OK', 'mocks/compare_bots.json') as request:
190+
monkeypatch.setattr('aiohttp.ClientSession.get', request)
191+
192+
bots = await client.compare_bots(1026525568344264724, 432610292342587392)
193+
194+
for bot in bots:
195+
_test_attributes(bot)
196+
197+
request.assert_called_once()
198+
199+
136200
@pytest.mark.parametrize(
137201
'ty', ('monthly_votes', 'review_count', 'server_count', 'total_votes')
138202
)
139203
@pytest.mark.asyncio
140-
async def test_Client_compare_bot_works(
204+
async def test_Client_specific_compare_bot_works(
141205
monkeypatch: pytest.MonkeyPatch, client: topstats.Client, ty: str
142206
) -> None:
143207
with RequestMock(200, 'OK', f'mocks/compare_bot_{ty}.json') as request:

topstats/bot.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class BotStats:
4848
server_count: Ranked
4949
"""The amount servers this bot is in."""
5050

51-
review_count: Ranked
51+
review_count: Optional[Ranked]
5252
"""The amount reviews this bot has."""
5353

5454
def __init__(self, json: dict):
@@ -124,7 +124,7 @@ def __eq__(self, other: object) -> bool:
124124
if isinstance(other, __class__):
125125
return self.id == other.id
126126

127-
return NotImplemented
127+
return NotImplemented # pragma: nocover
128128

129129
@property
130130
def created_at(self) -> datetime:
@@ -153,7 +153,7 @@ class Bot(PartialBot):
153153
'monthly_difference',
154154
)
155155

156-
topgg_id: int
156+
topgg_id: Optional[int]
157157
"""This bot's Top.gg ID."""
158158

159159
owners: list[int]
@@ -190,7 +190,11 @@ class Bot(PartialBot):
190190
"""Difference percentage from the previous month."""
191191

192192
def __init__(self, json: dict):
193-
self.topgg_id = int(json['topGGId'])
193+
if topgg_id := json.get('topGGId'):
194+
self.topgg_id = topgg_id
195+
else:
196+
self.topgg_id = None
197+
194198
self.owners = [int(i) for i in (json.get('owners') or ())]
195199
self.tags = json.get('tags') or []
196200
self.is_deleted = json['deleted']
@@ -210,7 +214,7 @@ def __init__(self, json: dict):
210214

211215
self.daily_difference = daily and float(daily)
212216
self.monthly_difference = monthly and float(monthly)
213-
else:
217+
else: # pragma: nocover
214218
self.daily_difference = None
215219
self.monthly_difference = None
216220

topstats/client.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ async def __get(
124124
)
125125
current_ratelimit = getattr(self.__current_ratelimits, ratelimiter_key)
126126

127-
if current_ratelimit is not None:
127+
if current_ratelimit is not None: # pragma: nocover
128128
current_time = time()
129129

130130
if current_time < current_ratelimit:
@@ -159,13 +159,15 @@ async def __get(
159159
try:
160160
output = await resp.json()
161161
retry_after = float(output.get('expiresIn', 0)) / 1000.0
162-
except (ValueError, json.decoder.JSONDecodeError):
162+
except (ValueError, json.decoder.JSONDecodeError): # pragma: nocover
163163
pass
164164

165165
resp.raise_for_status()
166166

167+
print(json.dumps(output, indent=2))
168+
167169
return output
168-
except ClientResponseError:
170+
except ClientResponseError: # pragma: nocover
169171
if status == 429 and retry_after is not None:
170172
if retry_after > MAXIMUM_DELAY_THRESHOLD:
171173
setattr(self.__current_ratelimits, ratelimiter_key, time() + retry_after)

0 commit comments

Comments
 (0)