Skip to content

Commit 37bc3bb

Browse files
renzonrenzon
authored andcommitted
Created tests for Discord API client
part of #4711
1 parent 10afa76 commit 37bc3bb

4 files changed

Lines changed: 259 additions & 32 deletions

File tree

‎pythonpro/discord/api_client.py‎

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from functools import cached_property
2+
from typing import Union
23

34
import requests
45
from requests import HTTPError
@@ -15,9 +16,8 @@ class UnathorizedOperation(Exception):
1516
pass
1617

1718

18-
class DiscordClient:
19-
def __init__(self, access_token, bot_token=None):
20-
self._bot_token = bot_token
19+
class DiscordAppClient:
20+
def __init__(self, access_token: str):
2121
self._access_token = access_token
2222

2323
@cached_property
@@ -30,15 +30,65 @@ def current_user(self):
3030
discord_user_dict = r.json()
3131
return discord_user_dict
3232

33+
34+
class DiscordBotClient:
35+
def __init__(self, bot_token: str):
36+
self._bot_token = bot_token
37+
38+
def list_guild_members(self, guild_id, limit=1, after=0) -> dict:
39+
"""
40+
:param guild_id: the discord server id
41+
"""
42+
headers = {
43+
'Authorization': f'Bot {self._bot_token}'
44+
}
45+
data = {'limit': limit, 'after': after}
46+
r = requests.get(
47+
f'{_BASE_ENDPOINT_URI}/guilds/{guild_id}/members',
48+
# data=data,
49+
headers=headers)
50+
try:
51+
r.raise_for_status()
52+
except HTTPError as error:
53+
if error.response.status_code == 403:
54+
raise UnathorizedOperation(
55+
f"You have no permission to list member of Guild with id {guild_id}"
56+
f"\nCheck if Guild id is right and you have added your Discord bot on Guild"
57+
) from error
58+
else:
59+
return r.json()
60+
61+
def remove_guild_member(self, guild_id, user_id):
62+
headers = {
63+
'Authorization': f'Bot {self._bot_token}'
64+
}
65+
r = requests.delete(
66+
f'{_BASE_ENDPOINT_URI}/guilds/{guild_id}/members/{user_id}',
67+
headers=headers)
68+
try:
69+
r.raise_for_status()
70+
except HTTPError as error:
71+
if error.response.status_code == 403:
72+
raise UnathorizedOperation(
73+
f"You have no permission to remove member {user_id} of Guild with id {guild_id}"
74+
f"\nCheck if Guild id is right and you have added your Discord bot on Guild"
75+
"\nAlso check that Bot has role bigger than the member you want to kick. Check more info on:"
76+
"\nhttps://discord.com/developers/docs/topics/permissions#permission-hierarchy"
77+
) from error
78+
79+
80+
class DiscordAppAndBotClient(DiscordAppClient, DiscordBotClient):
81+
def __init__(self, access_token: str, bot_token: str):
82+
DiscordAppClient.__init__(self, access_token)
83+
DiscordBotClient.__init__(self, bot_token)
84+
3385
def add_guild_member(self, guild_id) -> dict:
3486
"""
3587
:param guild_id: the discord server id
3688
:return: a dict representing guild's member
3789
3890
If user is already on guild, return a member dict but only with user data.
3991
"""
40-
if self._bot_token is None:
41-
raise MissingBotToken()
4292
current_user = self.current_user
4393
headers = {
4494
'Authorization': f'Bot {self._bot_token}'
@@ -70,7 +120,7 @@ def __init__(self, app_client_id: str, app_client_secret: str, redirect_uri: str
70120
self._app_client_secret = app_client_secret
71121
self._redirect_uri = redirect_uri
72122

73-
def generate_api_client(self, autorization_code: str) -> DiscordClient:
123+
def generate_api_client(self, autorization_code: str) -> Union[DiscordAppClient, DiscordAppAndBotClient]:
74124
data = {
75125
'client_id': self._app_client_id,
76126
'client_secret': self._app_client_secret,
@@ -85,4 +135,12 @@ def generate_api_client(self, autorization_code: str) -> DiscordClient:
85135
r.raise_for_status()
86136
oauth2_data = r.json()
87137
access_token = oauth2_data['access_token']
88-
return DiscordClient(access_token, self._bot_token)
138+
if self._bot_token is None:
139+
return DiscordAppClient(access_token)
140+
return DiscordAppAndBotClient(access_token, self._bot_token)
141+
142+
143+
if __name__ == '__main__':
144+
c = DiscordBotClient('MTAwNDU0MTg3NTk1NDQwNTQzNg.GFcT5X.Kp4gVn0U1kOPvdwLku-oki7LI_wtbMma2E_ET4')
145+
members = c.list_guild_members(971162582624903288)
146+
print(members)

‎pythonpro/discord/tests.py‎

Lines changed: 0 additions & 25 deletions
This file was deleted.

‎pythonpro/discord/tests/__init__.py‎

Whitespace-only changes.
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import pytest
2+
from responses import matchers
3+
4+
from pythonpro.discord.api_client import DiscordCredentials, DiscordAppClient, DiscordAppAndBotClient, DiscordBotClient, UnathorizedOperation
5+
6+
7+
def test_discord_credentials_generate_app_client(responses):
8+
app_client_id = '1004541875954405436'
9+
secret = 'Z5G0I747OrbYWugQRHfUgtcazuC6ArT5'
10+
redirect_url = 'http://localhost:8000/discord/autorize'
11+
discord_credentials = DiscordCredentials(
12+
app_client_id,
13+
secret,
14+
redirect_url
15+
)
16+
authorization_code = 'authorization_code'
17+
credentials_data = {
18+
'client_id': app_client_id,
19+
'client_secret': secret,
20+
'grant_type': 'authorization_code',
21+
'code': authorization_code,
22+
'redirect_uri': redirect_url
23+
}
24+
25+
responses.add(
26+
responses.POST, 'https://discord.com/api/v10/oauth2/token', json=valid_response, status=200,
27+
28+
match=[
29+
matchers.header_matcher({'Content-Type': 'application/x-www-form-urlencoded'}),
30+
matchers.urlencoded_params_matcher(credentials_data)
31+
]
32+
)
33+
34+
client = discord_credentials.generate_api_client(authorization_code)
35+
assert isinstance(client, DiscordAppClient)
36+
37+
38+
def test_discord_credentials_generate_app_and_bot_client(responses):
39+
app_client_id = '1004541875954405436'
40+
secret = 'Z5G0I747OrbYWugQRHfUgtcazuC6ArT5'
41+
redirect_url = 'http://localhost:8000/discord/autorize'
42+
discord_credentials = DiscordCredentials(
43+
app_client_id,
44+
secret,
45+
redirect_url,
46+
'MTAwNDU0MTg3NTk1NDQwNTQzNg.GFcT5X.Kp4gVn0U1kOPvdwLku-oki7LI_wtbMma2E_ET4'
47+
)
48+
authorization_code = 'authorization_code'
49+
credentials_data = {
50+
'client_id': app_client_id,
51+
'client_secret': secret,
52+
'grant_type': 'authorization_code',
53+
'code': authorization_code,
54+
'redirect_uri': redirect_url
55+
}
56+
57+
responses.add(
58+
responses.POST, 'https://discord.com/api/v10/oauth2/token', json=valid_response, status=200,
59+
match=[
60+
matchers.header_matcher({'Content-Type': 'application/x-www-form-urlencoded'}),
61+
matchers.urlencoded_params_matcher(credentials_data)
62+
]
63+
)
64+
65+
client = discord_credentials.generate_api_client(authorization_code)
66+
assert isinstance(client, DiscordAppAndBotClient)
67+
68+
69+
def test_current_user(responses):
70+
access_token = 'access_token'
71+
client = DiscordAppClient(access_token)
72+
responses.add(
73+
responses.GET, 'https://discord.com/api/v10/users/@me', json=valid_user, status=200,
74+
match=[
75+
matchers.header_matcher({'Authorization': f'Bearer {access_token}'}),
76+
]
77+
78+
)
79+
assert client.current_user == valid_user
80+
81+
82+
def test_current_user_in_cache(responses):
83+
access_token = 'access_token'
84+
client = DiscordAppClient(access_token)
85+
responses.add(
86+
responses.GET, 'https://discord.com/api/v10/users/@me', json=valid_user, status=200,
87+
match=[
88+
matchers.header_matcher({'Authorization': f'Bearer {access_token}'}),
89+
]
90+
)
91+
92+
first_current_user = client.current_user
93+
assert len(responses.calls) == 1
94+
second_current_user = client.current_user
95+
assert len(responses.calls) == 1
96+
assert first_current_user is second_current_user
97+
98+
99+
def test_add_guild_member(responses):
100+
access_token = 'access_token'
101+
bot_token = 'MTAwNDU0MTg3NTk1NDQwNTQzNg.GFcT5X.Kp4gVn0U1kOPvdwLku-oki7LI_wtbMma2E_ET4'
102+
guild_id = 971162582624903288
103+
responses.add(
104+
responses.GET, 'https://discord.com/api/v10/users/@me', json=valid_user, status=200,
105+
match=[
106+
matchers.header_matcher({'Authorization': f'Bearer {access_token}'}),
107+
]
108+
109+
)
110+
111+
user_id = valid_user['id']
112+
113+
responses.add(
114+
responses.PUT,
115+
f'https://discord.com/api/v10/guilds/{guild_id}/members/{user_id}',
116+
status=204,
117+
json={'user': valid_user},
118+
match=[
119+
matchers.header_matcher({'Authorization': f'Bot {bot_token}'}),
120+
matchers.json_params_matcher({'access_token': access_token})
121+
]
122+
123+
)
124+
client = DiscordAppAndBotClient(access_token, bot_token)
125+
member_data = client.add_guild_member(guild_id)
126+
127+
assert len(member_data) == 1, 'can not have more data on response'
128+
assert member_data['user'] == valid_user
129+
130+
131+
def test_remove_guild_member(responses):
132+
bot_token = 'MTAwNDU0MTg3NTk1NDQwNTQzNg.GFcT5X.Kp4gVn0U1kOPvdwLku-oki7LI_wtbMma2E_ET4'
133+
bot_client = DiscordBotClient(bot_token)
134+
guild_id = 971162582624903288
135+
user_id = 946364767864504360
136+
137+
responses.add(
138+
responses.DELETE,
139+
f'https://discord.com/api/v10/guilds/{guild_id}/members/{user_id}',
140+
status=204,
141+
match=[
142+
matchers.header_matcher({'Authorization': f'Bot {bot_token}'}),
143+
]
144+
145+
)
146+
147+
bot_client.remove_guild_member(guild_id=guild_id, user_id=user_id)
148+
149+
150+
def test_remove_guild_member_bot_missing_permissionts(responses):
151+
bot_token = 'MTAwNDU0MTg3NTk1NDQwNTQzNg.GFcT5X.Kp4gVn0U1kOPvdwLku-oki7LI_wtbMma2E_ET4'
152+
bot_client = DiscordBotClient(bot_token)
153+
guild_id = 971162582624903288
154+
user_id = 946364767864504360
155+
156+
responses.add(
157+
responses.DELETE,
158+
f'https://discord.com/api/v10/guilds/{guild_id}/members/{user_id}',
159+
status=403,
160+
json=unauthorized_kick_member_response,
161+
match=[
162+
matchers.header_matcher({'Authorization': f'Bot {bot_token}'}),
163+
]
164+
165+
)
166+
with pytest.raises(UnathorizedOperation):
167+
bot_client.remove_guild_member(guild_id=guild_id, user_id=user_id)
168+
169+
170+
valid_response = {
171+
'access_token': 'wAQlcLIjxlaJLoI7DPyMkJec5qki6C',
172+
'expires_in': 604800,
173+
'refresh_token': 'etIsXsLnDdv65lfxqbtxndorPbWpvE',
174+
'scope': 'email', 'token_type': 'Bearer'
175+
}
176+
177+
valid_user = {
178+
'id': '800764510645256242',
179+
'username': 'renzopro',
180+
'avatar': '824db4a9821e21f2609e03e5815c23c0',
181+
'avatar_decoration': None,
182+
'discriminator': '3978',
183+
'public_flags': 0,
184+
'flags': 0,
185+
'banner': None,
186+
'banner_color': None,
187+
'accent_color': None,
188+
'locale': 'pt-BR',
189+
'mfa_enabled': True,
190+
'email': 'renzo@dev.pro.br',
191+
'verified': True
192+
}
193+
194+
unauthorized_kick_member_response = {'message': 'Missing Permissions', 'code': 50013}

0 commit comments

Comments
 (0)