mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-09 12:30:15 +02:00
Fix managed roles and reserved groups bugs in Discord Service and more
This commit is contained in:
parent
22a270aedb
commit
d7fabccddd
@ -3,16 +3,17 @@ from urllib.parse import parse_qs
|
||||
|
||||
import requests_mock
|
||||
|
||||
from django.test import TestCase, override_settings
|
||||
from django.test import override_settings
|
||||
|
||||
from allianceauth.analytics.tasks import ANALYTICS_URL
|
||||
from allianceauth.eveonline.tasks import update_character
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
@requests_mock.mock()
|
||||
class TestAnalyticsForViews(TestCase):
|
||||
class TestAnalyticsForViews(NoSocketsTestCase):
|
||||
@override_settings(ANALYTICS_DISABLED=False)
|
||||
def test_should_run_analytics(self, requests_mocker):
|
||||
# given
|
||||
@ -40,7 +41,7 @@ class TestAnalyticsForViews(TestCase):
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
@requests_mock.mock()
|
||||
class TestAnalyticsForTasks(TestCase):
|
||||
class TestAnalyticsForTasks(NoSocketsTestCase):
|
||||
@override_settings(ANALYTICS_DISABLED=False)
|
||||
@patch("allianceauth.eveonline.models.EveCharacter.objects.update_character")
|
||||
def test_should_run_analytics_for_successful_task(
|
||||
|
@ -1,12 +1,22 @@
|
||||
import requests_mock
|
||||
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from allianceauth.analytics.tasks import (
|
||||
analytics_event,
|
||||
send_ga_tracking_celery_event,
|
||||
send_ga_tracking_web_view)
|
||||
from django.test.testcases import TestCase
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
|
||||
class TestAnalyticsTasks(TestCase):
|
||||
def test_analytics_event(self):
|
||||
GOOGLE_ANALYTICS_DEBUG_URL = 'https://www.google-analytics.com/debug/collect'
|
||||
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||
@requests_mock.Mocker()
|
||||
class TestAnalyticsTasks(NoSocketsTestCase):
|
||||
def test_analytics_event(self, requests_mocker):
|
||||
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
|
||||
analytics_event(
|
||||
category='allianceauth.analytics',
|
||||
action='send_tests',
|
||||
@ -14,15 +24,19 @@ class TestAnalyticsTasks(TestCase):
|
||||
value=1,
|
||||
event_type='Stats')
|
||||
|
||||
def test_send_ga_tracking_web_view_sent(self):
|
||||
# This test sends if the event SENDS to google
|
||||
# Not if it was successful
|
||||
def test_send_ga_tracking_web_view_sent(self, requests_mocker):
|
||||
"""This test sends if the event SENDS to google.
|
||||
Not if it was successful.
|
||||
"""
|
||||
# given
|
||||
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
|
||||
tracking_id = 'UA-186249766-2'
|
||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||
page = '/index/'
|
||||
title = 'Hello World'
|
||||
locale = 'en'
|
||||
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||
# when
|
||||
response = send_ga_tracking_web_view(
|
||||
tracking_id,
|
||||
client_id,
|
||||
@ -30,15 +44,23 @@ class TestAnalyticsTasks(TestCase):
|
||||
title,
|
||||
locale,
|
||||
useragent)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_send_ga_tracking_web_view_success(self):
|
||||
def test_send_ga_tracking_web_view_success(self, requests_mocker):
|
||||
# given
|
||||
requests_mocker.register_uri(
|
||||
'POST',
|
||||
GOOGLE_ANALYTICS_DEBUG_URL,
|
||||
json={"hitParsingResult":[{'valid': True}]}
|
||||
)
|
||||
tracking_id = 'UA-186249766-2'
|
||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||
page = '/index/'
|
||||
title = 'Hello World'
|
||||
locale = 'en'
|
||||
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||
# when
|
||||
json_response = send_ga_tracking_web_view(
|
||||
tracking_id,
|
||||
client_id,
|
||||
@ -46,15 +68,42 @@ class TestAnalyticsTasks(TestCase):
|
||||
title,
|
||||
locale,
|
||||
useragent).json()
|
||||
# then
|
||||
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
||||
|
||||
def test_send_ga_tracking_web_view_invalid_token(self):
|
||||
def test_send_ga_tracking_web_view_invalid_token(self, requests_mocker):
|
||||
# given
|
||||
requests_mocker.register_uri(
|
||||
'POST',
|
||||
GOOGLE_ANALYTICS_DEBUG_URL,
|
||||
json={
|
||||
"hitParsingResult":[
|
||||
{
|
||||
'valid': False,
|
||||
'parserMessage': [
|
||||
{
|
||||
'messageType': 'INFO',
|
||||
'description': 'IP Address from this hit was anonymized to 1.132.110.0.',
|
||||
'messageCode': 'VALUE_MODIFIED'
|
||||
},
|
||||
{
|
||||
'messageType': 'ERROR',
|
||||
'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.",
|
||||
'messageCode': 'VALUE_INVALID', 'parameter': 'tid'
|
||||
}
|
||||
],
|
||||
'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
tracking_id = 'UA-IntentionallyBadTrackingID-2'
|
||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||
page = '/index/'
|
||||
title = 'Hello World'
|
||||
locale = 'en'
|
||||
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
|
||||
# when
|
||||
json_response = send_ga_tracking_web_view(
|
||||
tracking_id,
|
||||
client_id,
|
||||
@ -62,18 +111,25 @@ class TestAnalyticsTasks(TestCase):
|
||||
title,
|
||||
locale,
|
||||
useragent).json()
|
||||
# then
|
||||
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
|
||||
self.assertEqual(json_response["hitParsingResult"][0]["parserMessage"][1]["description"], "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.")
|
||||
self.assertEqual(
|
||||
json_response["hitParsingResult"][0]["parserMessage"][1]["description"],
|
||||
"The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details."
|
||||
)
|
||||
|
||||
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]
|
||||
|
||||
def test_send_ga_tracking_celery_event_sent(self):
|
||||
def test_send_ga_tracking_celery_event_sent(self, requests_mocker):
|
||||
# given
|
||||
requests_mocker.register_uri('POST', GOOGLE_ANALYTICS_DEBUG_URL)
|
||||
tracking_id = 'UA-186249766-2'
|
||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||
category = 'test'
|
||||
action = 'test'
|
||||
label = 'test'
|
||||
value = '1'
|
||||
# when
|
||||
response = send_ga_tracking_celery_event(
|
||||
tracking_id,
|
||||
client_id,
|
||||
@ -81,15 +137,23 @@ class TestAnalyticsTasks(TestCase):
|
||||
action,
|
||||
label,
|
||||
value)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_send_ga_tracking_celery_event_success(self):
|
||||
def test_send_ga_tracking_celery_event_success(self, requests_mocker):
|
||||
# given
|
||||
requests_mocker.register_uri(
|
||||
'POST',
|
||||
GOOGLE_ANALYTICS_DEBUG_URL,
|
||||
json={"hitParsingResult":[{'valid': True}]}
|
||||
)
|
||||
tracking_id = 'UA-186249766-2'
|
||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||
category = 'test'
|
||||
action = 'test'
|
||||
label = 'test'
|
||||
value = '1'
|
||||
# when
|
||||
json_response = send_ga_tracking_celery_event(
|
||||
tracking_id,
|
||||
client_id,
|
||||
@ -97,15 +161,42 @@ class TestAnalyticsTasks(TestCase):
|
||||
action,
|
||||
label,
|
||||
value).json()
|
||||
# then
|
||||
self.assertTrue(json_response["hitParsingResult"][0]["valid"])
|
||||
|
||||
def test_send_ga_tracking_celery_event_invalid_token(self):
|
||||
def test_send_ga_tracking_celery_event_invalid_token(self, requests_mocker):
|
||||
# given
|
||||
requests_mocker.register_uri(
|
||||
'POST',
|
||||
GOOGLE_ANALYTICS_DEBUG_URL,
|
||||
json={
|
||||
"hitParsingResult":[
|
||||
{
|
||||
'valid': False,
|
||||
'parserMessage': [
|
||||
{
|
||||
'messageType': 'INFO',
|
||||
'description': 'IP Address from this hit was anonymized to 1.132.110.0.',
|
||||
'messageCode': 'VALUE_MODIFIED'
|
||||
},
|
||||
{
|
||||
'messageType': 'ERROR',
|
||||
'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.",
|
||||
'messageCode': 'VALUE_INVALID', 'parameter': 'tid'
|
||||
}
|
||||
],
|
||||
'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
tracking_id = 'UA-IntentionallyBadTrackingID-2'
|
||||
client_id = 'ab33e241fbf042b6aa77c7655a768af7'
|
||||
category = 'test'
|
||||
action = 'test'
|
||||
label = 'test'
|
||||
value = '1'
|
||||
# when
|
||||
json_response = send_ga_tracking_celery_event(
|
||||
tracking_id,
|
||||
client_id,
|
||||
@ -113,7 +204,9 @@ class TestAnalyticsTasks(TestCase):
|
||||
action,
|
||||
label,
|
||||
value).json()
|
||||
# then
|
||||
self.assertFalse(json_response["hitParsingResult"][0]["valid"])
|
||||
self.assertEqual(json_response["hitParsingResult"][0]["parserMessage"][1]["description"], "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.")
|
||||
|
||||
# [{'valid': False, 'parserMessage': [{'messageType': 'INFO', 'description': 'IP Address from this hit was anonymized to 1.132.110.0.', 'messageCode': 'VALUE_MODIFIED'}, {'messageType': 'ERROR', 'description': "The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details.", 'messageCode': 'VALUE_INVALID', 'parameter': 'tid'}], 'hit': '/debug/collect?v=1&tid=UA-IntentionallyBadTrackingID-2&cid=ab33e241fbf042b6aa77c7655a768af7&t=pageview&dp=/index/&dt=Hello World&ul=en&ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36&aip=1&an=allianceauth&av=2.9.0a2'}]
|
||||
self.assertEqual(
|
||||
json_response["hitParsingResult"][0]["parserMessage"][1]["description"],
|
||||
"The value provided for parameter 'tid' is invalid. Please see http://goo.gl/a8d4RP#tid for details."
|
||||
)
|
||||
|
@ -3,11 +3,11 @@ from django.contrib import admin
|
||||
|
||||
from allianceauth import hooks
|
||||
from allianceauth.authentication.admin import (
|
||||
MainAllianceFilter,
|
||||
MainCorporationsFilter,
|
||||
user_main_organization,
|
||||
user_profile_pic,
|
||||
user_username,
|
||||
user_main_organization,
|
||||
MainCorporationsFilter,
|
||||
MainAllianceFilter
|
||||
)
|
||||
|
||||
from .models import NameFormatConfig
|
||||
@ -36,19 +36,18 @@ class ServicesUserAdmin(admin.ModelAdmin):
|
||||
MainAllianceFilter,
|
||||
'user__date_joined',
|
||||
)
|
||||
list_select_related = (
|
||||
'user', 'user__profile__main_character', 'user__profile__state'
|
||||
)
|
||||
|
||||
@admin.display(ordering='user__profile__state__name')
|
||||
def _state(self, obj):
|
||||
return obj.user.profile.state.name
|
||||
|
||||
_state.short_description = 'state'
|
||||
_state.admin_order_field = 'user__profile__state__name'
|
||||
|
||||
@admin.display(ordering='user__date_joined')
|
||||
def _date_joined(self, obj):
|
||||
return obj.user.date_joined
|
||||
|
||||
_date_joined.short_description = 'date joined'
|
||||
_date_joined.admin_order_field = 'user__date_joined'
|
||||
|
||||
|
||||
class NameFormatConfigForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -62,6 +61,7 @@ class NameFormatConfigForm(forms.ModelForm):
|
||||
self.fields['service_name'] = forms.ChoiceField(choices=SERVICE_CHOICES)
|
||||
|
||||
|
||||
@admin.register(NameFormatConfig)
|
||||
class NameFormatConfigAdmin(admin.ModelAdmin):
|
||||
form = NameFormatConfigForm
|
||||
list_display = ('service_name', 'get_state_display_string')
|
||||
@ -69,6 +69,3 @@ class NameFormatConfigAdmin(admin.ModelAdmin):
|
||||
def get_state_display_string(self, obj):
|
||||
return ', '.join([state.name for state in obj.states.all()])
|
||||
get_state_display_string.short_description = 'States'
|
||||
|
||||
|
||||
admin.site.register(NameFormatConfig, NameFormatConfigAdmin)
|
||||
|
@ -2,12 +2,11 @@ import logging
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from . import __title__
|
||||
from ...admin import ServicesUserAdmin
|
||||
from . import __title__
|
||||
from .models import DiscordUser
|
||||
from .utils import LoggerAddTag
|
||||
|
||||
|
||||
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
||||
|
||||
|
||||
@ -18,21 +17,16 @@ class DiscordUserAdmin(ServicesUserAdmin):
|
||||
list_filter = ServicesUserAdmin.list_filter + ('activated',)
|
||||
ordering = ('-activated',)
|
||||
|
||||
def _uid(self, obj):
|
||||
return obj.uid
|
||||
|
||||
_uid.short_description = 'Discord ID (UID)'
|
||||
_uid.admin_order_field = 'uid'
|
||||
|
||||
def _username(self, obj):
|
||||
if obj.username and obj.discriminator:
|
||||
return f'{obj.username}#{obj.discriminator}'
|
||||
else:
|
||||
return ''
|
||||
|
||||
def delete_queryset(self, request, queryset):
|
||||
for user in queryset:
|
||||
user.delete_user()
|
||||
|
||||
_username.short_description = 'Discord Username'
|
||||
_username.admin_order_field = 'username'
|
||||
@admin.display(description='Discord ID (UID)', ordering='uid')
|
||||
def _uid(self, obj):
|
||||
return obj.uid
|
||||
|
||||
@admin.display(description='Discord Username', ordering='username')
|
||||
def _username(self, obj):
|
||||
if obj.username and obj.discriminator:
|
||||
return f'{obj.username}#{obj.discriminator}'
|
||||
return ''
|
||||
|
37
allianceauth/services/modules/discord/api.py
Normal file
37
allianceauth/services/modules/discord/api.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Public interface for community apps who want to interact with the Discord server
|
||||
of the current Alliance Auth instance.
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
Here is an example for using the api to fetch the current roles from the configured Discord server.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from allianceauth.services.modules.discord.api import create_bot_client, discord_guild_id
|
||||
|
||||
client = create_bot_client() # create a new Discord client
|
||||
guild_id = discord_guild_id() # get the ID of the configured Discord server
|
||||
roles = client.guild_roles(guild_id) # fetch the roles from our Discord server
|
||||
|
||||
.. seealso::
|
||||
The docs for the client class can be found here: :py:class:`~allianceauth.services.modules.discord.discord_client.client.DiscordClient`
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .app_settings import DISCORD_GUILD_ID
|
||||
from .core import create_bot_client, group_to_role, server_name # noqa
|
||||
from .discord_client.models import Role # noqa
|
||||
from .models import DiscordUser # noqa
|
||||
|
||||
__all__ = ["create_bot_client", "group_to_role", "server_name", "DiscordUser", "Role"]
|
||||
|
||||
|
||||
def discord_guild_id() -> Optional[int]:
|
||||
"""Guild ID of configured Discord server.
|
||||
|
||||
Returns:
|
||||
Guild ID or ``None`` if not configured
|
||||
"""
|
||||
return int(DISCORD_GUILD_ID) if DISCORD_GUILD_ID else None
|
@ -2,16 +2,25 @@ from .utils import clean_setting
|
||||
|
||||
|
||||
DISCORD_APP_ID = clean_setting('DISCORD_APP_ID', '')
|
||||
"""App ID for the AA bot on Discord. Needs to be set."""
|
||||
|
||||
DISCORD_APP_SECRET = clean_setting('DISCORD_APP_SECRET', '')
|
||||
"""App secret for the AA bot on Discord. Needs to be set."""
|
||||
|
||||
DISCORD_BOT_TOKEN = clean_setting('DISCORD_BOT_TOKEN', '')
|
||||
"""Token used by the AA bot on Discord. Needs to be set."""
|
||||
|
||||
DISCORD_CALLBACK_URL = clean_setting('DISCORD_CALLBACK_URL', '')
|
||||
"""Callback URL for OAuth with Discord. Needs to be set."""
|
||||
|
||||
DISCORD_GUILD_ID = clean_setting('DISCORD_GUILD_ID', '')
|
||||
"""ID of the Discord Server. Needs to be set."""
|
||||
|
||||
# max retries of tasks after an error occurred
|
||||
DISCORD_TASKS_MAX_RETRIES = clean_setting('DISCORD_TASKS_MAX_RETRIES', 3)
|
||||
"""Max retries of tasks after an error occurred."""
|
||||
|
||||
# Pause in seconds until next retry for tasks after the API returned an error
|
||||
DISCORD_TASKS_RETRY_PAUSE = clean_setting('DISCORD_TASKS_RETRY_PAUSE', 60)
|
||||
"""Pause in seconds until next retry for tasks after the API returned an error."""
|
||||
|
||||
# automatically sync Discord users names to user's main character name when created
|
||||
DISCORD_SYNC_NAMES = clean_setting('DISCORD_SYNC_NAMES', False)
|
||||
"""Automatically sync Discord users names to user's main character name when created."""
|
||||
|
@ -6,6 +6,7 @@ from django.template.loader import render_to_string
|
||||
from allianceauth import hooks
|
||||
from allianceauth.services.hooks import ServicesHook
|
||||
|
||||
from .core import server_name, user_formatted_nick
|
||||
from .models import DiscordUser
|
||||
from .urls import urlpatterns
|
||||
from .utils import LoggerAddTag
|
||||
@ -53,7 +54,7 @@ class DiscordService(ServicesHook):
|
||||
return render_to_string(
|
||||
self.service_ctrl_template,
|
||||
{
|
||||
'server_name': DiscordUser.objects.server_name(),
|
||||
'server_name': server_name(),
|
||||
'user_has_account': user_has_account,
|
||||
'discord_username': discord_username
|
||||
},
|
||||
@ -73,7 +74,7 @@ class DiscordService(ServicesHook):
|
||||
'user_pk': user.pk,
|
||||
# since the new nickname is not yet in the DB we need to
|
||||
# provide it manually to the task
|
||||
'nickname': DiscordUser.objects.user_formatted_nick(user)
|
||||
'nickname': user_formatted_nick(user)
|
||||
},
|
||||
priority=SINGLE_TASK_PRIORITY
|
||||
)
|
||||
|
129
allianceauth/services/modules/discord/core.py
Normal file
129
allianceauth/services/modules/discord/core.py
Normal file
@ -0,0 +1,129 @@
|
||||
"""Core functionality of the Discord service not directly related to models."""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from django.contrib.auth.models import Group, User
|
||||
|
||||
from allianceauth.groupmanagement.models import ReservedGroupName
|
||||
from allianceauth.services.hooks import NameFormatter
|
||||
|
||||
from . import __title__
|
||||
from .app_settings import DISCORD_BOT_TOKEN, DISCORD_GUILD_ID
|
||||
from .discord_client import DiscordClient, RolesSet, Role
|
||||
from .discord_client.exceptions import DiscordClientException
|
||||
from .utils import LoggerAddTag
|
||||
|
||||
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
||||
|
||||
|
||||
def create_bot_client(is_rate_limited: bool = True) -> DiscordClient:
|
||||
"""Create new bot client for accessing the configured Discord server.
|
||||
|
||||
Args:
|
||||
is_rate_limited: Set to False to turn off rate limiting (use with care).
|
||||
|
||||
Return:
|
||||
Discord client instance
|
||||
"""
|
||||
return DiscordClient(DISCORD_BOT_TOKEN, is_rate_limited=is_rate_limited)
|
||||
|
||||
|
||||
def calculate_roles_for_user(
|
||||
user: User,
|
||||
client: DiscordClient,
|
||||
discord_uid: int,
|
||||
state_name: str = None,
|
||||
) -> Tuple[RolesSet, Optional[bool]]:
|
||||
"""Calculate current Discord roles for an Auth user.
|
||||
|
||||
Takes into account reserved groups and existing managed roles (e.g. nitro).
|
||||
|
||||
Returns:
|
||||
- Discord roles, changed flag:
|
||||
- True when roles have changed,
|
||||
- False when they have not changed,
|
||||
- None if user is not a member of the guild
|
||||
"""
|
||||
roles_calculated = client.match_or_create_roles_from_names_2(
|
||||
guild_id=DISCORD_GUILD_ID,
|
||||
role_names=_user_group_names(user=user, state_name=state_name),
|
||||
)
|
||||
logger.debug("Calculated roles for user %s: %s", user, roles_calculated.ids())
|
||||
roles_current = client.guild_member_roles(
|
||||
guild_id=DISCORD_GUILD_ID, user_id=discord_uid
|
||||
)
|
||||
if roles_current is None:
|
||||
logger.debug("User %s is not a member of the guild.", user)
|
||||
return roles_calculated, None
|
||||
logger.debug("Current roles user %s: %s", user, roles_current.ids())
|
||||
reserved_role_names = ReservedGroupName.objects.values_list("name", flat=True)
|
||||
roles_reserved = roles_current.subset(role_names=reserved_role_names)
|
||||
roles_managed = roles_current.subset(managed_only=True)
|
||||
roles_persistent = roles_managed.union(roles_reserved)
|
||||
if roles_calculated == roles_current.difference(roles_persistent):
|
||||
return roles_calculated, False
|
||||
return roles_calculated.union(roles_persistent), True
|
||||
|
||||
|
||||
def _user_group_names(user: User, state_name: str = None) -> List[str]:
|
||||
"""Names of groups and state the given user is a member of."""
|
||||
if not state_name:
|
||||
state_name = user.profile.state.name
|
||||
group_names = [group.name for group in user.groups.all()] + [state_name]
|
||||
logger.debug("Group names for roles updates of user %s are: %s", user, group_names)
|
||||
return group_names
|
||||
|
||||
|
||||
def user_formatted_nick(user: User) -> Optional[str]:
|
||||
"""Name of the given user's main character with name formatting applied.
|
||||
|
||||
Returns:
|
||||
Name or ``None`` if user has no main.
|
||||
"""
|
||||
from .auth_hooks import DiscordService
|
||||
|
||||
if user.profile.main_character:
|
||||
return NameFormatter(DiscordService(), user).format_name()
|
||||
return None
|
||||
|
||||
|
||||
def group_to_role(group: Group) -> Optional[Role]:
|
||||
"""Fetch the Discord role matching the given Django group by name.
|
||||
|
||||
Returns:
|
||||
Discord role or None if no matching role exist
|
||||
"""
|
||||
return default_bot_client.match_role_from_name(
|
||||
guild_id=DISCORD_GUILD_ID, role_name=group.name
|
||||
)
|
||||
|
||||
|
||||
def server_name(use_cache: bool = True) -> str:
|
||||
"""Fetches the name of the current Discord server.
|
||||
|
||||
Args:
|
||||
use_cache: When set False will force an API call to get the server name
|
||||
|
||||
Returns:
|
||||
Server name or an empty string if the name could not be retrieved
|
||||
"""
|
||||
try:
|
||||
server_name = default_bot_client.guild_name(
|
||||
guild_id=DISCORD_GUILD_ID, use_cache=use_cache
|
||||
)
|
||||
except (HTTPError, DiscordClientException):
|
||||
server_name = ""
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Unexpected error when trying to retrieve the server name from Discord",
|
||||
exc_info=True,
|
||||
)
|
||||
server_name = ""
|
||||
return server_name
|
||||
|
||||
|
||||
# Default bot client to be used by modules of this package
|
||||
default_bot_client = create_bot_client()
|
@ -1,3 +1,10 @@
|
||||
from .client import DiscordClient # noqa
|
||||
from .exceptions import DiscordApiBackoff # noqa
|
||||
from .helpers import DiscordRoles # noqa
|
||||
from .app_settings import DISCORD_OAUTH_BASE_URL, DISCORD_OAUTH_TOKEN_URL # noqa
|
||||
from .client import DiscordClient # noqa
|
||||
from .exceptions import ( # noqa
|
||||
DiscordApiBackoff,
|
||||
DiscordClientException,
|
||||
DiscordRateLimitExhausted,
|
||||
DiscordTooManyRequestsError,
|
||||
)
|
||||
from .helpers import RolesSet # noqa
|
||||
from .models import Guild, GuildMember, Role, User # noqa
|
||||
|
@ -1,45 +1,56 @@
|
||||
"""Settings for the Discord client.
|
||||
|
||||
To overwrite a default set the variable in your local Django settings, e.g:
|
||||
|
||||
.. code:: python
|
||||
|
||||
DISCORD_GUILD_NAME_CACHE_MAX_AGE = 7200
|
||||
"""
|
||||
|
||||
from ..utils import clean_setting
|
||||
|
||||
|
||||
# Base URL for all API calls. Must end with /.
|
||||
DISCORD_API_BASE_URL = clean_setting(
|
||||
'DISCORD_API_BASE_URL', 'https://discord.com/api/'
|
||||
)
|
||||
"""Base URL for all API calls. Must end with /."""
|
||||
|
||||
# Low level connecttimeout for requests to the Discord API in seconds
|
||||
DISCORD_API_TIMEOUT_CONNECT = clean_setting(
|
||||
'DISCORD_API_TIMEOUT', 5
|
||||
)
|
||||
"""Low level connect timeout for requests to the Discord API in seconds."""
|
||||
|
||||
# Low level read timeout for requests to the Discord API in seconds
|
||||
DISCORD_API_TIMEOUT_READ = clean_setting(
|
||||
'DISCORD_API_TIMEOUT', 30
|
||||
)
|
||||
"""Low level read timeout for requests to the Discord API in seconds."""
|
||||
|
||||
# Base authorization URL for Discord Oauth
|
||||
DISCORD_OAUTH_BASE_URL = clean_setting(
|
||||
'DISCORD_OAUTH_BASE_URL', 'https://discord.com/api/oauth2/authorize'
|
||||
)
|
||||
"""Base authorization URL for Discord Oauth."""
|
||||
|
||||
# Base authorization URL for Discord Oauth
|
||||
DISCORD_OAUTH_TOKEN_URL = clean_setting(
|
||||
'DISCORD_OAUTH_TOKEN_URL', 'https://discord.com/api/oauth2/token'
|
||||
)
|
||||
"""Base authorization URL for Discord Oauth."""
|
||||
|
||||
# How long the Discord guild names retrieved from the server are
|
||||
# caches locally in seconds.
|
||||
DISCORD_GUILD_NAME_CACHE_MAX_AGE = clean_setting(
|
||||
'DISCORD_GUILD_NAME_CACHE_MAX_AGE', 3600 * 24
|
||||
)
|
||||
"""How long the Discord guild names retrieved from the server
|
||||
are caches locally in seconds.
|
||||
"""
|
||||
|
||||
# How long Discord roles retrieved from the server are caches locally in seconds.
|
||||
DISCORD_ROLES_CACHE_MAX_AGE = clean_setting(
|
||||
'DISCORD_ROLES_CACHE_MAX_AGE', 3600 * 1
|
||||
)
|
||||
"""How long Discord roles retrieved from the server are caches locally in seconds."""
|
||||
|
||||
# Turns off creation of new roles. In case the rate limit for creating roles is
|
||||
# exhausted, this setting allows the Discord service to continue to function
|
||||
# and wait out the reset. Rate limit is about 250 per 48 hrs.
|
||||
DISCORD_DISABLE_ROLE_CREATION = clean_setting(
|
||||
'DISCORD_DISABLE_ROLE_CREATION', False
|
||||
)
|
||||
"""Turns off creation of new roles. In case the rate limit for creating roles is
|
||||
exhausted, this setting allows the Discord service to continue to function
|
||||
and wait out the reset. Rate limit is about 250 per 48 hrs.
|
||||
"""
|
||||
|
@ -1,32 +1,37 @@
|
||||
from hashlib import md5
|
||||
"""Client for interacting with the Discord API."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from enum import IntEnum
|
||||
from hashlib import md5
|
||||
from http import HTTPStatus
|
||||
from time import sleep
|
||||
from typing import Iterable, List, Optional, Set, Tuple
|
||||
from urllib.parse import urljoin
|
||||
from uuid import uuid1
|
||||
|
||||
from redis import Redis
|
||||
import requests
|
||||
from requests.exceptions import HTTPError
|
||||
from redis import Redis
|
||||
|
||||
from allianceauth.utils.cache import get_redis_client
|
||||
|
||||
from allianceauth import __title__ as AUTH_TITLE, __url__, __version__
|
||||
from allianceauth import __title__ as AUTH_TITLE
|
||||
from allianceauth import __url__, __version__
|
||||
|
||||
from .. import __title__
|
||||
from ..utils import LoggerAddTag
|
||||
from .app_settings import (
|
||||
DISCORD_API_BASE_URL,
|
||||
DISCORD_API_TIMEOUT_CONNECT,
|
||||
DISCORD_API_TIMEOUT_READ,
|
||||
DISCORD_DISABLE_ROLE_CREATION,
|
||||
DISCORD_GUILD_NAME_CACHE_MAX_AGE,
|
||||
DISCORD_OAUTH_BASE_URL,
|
||||
DISCORD_OAUTH_TOKEN_URL,
|
||||
DISCORD_ROLES_CACHE_MAX_AGE,
|
||||
)
|
||||
from .exceptions import DiscordRateLimitExhausted, DiscordTooManyRequestsError
|
||||
from .helpers import DiscordRoles
|
||||
from ..utils import LoggerAddTag
|
||||
|
||||
from .helpers import RolesSet
|
||||
from .models import Guild, GuildMember, Role, User
|
||||
|
||||
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
||||
|
||||
@ -58,8 +63,13 @@ MINIMUM_BLOCKING_WAIT = 50
|
||||
RATE_LIMIT_RETRIES = 1000
|
||||
|
||||
|
||||
class DiscordApiStatusCode(IntEnum):
|
||||
"""Status code returned from the Discord API."""
|
||||
UNKNOWN_MEMBER = 10007 #:
|
||||
|
||||
|
||||
class DiscordClient:
|
||||
"""This class provides a web client for interacting with the Discord API
|
||||
"""This class provides a web client for interacting with the Discord API.
|
||||
|
||||
The client has rate limiting that supports concurrency.
|
||||
This means it is able to ensure the API rate limit is not violated,
|
||||
@ -67,24 +77,30 @@ class DiscordClient:
|
||||
|
||||
In addition the client support proper API backoff.
|
||||
|
||||
Synchronization of rate limit infos accross multiple processes
|
||||
Synchronization of rate limit infos across multiple processes
|
||||
is implemented with Redis and thus requires Redis as Django cache backend.
|
||||
|
||||
All durations are in milliseconds.
|
||||
"""
|
||||
OAUTH_BASE_URL = DISCORD_OAUTH_BASE_URL
|
||||
OAUTH_TOKEN_URL = DISCORD_OAUTH_TOKEN_URL
|
||||
The cache is shared across all clients and processes (also using Redis).
|
||||
|
||||
All durations are in milliseconds.
|
||||
|
||||
Most errors from the API will raise a requests.HTTPError.
|
||||
|
||||
Args:
|
||||
access_token: Discord access token used to authenticate all calls to the API
|
||||
redis: Redis instance to be used.
|
||||
is_rate_limited: Set to False to turn off rate limiting (use with care).
|
||||
If not specified will try to use the Redis instance
|
||||
from the default Django cache backend.
|
||||
|
||||
Raises:
|
||||
ValueError: No access token provided
|
||||
"""
|
||||
_KEY_GLOBAL_BACKOFF_UNTIL = 'DISCORD_GLOBAL_BACKOFF_UNTIL'
|
||||
_KEY_GLOBAL_RATE_LIMIT_REMAINING = 'DISCORD_GLOBAL_RATE_LIMIT_REMAINING'
|
||||
_KEYPREFIX_GUILD_NAME = 'DISCORD_GUILD_NAME'
|
||||
_KEYPREFIX_GUILD_ROLES = 'DISCORD_GUILD_ROLES'
|
||||
_KEYPREFIX_ROLE_NAME = 'DISCORD_ROLE_NAME'
|
||||
_NICK_MAX_CHARS = 32
|
||||
|
||||
_HTTP_STATUS_CODE_NOT_FOUND = 404
|
||||
_HTTP_STATUS_CODE_RATE_LIMITED = 429
|
||||
_DISCORD_STATUS_CODE_UNKNOWN_MEMBER = 10007
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -92,14 +108,8 @@ class DiscordClient:
|
||||
redis: Redis = None,
|
||||
is_rate_limited: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Params:
|
||||
- access_token: Discord access token used to authenticate all calls to the API
|
||||
- redis: Redis instance to be used.
|
||||
- is_rate_limited: Set to False to run of rate limiting (use with care)
|
||||
If not specified will try to use the Redis instance
|
||||
from the default Django cache backend.
|
||||
"""
|
||||
if not access_token:
|
||||
raise ValueError('You must provide an access token.')
|
||||
self._access_token = str(access_token)
|
||||
self._is_rate_limited = bool(is_rate_limited)
|
||||
if not redis:
|
||||
@ -131,19 +141,20 @@ class DiscordClient:
|
||||
self.__redis_script_set_longer = self._redis.register_script(lua_2)
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
def access_token(self) -> str:
|
||||
"""Discord access token."""
|
||||
return self._access_token
|
||||
|
||||
@property
|
||||
def is_rate_limited(self):
|
||||
def is_rate_limited(self) -> bool:
|
||||
"""Wether this instance is rate limited."""
|
||||
return self._is_rate_limited
|
||||
|
||||
def __repr__(self):
|
||||
return f'{type(self).__name__}(access_token=...{self.access_token[-5:]})'
|
||||
|
||||
def _redis_decr_or_set(self, name: str, value: str, px: int) -> bool:
|
||||
"""decreases the key value if it exists and returns the result
|
||||
else sets the key
|
||||
"""Decrease the key value if it exists and returns the result else set the key.
|
||||
|
||||
Implemented as Lua script to ensure atomicity.
|
||||
"""
|
||||
@ -152,7 +163,7 @@ class DiscordClient:
|
||||
)
|
||||
|
||||
def _redis_set_if_longer(self, name: str, value: str, px: int) -> bool:
|
||||
"""like set, but only goes through if either key doesn't exist
|
||||
"""Like set, but only goes through if either key doesn't exist
|
||||
or px would be extended.
|
||||
|
||||
Implemented as Lua script to ensure atomicity.
|
||||
@ -163,111 +174,134 @@ class DiscordClient:
|
||||
|
||||
# users
|
||||
|
||||
def current_user(self) -> dict:
|
||||
"""returns the user belonging to the current access_token"""
|
||||
def current_user(self) -> User:
|
||||
"""Fetch user belonging to the current access_token."""
|
||||
authorization = f'Bearer {self.access_token}'
|
||||
r = self._api_request(
|
||||
method='get', route='users/@me', authorization=authorization
|
||||
)
|
||||
return r.json()
|
||||
return User.from_dict(r.json())
|
||||
|
||||
# guild
|
||||
|
||||
def guild_infos(self, guild_id: int) -> dict:
|
||||
"""Returns all basic infos about this guild"""
|
||||
def guild_infos(self, guild_id: int) -> Guild:
|
||||
"""Fetch all basic infos about this guild.
|
||||
|
||||
Args:
|
||||
guild_id: Discord ID of the guild
|
||||
"""
|
||||
route = f"guilds/{guild_id}"
|
||||
r = self._api_request(method='get', route=route)
|
||||
return r.json()
|
||||
return Guild.from_dict(r.json())
|
||||
|
||||
def guild_name(self, guild_id: int, use_cache: bool = True) -> str:
|
||||
"""returns the name of this guild (cached)
|
||||
or an empty string if something went wrong
|
||||
"""Fetch the name of this guild (cached).
|
||||
|
||||
Params:
|
||||
- guild_id: ID of current guild
|
||||
- use_cache: When set to False will force an API call to get the server name
|
||||
Args:
|
||||
guild_id: Discord ID of the guild
|
||||
use_cache: When set to False will force an API call to get the server name
|
||||
|
||||
Returns:
|
||||
Name of the server or an empty string if something went wrong.
|
||||
"""
|
||||
key_name = self._guild_name_cache_key(guild_id)
|
||||
if use_cache:
|
||||
guild_name = self._redis_decode(self._redis.get(key_name))
|
||||
else:
|
||||
guild_name = None
|
||||
guild_name = ""
|
||||
if not guild_name:
|
||||
guild_infos = self.guild_infos(guild_id)
|
||||
if 'name' in guild_infos:
|
||||
guild_name = guild_infos['name']
|
||||
self._redis.set(
|
||||
name=key_name,
|
||||
value=guild_name,
|
||||
ex=DISCORD_GUILD_NAME_CACHE_MAX_AGE
|
||||
)
|
||||
try:
|
||||
guild = self.guild_infos(guild_id)
|
||||
except HTTPError:
|
||||
guild_name = ""
|
||||
else:
|
||||
guild_name = ''
|
||||
|
||||
guild_name = guild.name
|
||||
self._redis.set(
|
||||
name=key_name, value=guild_name, ex=DISCORD_GUILD_NAME_CACHE_MAX_AGE
|
||||
)
|
||||
return guild_name
|
||||
|
||||
@classmethod
|
||||
def _guild_name_cache_key(cls, guild_id: int) -> str:
|
||||
"""Returns key for accessing role given by name in the role cache"""
|
||||
"""Construct key for accessing role given by name in the role cache.
|
||||
|
||||
Args:
|
||||
guild_id: Discord ID of the guild
|
||||
"""
|
||||
gen_key = DiscordClient._generate_hash(f'{guild_id}')
|
||||
return f'{cls._KEYPREFIX_GUILD_NAME}__{gen_key}'
|
||||
|
||||
# guild roles
|
||||
|
||||
def guild_roles(self, guild_id: int, use_cache: bool = True) -> list:
|
||||
"""Returns the list of all roles for this guild
|
||||
def guild_roles(self, guild_id: int, use_cache: bool = True) -> Set[Role]:
|
||||
"""Fetch all roles for this guild.
|
||||
|
||||
If use_cache is set to False it will always hit the API to retrieve
|
||||
fresh data and update the cache
|
||||
Args:
|
||||
guild_id: Discord ID of the guild
|
||||
use_cache: If is set to False it will always hit the API to retrieve
|
||||
fresh data and update the cache.
|
||||
|
||||
Returns:
|
||||
"""
|
||||
cache_key = self._guild_roles_cache_key(guild_id)
|
||||
roles = None
|
||||
if use_cache:
|
||||
roles_raw = self._redis.get(name=cache_key)
|
||||
if roles_raw:
|
||||
logger.debug('Returning roles for guild %s from cache', guild_id)
|
||||
return json.loads(self._redis_decode(roles_raw))
|
||||
else:
|
||||
logger.debug('No roles for guild %s in cache', guild_id)
|
||||
|
||||
route = f"guilds/{guild_id}/roles"
|
||||
r = self._api_request(method='get', route=route)
|
||||
roles = r.json()
|
||||
if roles and isinstance(roles, list):
|
||||
roles = json.loads(self._redis_decode(roles_raw))
|
||||
logger.debug('No roles for guild %s in cache', guild_id)
|
||||
if roles is None:
|
||||
route = f"guilds/{guild_id}/roles"
|
||||
r = self._api_request(method='get', route=route)
|
||||
roles = r.json()
|
||||
if not roles or not isinstance(roles, list):
|
||||
raise RuntimeError(
|
||||
f"Unexpected response when fetching roles from API: {roles}"
|
||||
)
|
||||
self._redis.set(
|
||||
name=cache_key,
|
||||
value=json.dumps(roles),
|
||||
ex=DISCORD_ROLES_CACHE_MAX_AGE
|
||||
)
|
||||
return roles
|
||||
return {Role.from_dict(role) for role in roles}
|
||||
|
||||
def create_guild_role(self, guild_id: int, role_name: str, **kwargs) -> dict:
|
||||
def create_guild_role(
|
||||
self, guild_id: int, role_name: str, **kwargs
|
||||
) -> Optional[Role]:
|
||||
"""Create a new guild role with the given name.
|
||||
|
||||
See official documentation for additional optional parameters.
|
||||
|
||||
Note that Discord allows the creation of multiple roles with the same name,
|
||||
so to avoid duplicates it's important to check existing roles
|
||||
before creating new one
|
||||
|
||||
returns a new role dict on success
|
||||
Args:
|
||||
guild_id: Discord ID of the guild
|
||||
role_name: Name of new role to create
|
||||
|
||||
Returns:
|
||||
new role on success
|
||||
"""
|
||||
route = f"guilds/{guild_id}/roles"
|
||||
data = {'name': DiscordRoles.sanitize_role_name(role_name)}
|
||||
data = {'name': Role.sanitize_name(role_name)}
|
||||
data.update(kwargs)
|
||||
r = self._api_request(method='post', route=route, data=data)
|
||||
role = r.json()
|
||||
if role:
|
||||
self._invalidate_guild_roles_cache(guild_id)
|
||||
return role
|
||||
return Role.from_dict(role)
|
||||
return None
|
||||
|
||||
def delete_guild_role(self, guild_id: int, role_id: int) -> bool:
|
||||
"""Deletes a guild role"""
|
||||
"""Delete a guild role."""
|
||||
route = f"guilds/{guild_id}/roles/{role_id}"
|
||||
r = self._api_request(method='delete', route=route)
|
||||
if r.status_code == 204:
|
||||
self._invalidate_guild_roles_cache(guild_id)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
def _invalidate_guild_roles_cache(self, guild_id: int) -> None:
|
||||
cache_key = self._guild_roles_cache_key(guild_id)
|
||||
@ -276,67 +310,79 @@ class DiscordClient:
|
||||
|
||||
@classmethod
|
||||
def _guild_roles_cache_key(cls, guild_id: int) -> str:
|
||||
"""Returns key for accessing cached roles for a guild"""
|
||||
"""Construct key for accessing cached roles for a guild.
|
||||
|
||||
Args:
|
||||
guild_id: Discord ID of the guild
|
||||
"""
|
||||
gen_key = cls._generate_hash(f'{guild_id}')
|
||||
return f'{cls._KEYPREFIX_GUILD_ROLES}__{gen_key}'
|
||||
|
||||
def match_role_from_name(self, guild_id: int, role_name: str) -> dict:
|
||||
"""returns Discord role matching the given name or an empty dict"""
|
||||
guild_roles = DiscordRoles(self.guild_roles(guild_id))
|
||||
def match_role_from_name(self, guild_id: int, role_name: str) -> Optional[Role]:
|
||||
"""Fetch Discord role matching the given name (cached).
|
||||
|
||||
Args:
|
||||
guild_id: Discord ID of the guild
|
||||
role_name: Name of role
|
||||
|
||||
Returns:
|
||||
Matching role or None if no match is found
|
||||
"""
|
||||
guild_roles = RolesSet(self.guild_roles(guild_id))
|
||||
return guild_roles.role_by_name(role_name)
|
||||
|
||||
def match_or_create_roles_from_names(self, guild_id: int, role_names: list) -> list:
|
||||
"""returns Discord roles matching the given names
|
||||
|
||||
Returns as list of tuple of role and created flag
|
||||
def match_or_create_roles_from_names(
|
||||
self, guild_id: int, role_names: Iterable[str]
|
||||
) -> List[Tuple[Role, bool]]:
|
||||
"""Fetch or create Discord roles matching the given names (cached).
|
||||
|
||||
Will try to match with existing roles names
|
||||
Non-existing roles will be created, then created flag will be True
|
||||
|
||||
Params:
|
||||
- guild_id: ID of guild
|
||||
- role_names: list of name strings each defining a role
|
||||
Args:
|
||||
guild_id: ID of guild
|
||||
role_names: list of name strings each defining a role
|
||||
|
||||
Returns:
|
||||
List of tuple of Role and created flag
|
||||
"""
|
||||
roles = list()
|
||||
guild_roles = DiscordRoles(self.guild_roles(guild_id))
|
||||
role_names_cleaned = {
|
||||
DiscordRoles.sanitize_role_name(name) for name in role_names
|
||||
}
|
||||
guild_roles = RolesSet(self.guild_roles(guild_id))
|
||||
role_names_cleaned = {Role.sanitize_name(name) for name in role_names}
|
||||
for role_name in role_names_cleaned:
|
||||
role, created = self.match_or_create_role_from_name(
|
||||
guild_id=guild_id,
|
||||
role_name=DiscordRoles.sanitize_role_name(role_name),
|
||||
guild_roles=guild_roles
|
||||
guild_id=guild_id, role_name=role_name, guild_roles=guild_roles
|
||||
)
|
||||
if role:
|
||||
roles.append((role, created))
|
||||
if created:
|
||||
guild_roles = guild_roles.union(DiscordRoles([role]))
|
||||
guild_roles = guild_roles.union(RolesSet([role]))
|
||||
return roles
|
||||
|
||||
def match_or_create_role_from_name(
|
||||
self, guild_id: int, role_name: str, guild_roles: DiscordRoles = None
|
||||
) -> tuple:
|
||||
"""returns Discord role matching the given name
|
||||
|
||||
Returns as tuple of role and created flag
|
||||
self, guild_id: int, role_name: str, guild_roles: RolesSet = None
|
||||
) -> Tuple[Role, bool]:
|
||||
"""Fetch or create Discord role matching the given name.
|
||||
|
||||
Will try to match with existing roles names
|
||||
Non-existing roles will be created, then created flag will be True
|
||||
|
||||
Params:
|
||||
- guild_id: ID of guild
|
||||
- role_name: strings defining name of a role
|
||||
- guild_roles: All known guild roles as DiscordRoles object.
|
||||
Helps to void redundant lookups of guild roles
|
||||
when this method is used multiple times.
|
||||
Args:
|
||||
guild_id: ID of guild
|
||||
role_name: strings defining name of a role
|
||||
guild_roles: All known guild roles as RolesSet object.
|
||||
Helps to void redundant lookups of guild roles
|
||||
when this method is used multiple times.
|
||||
|
||||
Returns:
|
||||
Tuple of Role and created flag
|
||||
"""
|
||||
if not isinstance(role_name, str):
|
||||
raise TypeError('role_name must be of type string')
|
||||
|
||||
created = False
|
||||
if guild_roles is None:
|
||||
guild_roles = DiscordRoles(self.guild_roles(guild_id))
|
||||
guild_roles = RolesSet(self.guild_roles(guild_id))
|
||||
role = guild_roles.role_by_name(role_name)
|
||||
if not role:
|
||||
if not DISCORD_DISABLE_ROLE_CREATION:
|
||||
@ -345,9 +391,24 @@ class DiscordClient:
|
||||
created = True
|
||||
else:
|
||||
role = None
|
||||
|
||||
return role, created
|
||||
|
||||
def match_or_create_roles_from_names_2(
|
||||
self, guild_id: int, role_names: Iterable[str]
|
||||
) -> RolesSet:
|
||||
"""Fetch or create Discord role matching the given name.
|
||||
|
||||
Wrapper for ``match_or_create_role_from_name()``
|
||||
|
||||
Returns:
|
||||
Roles as RolesSet object.
|
||||
"""
|
||||
return RolesSet.create_from_matched_roles(
|
||||
self.match_or_create_roles_from_names(
|
||||
guild_id=guild_id, role_names=role_names
|
||||
)
|
||||
)
|
||||
|
||||
# guild members
|
||||
|
||||
def add_guild_member(
|
||||
@ -357,13 +418,13 @@ class DiscordClient:
|
||||
access_token: str,
|
||||
role_ids: list = None,
|
||||
nick: str = None
|
||||
) -> bool:
|
||||
"""Adds a user to the guilds.
|
||||
) -> Optional[bool]:
|
||||
"""Adds a user to the guild.
|
||||
|
||||
Returns:
|
||||
- True when a new user was added
|
||||
- None if the user already existed
|
||||
- False when something went wrong or raises exception
|
||||
- True when a new user was added
|
||||
- None if the user already existed
|
||||
- False when something went wrong or raises exception
|
||||
"""
|
||||
route = f"guilds/{guild_id}/members/{user_id}"
|
||||
data = {
|
||||
@ -371,42 +432,49 @@ class DiscordClient:
|
||||
}
|
||||
if role_ids:
|
||||
data['roles'] = self._sanitize_role_ids(role_ids)
|
||||
|
||||
if nick:
|
||||
data['nick'] = str(nick)[:self._NICK_MAX_CHARS]
|
||||
|
||||
data['nick'] = GuildMember.sanitize_nick(nick)
|
||||
r = self._api_request(method='put', route=route, data=data)
|
||||
r.raise_for_status()
|
||||
if r.status_code == 201:
|
||||
return True
|
||||
elif r.status_code == 204:
|
||||
return None
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
def guild_member(self, guild_id: int, user_id: int) -> dict:
|
||||
"""returns the user info for a guild member
|
||||
def guild_member(self, guild_id: int, user_id: int) -> Optional[GuildMember]:
|
||||
"""Fetch info for a guild member.
|
||||
|
||||
or None if the user is not a member of the guild
|
||||
Args:
|
||||
guild_id: Discord ID of the guild
|
||||
user_id: Discord ID of the user
|
||||
|
||||
Returns:
|
||||
guild member or ``None`` if the user is not a member of the guild
|
||||
"""
|
||||
route = f'guilds/{guild_id}/members/{user_id}'
|
||||
r = self._api_request(method='get', route=route, raise_for_status=False)
|
||||
if self._is_member_unknown_error(r):
|
||||
logger.warning("Discord user ID %s could not be found on server.", user_id)
|
||||
return None
|
||||
else:
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
r.raise_for_status()
|
||||
return GuildMember.from_dict(r.json())
|
||||
|
||||
def modify_guild_member(
|
||||
self, guild_id: int, user_id: int, role_ids: list = None, nick: str = None
|
||||
) -> bool:
|
||||
"""Modify attributes of a guild member.
|
||||
self, guild_id: int, user_id: int, role_ids: List[int] = None, nick: str = None
|
||||
) -> Optional[bool]:
|
||||
"""Set properties of a guild member.
|
||||
|
||||
Args:
|
||||
guild_id: Discord ID of the guild
|
||||
user_id: Discord ID of the user
|
||||
roles_id: New list of role IDs (if provided)
|
||||
nick: New nickname (if provided)
|
||||
|
||||
Returns
|
||||
- True when successful
|
||||
- None if user is not a member of this guild
|
||||
- False otherwise
|
||||
- True when successful
|
||||
- None if user is not a member of this guild
|
||||
- False otherwise
|
||||
"""
|
||||
if not role_ids and not nick:
|
||||
raise ValueError('Must specify role_ids or nick')
|
||||
@ -419,7 +487,7 @@ class DiscordClient:
|
||||
data['roles'] = self._sanitize_role_ids(role_ids)
|
||||
|
||||
if nick:
|
||||
data['nick'] = self._sanitize_nick(nick)
|
||||
data['nick'] = GuildMember.sanitize_nick(nick)
|
||||
|
||||
route = f"guilds/{guild_id}/members/{user_id}"
|
||||
r = self._api_request(
|
||||
@ -428,21 +496,22 @@ class DiscordClient:
|
||||
if self._is_member_unknown_error(r):
|
||||
logger.warning('User ID %s is not a member of this guild', user_id)
|
||||
return None
|
||||
else:
|
||||
r.raise_for_status()
|
||||
|
||||
r.raise_for_status()
|
||||
if r.status_code == 204:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
def remove_guild_member(self, guild_id: int, user_id: int) -> bool:
|
||||
"""Remove a member from a guild
|
||||
def remove_guild_member(self, guild_id: int, user_id: int) -> Optional[bool]:
|
||||
"""Remove a member from a guild.
|
||||
|
||||
Args:
|
||||
guild_id: Discord ID of the guild
|
||||
user_id: Discord ID of the user
|
||||
|
||||
Returns:
|
||||
- True when successful
|
||||
- None if member does not exist
|
||||
- False otherwise
|
||||
- True when successful
|
||||
- None if member does not exist
|
||||
- False otherwise
|
||||
"""
|
||||
route = f"guilds/{guild_id}/members/{user_id}"
|
||||
r = self._api_request(
|
||||
@ -451,19 +520,16 @@ class DiscordClient:
|
||||
if self._is_member_unknown_error(r):
|
||||
logger.warning('User ID %s is not a member of this guild', user_id)
|
||||
return None
|
||||
else:
|
||||
r.raise_for_status()
|
||||
|
||||
r.raise_for_status()
|
||||
if r.status_code == 204:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
# Guild member roles
|
||||
|
||||
def add_guild_member_role(
|
||||
self, guild_id: int, user_id: int, role_id: int
|
||||
) -> bool:
|
||||
) -> Optional[bool]:
|
||||
"""Adds a role to a guild member
|
||||
|
||||
Returns:
|
||||
@ -476,43 +542,69 @@ class DiscordClient:
|
||||
if self._is_member_unknown_error(r):
|
||||
logger.warning('User ID %s is not a member of this guild', user_id)
|
||||
return None
|
||||
else:
|
||||
r.raise_for_status()
|
||||
|
||||
r.raise_for_status()
|
||||
if r.status_code == 204:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
def remove_guild_member_role(
|
||||
self, guild_id: int, user_id: int, role_id: int
|
||||
) -> bool:
|
||||
"""Removes a role to a guild member
|
||||
) -> Optional[bool]:
|
||||
"""Remove a role to a guild member
|
||||
|
||||
Args:
|
||||
guild_id: Discord ID of the guild
|
||||
user_id: Discord ID of the user
|
||||
role_id: Discord ID of role to be removed
|
||||
|
||||
Returns:
|
||||
- True when successful
|
||||
- None if member does not exist
|
||||
- False otherwise
|
||||
- True when successful
|
||||
- None if member does not exist
|
||||
- False otherwise
|
||||
"""
|
||||
route = f"guilds/{guild_id}/members/{user_id}/roles/{role_id}"
|
||||
r = self._api_request(method='delete', route=route, raise_for_status=False)
|
||||
if self._is_member_unknown_error(r):
|
||||
logger.warning('User ID %s is not a member of this guild', user_id)
|
||||
return None
|
||||
else:
|
||||
r.raise_for_status()
|
||||
|
||||
r.raise_for_status()
|
||||
if r.status_code == 204:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
def guild_member_roles(self, guild_id: int, user_id: int) -> Optional[RolesSet]:
|
||||
"""Fetch the current guild roles of a guild member.
|
||||
|
||||
Args:
|
||||
- guild_id: Discord guild ID
|
||||
- user_id: Discord user ID
|
||||
|
||||
Returns:
|
||||
- Member roles
|
||||
- None if user is not a member of the guild
|
||||
"""
|
||||
member_info = self.guild_member(guild_id=guild_id, user_id=user_id)
|
||||
if member_info is None:
|
||||
return None # User is no longer a member
|
||||
guild_roles = RolesSet(self.guild_roles(guild_id=guild_id))
|
||||
logger.debug('Current guild roles: %s', guild_roles.ids())
|
||||
if not guild_roles.has_roles(member_info.roles):
|
||||
guild_roles = RolesSet(
|
||||
self.guild_roles(guild_id=guild_id, use_cache=False)
|
||||
)
|
||||
if not guild_roles.has_roles(member_info.roles):
|
||||
role_ids = set(member_info.roles).difference(guild_roles.ids())
|
||||
raise RuntimeError(
|
||||
f'Discord user {user_id} has unknown roles: {role_ids}'
|
||||
)
|
||||
return guild_roles.subset(member_info.roles)
|
||||
|
||||
@classmethod
|
||||
def _is_member_unknown_error(cls, r: requests.Response) -> bool:
|
||||
try:
|
||||
result = (
|
||||
r.status_code == cls._HTTP_STATUS_CODE_NOT_FOUND
|
||||
and r.json()['code'] == cls._DISCORD_STATUS_CODE_UNKNOWN_MEMBER
|
||||
r.status_code == HTTPStatus.NOT_FOUND
|
||||
and r.json()['code'] == DiscordApiStatusCode.UNKNOWN_MEMBER
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
result = False
|
||||
@ -529,7 +621,19 @@ class DiscordClient:
|
||||
authorization: str = None,
|
||||
raise_for_status: bool = True
|
||||
) -> requests.Response:
|
||||
"""Core method for performing all API calls"""
|
||||
"""Core method for performing all API calls.
|
||||
|
||||
Args:
|
||||
method: HTTP method of the request, e.g. "get"
|
||||
route: Route in the Discord API, e.g. "users/@me"
|
||||
data: Data to be send with the request
|
||||
authorization: The authorization string to be used.
|
||||
Will use the default bot token if not set.
|
||||
raise_for_status: Whether a requests exception is to be raised when not ok
|
||||
|
||||
Returns:
|
||||
The raw response from the API
|
||||
"""
|
||||
uid = uuid1().hex
|
||||
|
||||
if not hasattr(requests, method):
|
||||
@ -577,7 +681,7 @@ class DiscordClient:
|
||||
r.text
|
||||
)
|
||||
|
||||
if r.status_code == self._HTTP_STATUS_CODE_RATE_LIMITED:
|
||||
if r.status_code == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
self._handle_new_api_backoff(r, uid)
|
||||
|
||||
self._report_rate_limit_from_api(r, uid)
|
||||
@ -588,9 +692,10 @@ class DiscordClient:
|
||||
return r
|
||||
|
||||
def _handle_ongoing_api_backoff(self, uid: str) -> None:
|
||||
"""checks if api is currently on backoff
|
||||
if on backoff: will do a blocking wait if it expires soon,
|
||||
else raises exception
|
||||
"""Check if api is currently on backoff.
|
||||
|
||||
If on backoff: will do a blocking wait if it expires soon,
|
||||
else raises exception.
|
||||
"""
|
||||
global_backoff_duration = self._redis.pttl(self._KEY_GLOBAL_BACKOFF_UNTIL)
|
||||
if global_backoff_duration > 0:
|
||||
@ -610,8 +715,9 @@ class DiscordClient:
|
||||
raise DiscordTooManyRequestsError(retry_after=global_backoff_duration)
|
||||
|
||||
def _ensure_rate_limed_not_exhausted(self, uid: str) -> int:
|
||||
"""ensures that the rate limit is not exhausted
|
||||
if exhausted: will do a blocking wait if rate limit resets soon,
|
||||
"""Ensures that the rate limit is not exhausted.
|
||||
|
||||
If exhausted: will do a blocking wait if rate limit resets soon,
|
||||
else raises exception
|
||||
|
||||
returns requests remaining on success
|
||||
@ -654,10 +760,10 @@ class DiscordClient:
|
||||
)
|
||||
raise DiscordRateLimitExhausted(resets_in)
|
||||
|
||||
raise RuntimeError('Failed to handle rate limit after after too tries.')
|
||||
raise RuntimeError('Failed to handle rate limit after after too many tries.')
|
||||
|
||||
def _handle_new_api_backoff(self, r: requests.Response, uid: str) -> None:
|
||||
"""raises exception for new API backoff error"""
|
||||
"""Raise exception for new API backoff error."""
|
||||
response = r.json()
|
||||
if 'retry_after' in response:
|
||||
try:
|
||||
@ -679,8 +785,8 @@ class DiscordClient:
|
||||
)
|
||||
raise DiscordTooManyRequestsError(retry_after=retry_after)
|
||||
|
||||
def _report_rate_limit_from_api(self, r, uid):
|
||||
"""Tries to log the current rate limit reported from API"""
|
||||
def _report_rate_limit_from_api(self, r, uid) -> None:
|
||||
"""Try to log the current rate limit reported from API."""
|
||||
if (
|
||||
logger.getEffectiveLevel() <= logging.DEBUG
|
||||
and 'x-ratelimit-limit' in r.headers
|
||||
@ -703,22 +809,17 @@ class DiscordClient:
|
||||
|
||||
@staticmethod
|
||||
def _redis_decode(value: str) -> str:
|
||||
"""Decodes a string from Redis and passes through None and Booleans"""
|
||||
"""Decode a string from Redis and passes through None and Booleans."""
|
||||
if value is not None and not isinstance(value, bool):
|
||||
return value.decode('utf-8')
|
||||
else:
|
||||
return value
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _generate_hash(key: str) -> str:
|
||||
"""Generate hash key for given string."""
|
||||
return md5(key.encode('utf-8')).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_role_ids(role_ids: list) -> list:
|
||||
"""make sure its a list of integers"""
|
||||
return [int(role_id) for role_id in list(role_ids)]
|
||||
|
||||
@classmethod
|
||||
def _sanitize_nick(cls, nick: str) -> str:
|
||||
"""shortens too long strings if necessary"""
|
||||
return str(nick)[:cls._NICK_MAX_CHARS]
|
||||
def _sanitize_role_ids(role_ids: Iterable[int]) -> List[int]:
|
||||
"""Sanitize a list of role IDs, i.e. make sure its a list of unique integers."""
|
||||
return [int(role_id) for role_id in set(role_ids)]
|
||||
|
@ -1,23 +1,26 @@
|
||||
"""Custom exceptions for the Discord Client package."""
|
||||
|
||||
import math
|
||||
|
||||
|
||||
class DiscordClientException(Exception):
|
||||
"""Base Exception for the Discord client"""
|
||||
"""Base Exception for the Discord client."""
|
||||
|
||||
|
||||
class DiscordApiBackoff(DiscordClientException):
|
||||
"""Exception signaling we need to backoff from sending requests to the API for now
|
||||
"""Exception signaling we need to backoff from sending requests to the API for now.
|
||||
|
||||
Args:
|
||||
retry_after: time to retry after in milliseconds
|
||||
"""
|
||||
|
||||
def __init__(self, retry_after: int):
|
||||
"""
|
||||
:param retry_after: int time to retry after in milliseconds
|
||||
"""
|
||||
super().__init__()
|
||||
self.retry_after = int(retry_after)
|
||||
|
||||
@property
|
||||
def retry_after_seconds(self):
|
||||
"""Time to retry after in seconds."""
|
||||
return math.ceil(self.retry_after / 1000)
|
||||
|
||||
|
||||
|
@ -1,27 +1,37 @@
|
||||
from copy import copy
|
||||
from typing import Set, Iterable
|
||||
from typing import Iterable, List, Optional, Set, Tuple
|
||||
|
||||
from .models import Role
|
||||
|
||||
|
||||
class DiscordRoles:
|
||||
"""Container class that helps dealing with Discord roles.
|
||||
class RolesSet:
|
||||
"""Container of Discord roles with added functionality.
|
||||
|
||||
Objects of this class are immutable and work in many ways like sets.
|
||||
|
||||
Ideally objects are initialized from raw API responses,
|
||||
e.g. from DiscordClient.guild.roles()
|
||||
"""
|
||||
_ROLE_NAME_MAX_CHARS = 100
|
||||
e.g. from DiscordClient.guild.roles().
|
||||
|
||||
def __init__(self, roles_lst: list) -> None:
|
||||
"""roles_lst must be a list of dict, each defining a role"""
|
||||
Args:
|
||||
roles_lst: List of dicts, each defining a role
|
||||
"""
|
||||
def __init__(self, roles_lst: Iterable[Role]) -> None:
|
||||
if not isinstance(roles_lst, (list, set, tuple)):
|
||||
raise TypeError('roles_lst must be of type list, set or tuple')
|
||||
self._roles = dict()
|
||||
self._roles_by_name = dict()
|
||||
for role in list(roles_lst):
|
||||
self._assert_valid_role(role)
|
||||
self._roles[int(role['id'])] = role
|
||||
self._roles_by_name[self.sanitize_role_name(role['name'])] = role
|
||||
if not isinstance(role, Role):
|
||||
raise TypeError('Roles must be of type Role: %s' % role)
|
||||
self._roles[role.id] = role
|
||||
self._roles_by_name[role.name] = role
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self._roles_by_name:
|
||||
roles = '"' + '", "'.join(sorted(list(self._roles_by_name.keys()))) + '"'
|
||||
else:
|
||||
roles = ""
|
||||
return f'{self.__class__.__name__}([{roles}])'
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, type(self)):
|
||||
@ -41,15 +51,15 @@ class DiscordRoles:
|
||||
return len(self._roles.keys())
|
||||
|
||||
def has_roles(self, role_ids: Set[int]) -> bool:
|
||||
"""returns true if this objects contains all roles defined by given role_ids
|
||||
incl. managed roles
|
||||
"""True if this objects contains all roles defined by given role_ids
|
||||
incl. managed roles.
|
||||
"""
|
||||
role_ids = {int(id) for id in role_ids}
|
||||
all_role_ids = self._roles.keys()
|
||||
return role_ids.issubset(all_role_ids)
|
||||
|
||||
def ids(self) -> Set[int]:
|
||||
"""return a set of all role IDs"""
|
||||
"""Set of all role IDs."""
|
||||
return set(self._roles.keys())
|
||||
|
||||
def subset(
|
||||
@ -57,13 +67,13 @@ class DiscordRoles:
|
||||
role_ids: Iterable[int] = None,
|
||||
managed_only: bool = False,
|
||||
role_names: Iterable[str] = None
|
||||
) -> "DiscordRoles":
|
||||
"""returns a new object containing the subset of roles
|
||||
) -> "RolesSet":
|
||||
"""Create instance containing the subset of roles
|
||||
|
||||
Args:
|
||||
- role_ids: role ids must be in the provided list
|
||||
- managed_only: roles must be managed
|
||||
- role_names: role names must match provided list (not case sensitive)
|
||||
role_ids: role ids must be in the provided list
|
||||
managed_only: roles must be managed
|
||||
role_names: role names must match provided list (not case sensitive)
|
||||
"""
|
||||
if role_ids is not None:
|
||||
role_ids = {int(id) for id in role_ids}
|
||||
@ -75,72 +85,50 @@ class DiscordRoles:
|
||||
|
||||
elif role_ids is None and managed_only:
|
||||
return type(self)([
|
||||
role for _, role in self._roles.items() if role['managed']
|
||||
role for _, role in self._roles.items() if role.managed
|
||||
])
|
||||
|
||||
elif role_ids is not None and managed_only:
|
||||
return type(self)([
|
||||
role for role_id, role in self._roles.items()
|
||||
if role_id in role_ids and role['managed']
|
||||
if role_id in role_ids and role.managed
|
||||
])
|
||||
|
||||
elif role_ids is None and managed_only is False and role_names is not None:
|
||||
role_names = {self.sanitize_role_name(name).lower() for name in role_names}
|
||||
role_names = {Role.sanitize_name(name).lower() for name in role_names}
|
||||
return type(self)([
|
||||
role for role in self._roles.values()
|
||||
if role["name"].lower() in role_names
|
||||
if role.name.lower() in role_names
|
||||
])
|
||||
|
||||
return copy(self)
|
||||
|
||||
def union(self, other: object) -> "DiscordRoles":
|
||||
"""returns a new roles object that is the union of this roles object
|
||||
with other"""
|
||||
def union(self, other: object) -> "RolesSet":
|
||||
"""Create instance that is the union of this roles object with other."""
|
||||
return type(self)(list(self) + list(other))
|
||||
|
||||
def difference(self, other: object) -> "DiscordRoles":
|
||||
"""returns a new roles object that only contains the roles
|
||||
that exist in the current objects, but not in other
|
||||
def difference(self, other: object) -> "RolesSet":
|
||||
"""Create instance that only contains the roles
|
||||
that exist in the current objects, but not in other.
|
||||
"""
|
||||
new_ids = self.ids().difference(other.ids())
|
||||
return self.subset(role_ids=new_ids)
|
||||
|
||||
def role_by_name(self, role_name: str) -> dict:
|
||||
"""returns role if one with matching name is found else an empty dict"""
|
||||
role_name = self.sanitize_role_name(role_name)
|
||||
def role_by_name(self, role_name: str) -> Optional[Role]:
|
||||
"""Role if one with matching name is found else None."""
|
||||
role_name = Role.sanitize_name(role_name)
|
||||
if role_name in self._roles_by_name:
|
||||
return self._roles_by_name[role_name]
|
||||
return dict()
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def create_from_matched_roles(cls, matched_roles: list) -> "DiscordRoles":
|
||||
"""returns a new object created from the given list of matches roles
|
||||
def create_from_matched_roles(
|
||||
cls, matched_roles: List[Tuple[Role, bool]]
|
||||
) -> "RolesSet":
|
||||
"""Create new instance from the given list of matches roles.
|
||||
|
||||
matches_roles must be a list of tuples in the form: (role, created)
|
||||
Args:
|
||||
matches_roles: list of matches roles
|
||||
"""
|
||||
raw_roles = [x[0] for x in matched_roles]
|
||||
return cls(raw_roles)
|
||||
|
||||
@staticmethod
|
||||
def _assert_valid_role(role: dict) -> None:
|
||||
if not isinstance(role, dict):
|
||||
raise TypeError('Roles must be of type dict: %s' % role)
|
||||
|
||||
if 'id' not in role or 'name' not in role or 'managed' not in role:
|
||||
raise ValueError('This role is not valid: %s' % role)
|
||||
|
||||
@classmethod
|
||||
def sanitize_role_name(cls, role_name: str) -> str:
|
||||
"""shortens too long strings if necessary"""
|
||||
return str(role_name)[:cls._ROLE_NAME_MAX_CHARS]
|
||||
|
||||
|
||||
def match_or_create_roles_from_names(
|
||||
client: object, guild_id: int, role_names: list
|
||||
) -> DiscordRoles:
|
||||
"""Shortcut for getting the result of matching role names as DiscordRoles object"""
|
||||
return DiscordRoles.create_from_matched_roles(
|
||||
client.match_or_create_roles_from_names(
|
||||
guild_id=guild_id, role_names=role_names
|
||||
)
|
||||
)
|
||||
|
125
allianceauth/services/modules/discord/discord_client/models.py
Normal file
125
allianceauth/services/modules/discord/discord_client/models.py
Normal file
@ -0,0 +1,125 @@
|
||||
"""Implementation of Discord objects used by this client.
|
||||
|
||||
Note that only those objects and properties are implemented, which are needed by AA.
|
||||
|
||||
Names and types are mirrored from the API whenever possible.
|
||||
Discord's snowflake type (used by Discord IDs) is implemented as int.
|
||||
"""
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import FrozenSet
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class User:
|
||||
"""A user on Discord."""
|
||||
|
||||
id: int
|
||||
username: str
|
||||
discriminator: str
|
||||
|
||||
def __post_init__(self):
|
||||
object.__setattr__(self, "id", int(self.id))
|
||||
object.__setattr__(self, "username", str(self.username))
|
||||
object.__setattr__(self, "discriminator", str(self.discriminator))
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "User":
|
||||
"""Create object from dictionary as received from the API."""
|
||||
return cls(
|
||||
id=int(data["id"]),
|
||||
username=data["username"],
|
||||
discriminator=data["discriminator"],
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Role:
|
||||
"""A role on Discord."""
|
||||
|
||||
_ROLE_NAME_MAX_CHARS = 100
|
||||
|
||||
id: int
|
||||
name: str
|
||||
managed: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
object.__setattr__(self, "id", int(self.id))
|
||||
object.__setattr__(self, "name", self.sanitize_name(self.name))
|
||||
object.__setattr__(self, "managed", bool(self.managed))
|
||||
|
||||
def asdict(self) -> dict:
|
||||
"""Convert object into a dictionary representation."""
|
||||
return asdict(self)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "Role":
|
||||
"""Create object from dictionary as received from the API."""
|
||||
return cls(id=int(data["id"]), name=data["name"], managed=data["managed"])
|
||||
|
||||
@classmethod
|
||||
def sanitize_name(cls, role_name: str) -> str:
|
||||
"""Shorten too long names if necessary."""
|
||||
return str(role_name)[: cls._ROLE_NAME_MAX_CHARS]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Guild:
|
||||
"""A guild on Discord."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
roles: FrozenSet[Role]
|
||||
|
||||
def __post_init__(self):
|
||||
object.__setattr__(self, "id", int(self.id))
|
||||
object.__setattr__(self, "name", str(self.name))
|
||||
for role in self.roles:
|
||||
if not isinstance(role, Role):
|
||||
raise TypeError("roles can only contain Role objects.")
|
||||
object.__setattr__(self, "roles", frozenset(self.roles))
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "Guild":
|
||||
"""Create object from dictionary as received from the API."""
|
||||
return cls(
|
||||
id=int(data["id"]),
|
||||
name=data["name"],
|
||||
roles=frozenset(Role.from_dict(obj) for obj in data["roles"]),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GuildMember:
|
||||
"""A member of a guild on Discord."""
|
||||
|
||||
_NICK_MAX_CHARS = 32
|
||||
|
||||
roles: FrozenSet[int]
|
||||
nick: str = None
|
||||
user: User = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.nick:
|
||||
object.__setattr__(self, "nick", self.sanitize_nick(self.nick))
|
||||
if self.user and not isinstance(self.user, User):
|
||||
raise TypeError("user must be of type User")
|
||||
for role in self.roles:
|
||||
if not isinstance(role, int):
|
||||
raise TypeError("roles can only contain ints")
|
||||
object.__setattr__(self, "roles", frozenset(self.roles))
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "GuildMember":
|
||||
"""Create object from dictionary as received from the API."""
|
||||
params = {"roles": {int(obj) for obj in data["roles"]}}
|
||||
if data.get("user"):
|
||||
params["user"] = User.from_dict(data["user"])
|
||||
if data.get("nick"):
|
||||
params["nick"] = data["nick"]
|
||||
return cls(**params)
|
||||
|
||||
@classmethod
|
||||
def sanitize_nick(cls, nick: str) -> str:
|
||||
"""Sanitize a nick, i.e. shorten too long strings if necessary."""
|
||||
return str(nick)[: cls._NICK_MAX_CHARS]
|
@ -1,40 +0,0 @@
|
||||
TEST_GUILD_ID = 123456789012345678
|
||||
TEST_USER_ID = 198765432012345678
|
||||
TEST_USER_NAME = 'Peter Parker'
|
||||
TEST_USER_DISCRIMINATOR = '1234'
|
||||
TEST_BOT_TOKEN = 'abcdefhijlkmnopqastzvwxyz1234567890ABCDEFGHOJKLMNOPQRSTUVWXY'
|
||||
TEST_ROLE_ID = 654321012345678912
|
||||
|
||||
|
||||
def create_role(id: int, name: str, managed=False) -> dict:
|
||||
return {
|
||||
'id': int(id),
|
||||
'name': str(name),
|
||||
'managed': bool(managed)
|
||||
}
|
||||
|
||||
|
||||
def create_matched_role(role, created=False) -> tuple:
|
||||
return role, created
|
||||
|
||||
|
||||
ROLE_ALPHA = create_role(1, 'alpha')
|
||||
ROLE_BRAVO = create_role(2, 'bravo')
|
||||
ROLE_CHARLIE = create_role(3, 'charlie')
|
||||
ROLE_CHARLIE_2 = create_role(4, 'Charlie') # Discord roles are case sensitive
|
||||
ROLE_MIKE = create_role(13, 'mike', True)
|
||||
|
||||
|
||||
ALL_ROLES = [ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE]
|
||||
|
||||
|
||||
def create_user_info(
|
||||
id: int = TEST_USER_ID,
|
||||
username: str = TEST_USER_NAME,
|
||||
discriminator: str = TEST_USER_DISCRIMINATOR
|
||||
):
|
||||
return {
|
||||
'id': str(id),
|
||||
'username': str(username[:32]),
|
||||
'discriminator': str(discriminator[:4])
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
{
|
||||
"guilds": {
|
||||
"2909267986263572999": {
|
||||
"id": "2909267986263572999",
|
||||
"name": "Mason's Test Server",
|
||||
"icon": "389030ec9db118cb5b85a732333b7c98",
|
||||
"description": null,
|
||||
"splash": "75610b05a0dd09ec2c3c7df9f6975ea0",
|
||||
"discovery_splash": null,
|
||||
"approximate_member_count": 2,
|
||||
"approximate_presence_count": 2,
|
||||
"features": [
|
||||
"INVITE_SPLASH",
|
||||
"VANITY_URL",
|
||||
"COMMERCE",
|
||||
"BANNER",
|
||||
"NEWS",
|
||||
"VERIFIED",
|
||||
"VIP_REGIONS"
|
||||
],
|
||||
"emojis": [
|
||||
{
|
||||
"name": "ultrafastparrot",
|
||||
"roles": [],
|
||||
"id": "393564762228785161",
|
||||
"require_colons": true,
|
||||
"managed": false,
|
||||
"animated": true,
|
||||
"available": true
|
||||
}
|
||||
],
|
||||
"banner": "5c3cb8d1bc159937fffe7e641ec96ca7",
|
||||
"owner_id": "53908232506183680",
|
||||
"application_id": null,
|
||||
"region": null,
|
||||
"afk_channel_id": null,
|
||||
"afk_timeout": 300,
|
||||
"system_channel_id": null,
|
||||
"widget_enabled": true,
|
||||
"widget_channel_id": "639513352485470208",
|
||||
"verification_level": 0,
|
||||
"roles": [
|
||||
{
|
||||
"id": "2909267986263572999",
|
||||
"name": "@everyone",
|
||||
"permissions": "49794752",
|
||||
"position": 0,
|
||||
"color": 0,
|
||||
"hoist": false,
|
||||
"managed": false,
|
||||
"mentionable": false
|
||||
}
|
||||
],
|
||||
"default_message_notifications": 1,
|
||||
"mfa_level": 0,
|
||||
"explicit_content_filter": 0,
|
||||
"max_presences": null,
|
||||
"max_members": 250000,
|
||||
"max_video_channel_users": 25,
|
||||
"vanity_url_code": "no",
|
||||
"premium_tier": 0,
|
||||
"premium_subscription_count": 0,
|
||||
"system_channel_flags": 0,
|
||||
"preferred_locale": "en-US",
|
||||
"rules_channel_id": null,
|
||||
"public_updates_channel_id": null
|
||||
}
|
||||
},
|
||||
"guildMembers": {
|
||||
"1": {
|
||||
"user": {},
|
||||
"nick": null,
|
||||
"avatar": null,
|
||||
"roles": [],
|
||||
"joined_at": "2015-04-26T06:26:56.936000+00:00",
|
||||
"deaf": false,
|
||||
"mute": false
|
||||
},
|
||||
"2": {
|
||||
"user": {
|
||||
"id": "80351110224678912",
|
||||
"username": "Nelly",
|
||||
"discriminator": "1337",
|
||||
"avatar": "8342729096ea3675442027381ff50dfe",
|
||||
"verified": true,
|
||||
"email": "nelly@discord.com",
|
||||
"flags": 64,
|
||||
"banner": "06c16474723fe537c283b8efa61a30c8",
|
||||
"accent_color": 16711680,
|
||||
"premium_type": 1,
|
||||
"public_flags": 64
|
||||
},
|
||||
"nick": "Nelly the great",
|
||||
"avatar": null,
|
||||
"roles": [
|
||||
"197150972374548480",
|
||||
"41771983423143936"
|
||||
],
|
||||
"joined_at": "2015-04-26T06:26:56.936000+00:00",
|
||||
"deaf": false,
|
||||
"mute": false
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"197150972374548480": {
|
||||
"id": "197150972374548480",
|
||||
"name": "My Managed Role",
|
||||
"color": 3447003,
|
||||
"hoist": false,
|
||||
"icon": "cf3ced8600b777c9486c6d8d84fb4327",
|
||||
"unicode_emoji": null,
|
||||
"position": 2,
|
||||
"permissions": "66321471",
|
||||
"managed": true,
|
||||
"mentionable": false
|
||||
},
|
||||
"2909267986263572999": {
|
||||
"id": "2909267986263572999",
|
||||
"name": "@everyone",
|
||||
"permissions": "49794752",
|
||||
"position": 0,
|
||||
"color": 0,
|
||||
"hoist": false,
|
||||
"managed": false,
|
||||
"mentionable": false
|
||||
},
|
||||
"41771983423143936": {
|
||||
"id": "41771983423143936",
|
||||
"name": "WE DEM BOYZZ!!!!!!",
|
||||
"color": 3447003,
|
||||
"hoist": true,
|
||||
"icon": "cf3ced8600b777c9486c6d8d84fb4327",
|
||||
"unicode_emoji": null,
|
||||
"position": 1,
|
||||
"permissions": "66321471",
|
||||
"managed": false,
|
||||
"mentionable": false
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"80351110224678912": {
|
||||
"id": "80351110224678912",
|
||||
"username": "Nelly",
|
||||
"discriminator": "1337",
|
||||
"avatar": "8342729096ea3675442027381ff50dfe",
|
||||
"verified": true,
|
||||
"email": "nelly@discord.com",
|
||||
"flags": 64,
|
||||
"banner": "06c16474723fe537c283b8efa61a30c8",
|
||||
"accent_color": 16711680,
|
||||
"premium_type": 1,
|
||||
"public_flags": 64
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
from itertools import count
|
||||
|
||||
from django.utils.timezone import now
|
||||
|
||||
from ..client import DiscordApiStatusCode
|
||||
from ..models import Guild, GuildMember, Role, User
|
||||
|
||||
TEST_GUILD_ID = 123456789012345678
|
||||
TEST_GUILD_NAME = "Test Guild"
|
||||
TEST_USER_ID = 198765432012345678
|
||||
TEST_USER_NAME = "Peter Parker"
|
||||
TEST_USER_DISCRIMINATOR = "1234"
|
||||
TEST_BOT_TOKEN = "abcdefhijlkmnopqastzvwxyz1234567890ABCDEFGHOJKLMNOPQRSTUVWXY"
|
||||
TEST_ROLE_ID = 654321012345678912
|
||||
|
||||
|
||||
def create_discord_role_object(id: int, name: str, managed: bool = False) -> dict:
|
||||
return {"id": str(int(id)), "name": str(name), "managed": bool(managed)}
|
||||
|
||||
|
||||
def create_matched_role(role, created=False) -> tuple:
|
||||
return role, created
|
||||
|
||||
|
||||
def create_discord_user_object(**kwargs):
|
||||
params = {
|
||||
"id": TEST_USER_ID,
|
||||
"username": TEST_USER_NAME,
|
||||
"discriminator": TEST_USER_DISCRIMINATOR,
|
||||
}
|
||||
params.update(kwargs)
|
||||
params["id"] = str(int(params["id"]))
|
||||
return params
|
||||
|
||||
|
||||
def create_discord_guild_member_object(user=None, **kwargs):
|
||||
user_params = {}
|
||||
if user:
|
||||
user_params["user"] = user
|
||||
params = {
|
||||
"user": create_discord_user_object(**user_params),
|
||||
"roles": [],
|
||||
"joined_at": now().isoformat(),
|
||||
"deaf": False,
|
||||
"mute": False,
|
||||
}
|
||||
params.update(kwargs)
|
||||
params["roles"] = [str(int(obj)) for obj in params["roles"]]
|
||||
return params
|
||||
|
||||
|
||||
def create_discord_error_response(code: int) -> dict:
|
||||
return {"code": int(code)}
|
||||
|
||||
|
||||
def create_discord_error_response_unknown_member() -> dict:
|
||||
return create_discord_error_response(DiscordApiStatusCode.UNKNOWN_MEMBER.value)
|
||||
|
||||
|
||||
def create_discord_guild_object(**kwargs):
|
||||
params = {"id": TEST_GUILD_ID, "name": TEST_GUILD_NAME, "roles": []}
|
||||
params.update(kwargs)
|
||||
params["id"] = str(int(params["id"]))
|
||||
return params
|
||||
|
||||
|
||||
def create_user(**kwargs):
|
||||
params = {
|
||||
"id": TEST_USER_ID,
|
||||
"username": TEST_USER_NAME,
|
||||
"discriminator": TEST_USER_DISCRIMINATOR,
|
||||
}
|
||||
params.update(kwargs)
|
||||
return User(**params)
|
||||
|
||||
|
||||
def create_guild(**kwargs):
|
||||
params = {"id": TEST_GUILD_ID, "name": TEST_GUILD_NAME, "roles": []}
|
||||
params.update(kwargs)
|
||||
return Guild(**params)
|
||||
|
||||
|
||||
def create_guild_member(**kwargs):
|
||||
params = {"user": create_user(), "roles": []}
|
||||
params.update(kwargs)
|
||||
return GuildMember(**params)
|
||||
|
||||
|
||||
def create_role(**kwargs) -> dict:
|
||||
params = {"managed": False}
|
||||
params.update(kwargs)
|
||||
if "id" not in params:
|
||||
params["id"] = next_number("role")
|
||||
if "name" not in params:
|
||||
params["name"] = f"Test Role #{params['id']}"
|
||||
return Role(**params)
|
||||
|
||||
|
||||
def next_number(key: str = None) -> int:
|
||||
"""Calculate the next number in a persistent sequence."""
|
||||
if key is None:
|
||||
key = "_general"
|
||||
try:
|
||||
return next_number._counter[key].__next__()
|
||||
except AttributeError:
|
||||
next_number._counter = dict()
|
||||
except KeyError:
|
||||
pass
|
||||
next_number._counter[key] = count(start=1)
|
||||
return next_number._counter[key].__next__()
|
File diff suppressed because it is too large
Load Diff
@ -1,177 +1,201 @@
|
||||
from unittest import TestCase
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
from . import (
|
||||
ROLE_ALPHA,
|
||||
ROLE_BRAVO,
|
||||
ROLE_CHARLIE,
|
||||
ROLE_CHARLIE_2,
|
||||
ROLE_MIKE,
|
||||
ALL_ROLES,
|
||||
create_role
|
||||
)
|
||||
from .. import DiscordRoles
|
||||
from ..helpers import RolesSet
|
||||
from .factories import create_matched_role, create_role
|
||||
|
||||
|
||||
MODULE_PATH = 'allianceauth.services.modules.discord.discord_client.client'
|
||||
|
||||
|
||||
class TestDiscordRoles(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.all_roles = DiscordRoles(ALL_ROLES)
|
||||
|
||||
class TestRolesSet(NoSocketsTestCase):
|
||||
def test_can_create_simple(self):
|
||||
roles_raw = [ROLE_ALPHA]
|
||||
roles = DiscordRoles(roles_raw)
|
||||
# given
|
||||
roles_raw = [create_role()]
|
||||
# when
|
||||
roles = RolesSet(roles_raw)
|
||||
# then
|
||||
self.assertListEqual(list(roles), roles_raw)
|
||||
|
||||
def test_can_create_empty(self):
|
||||
roles_raw = []
|
||||
roles = DiscordRoles(roles_raw)
|
||||
# when
|
||||
roles = RolesSet([])
|
||||
# then
|
||||
self.assertListEqual(list(roles), [])
|
||||
|
||||
def test_raises_exception_if_roles_raw_of_wrong_type(self):
|
||||
with self.assertRaises(TypeError):
|
||||
DiscordRoles({'id': 1})
|
||||
RolesSet({"id": 1})
|
||||
|
||||
def test_raises_exception_if_list_contains_non_dict(self):
|
||||
roles_raw = [ROLE_ALPHA, 'not_valid']
|
||||
# given
|
||||
roles_raw = [create_role(), "not_valid"]
|
||||
# when/then
|
||||
with self.assertRaises(TypeError):
|
||||
DiscordRoles(roles_raw)
|
||||
|
||||
def test_raises_exception_if_invalid_role_1(self):
|
||||
roles_raw = [{'name': 'alpha', 'managed': False}]
|
||||
with self.assertRaises(ValueError):
|
||||
DiscordRoles(roles_raw)
|
||||
|
||||
def test_raises_exception_if_invalid_role_2(self):
|
||||
roles_raw = [{'id': 1, 'managed': False}]
|
||||
with self.assertRaises(ValueError):
|
||||
DiscordRoles(roles_raw)
|
||||
|
||||
def test_raises_exception_if_invalid_role_3(self):
|
||||
roles_raw = [{'id': 1, 'name': 'alpha'}]
|
||||
with self.assertRaises(ValueError):
|
||||
DiscordRoles(roles_raw)
|
||||
RolesSet(roles_raw)
|
||||
|
||||
def test_roles_are_equal(self):
|
||||
roles_a = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
roles_b = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
# given
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
roles_a = RolesSet([role_a, role_b])
|
||||
roles_b = RolesSet([role_a, role_b])
|
||||
# when/then
|
||||
self.assertEqual(roles_a, roles_b)
|
||||
|
||||
def test_roles_are_not_equal(self):
|
||||
roles_a = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
roles_b = DiscordRoles([ROLE_ALPHA])
|
||||
# given
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
roles_a = RolesSet([role_a, role_b])
|
||||
roles_b = RolesSet([role_a])
|
||||
# when/then
|
||||
self.assertNotEqual(roles_a, roles_b)
|
||||
|
||||
def test_different_objects_are_not_equal(self):
|
||||
roles_a = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
roles_a = RolesSet([])
|
||||
self.assertFalse(roles_a == "invalid")
|
||||
|
||||
def test_len(self):
|
||||
self.assertEqual(len(self.all_roles), 4)
|
||||
# given
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
roles = RolesSet([role_a, role_b])
|
||||
# when/then
|
||||
self.assertEqual(len(roles), 2)
|
||||
|
||||
def test_contains(self):
|
||||
self.assertTrue(1 in self.all_roles)
|
||||
self.assertFalse(99 in self.all_roles)
|
||||
|
||||
def test_sanitize_role_name(self):
|
||||
role_name_input = 'x' * 110
|
||||
role_name_expected = 'x' * 100
|
||||
result = DiscordRoles.sanitize_role_name(role_name_input)
|
||||
self.assertEqual(result, role_name_expected)
|
||||
# given
|
||||
role_a = create_role(id=1)
|
||||
roles = RolesSet([role_a])
|
||||
# when/then
|
||||
self.assertTrue(1 in roles)
|
||||
self.assertFalse(99 in roles)
|
||||
|
||||
def test_objects_are_hashable(self):
|
||||
roles_a = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
roles_b = DiscordRoles([ROLE_BRAVO, ROLE_ALPHA])
|
||||
roles_c = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE])
|
||||
# given
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
role_c = create_role()
|
||||
roles_a = RolesSet([role_a, role_b])
|
||||
roles_b = RolesSet([role_b, role_a])
|
||||
roles_c = RolesSet([role_a, role_b, role_c])
|
||||
# when/then
|
||||
self.assertIsNotNone(hash(roles_a))
|
||||
self.assertEqual(hash(roles_a), hash(roles_b))
|
||||
self.assertNotEqual(hash(roles_a), hash(roles_c))
|
||||
|
||||
def test_create_from_matched_roles(self):
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
matched_roles = [
|
||||
(ROLE_ALPHA, True),
|
||||
(ROLE_BRAVO, False)
|
||||
create_matched_role(role_a, True),
|
||||
create_matched_role(role_b, False),
|
||||
]
|
||||
roles = DiscordRoles.create_from_matched_roles(matched_roles)
|
||||
self.assertSetEqual(roles.ids(), {1, 2})
|
||||
|
||||
|
||||
class TestIds(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.all_roles = DiscordRoles(ALL_ROLES)
|
||||
# when
|
||||
roles = RolesSet.create_from_matched_roles(matched_roles)
|
||||
# then
|
||||
self.assertEqual(roles, RolesSet([role_a, role_b]))
|
||||
|
||||
def test_return_role_ids_default(self):
|
||||
result = self.all_roles.ids()
|
||||
expected = {1, 2, 3, 13}
|
||||
self.assertSetEqual(result, expected)
|
||||
role_a = create_role(id=1)
|
||||
role_b = create_role(id=2)
|
||||
roles = RolesSet([role_a, role_b])
|
||||
# when/then
|
||||
self.assertSetEqual(roles.ids(), {1, 2})
|
||||
|
||||
def test_return_role_ids_empty(self):
|
||||
roles = DiscordRoles([])
|
||||
# given
|
||||
roles = RolesSet([])
|
||||
# when/then
|
||||
self.assertSetEqual(roles.ids(), set())
|
||||
|
||||
|
||||
class TestSubset(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.all_roles = DiscordRoles(ALL_ROLES)
|
||||
|
||||
class TestRolesSetSubset(NoSocketsTestCase):
|
||||
def test_ids_only(self):
|
||||
role_ids = {1, 3}
|
||||
roles_subset = self.all_roles.subset(role_ids)
|
||||
expected = {1, 3}
|
||||
self.assertSetEqual(roles_subset.ids(), expected)
|
||||
# given
|
||||
role_a = create_role(id=1)
|
||||
role_b = create_role(id=2)
|
||||
role_c = create_role(id=3)
|
||||
roles_all = RolesSet([role_a, role_b, role_c])
|
||||
# when
|
||||
roles_subset = roles_all.subset({1, 3})
|
||||
# then
|
||||
self.assertEqual(roles_subset, RolesSet([role_a, role_c]))
|
||||
|
||||
def test_ids_as_string_work_too(self):
|
||||
role_ids = {'1', '3'}
|
||||
roles_subset = self.all_roles.subset(role_ids)
|
||||
expected = {1, 3}
|
||||
self.assertSetEqual(roles_subset.ids(), expected)
|
||||
# given
|
||||
role_a = create_role(id=1)
|
||||
role_b = create_role(id=2)
|
||||
role_c = create_role(id=3)
|
||||
roles_all = RolesSet([role_a, role_b, role_c])
|
||||
# when
|
||||
roles_subset = roles_all.subset({"1", "3"})
|
||||
# then
|
||||
self.assertEqual(roles_subset, RolesSet([role_a, role_c]))
|
||||
|
||||
def test_managed_only(self):
|
||||
roles = self.all_roles.subset(managed_only=True)
|
||||
expected = {13}
|
||||
self.assertSetEqual(roles.ids(), expected)
|
||||
# given
|
||||
role_a = create_role(id=1)
|
||||
role_m = create_role(id=13, managed=True)
|
||||
roles_all = RolesSet([role_a, role_m])
|
||||
# when
|
||||
roles_subset = roles_all.subset(managed_only=True)
|
||||
# then
|
||||
self.assertEqual(roles_subset, RolesSet([role_m]))
|
||||
|
||||
def test_ids_and_managed_only(self):
|
||||
role_ids = {1, 3, 13}
|
||||
roles_subset = self.all_roles.subset(role_ids, managed_only=True)
|
||||
expected = {13}
|
||||
self.assertSetEqual(roles_subset.ids(), expected)
|
||||
# given
|
||||
role_a = create_role(id=1)
|
||||
role_b = create_role(id=2)
|
||||
role_m = create_role(id=13, managed=True)
|
||||
roles_all = RolesSet([role_a, role_b, role_m])
|
||||
# when
|
||||
roles_subset = roles_all.subset({1, 13}, managed_only=True)
|
||||
# then
|
||||
self.assertEqual(roles_subset, RolesSet([role_m]))
|
||||
|
||||
def test_ids_are_empty(self):
|
||||
roles = self.all_roles.subset([])
|
||||
expected = set()
|
||||
self.assertSetEqual(roles.ids(), expected)
|
||||
# given
|
||||
role_a = create_role(id=1)
|
||||
role_b = create_role(id=2)
|
||||
roles_all = RolesSet([role_a, role_b])
|
||||
roles_subset = roles_all.subset([])
|
||||
# then
|
||||
self.assertEqual(roles_subset, RolesSet([]))
|
||||
|
||||
def test_no_parameters(self):
|
||||
roles = self.all_roles.subset()
|
||||
expected = {1, 2, 3, 13}
|
||||
self.assertSetEqual(roles.ids(), expected)
|
||||
# given
|
||||
role_a = create_role(id=1)
|
||||
role_b = create_role(id=2)
|
||||
roles_all = RolesSet([role_a, role_b])
|
||||
roles_subset = roles_all.subset()
|
||||
# then
|
||||
self.assertEqual(roles_subset, roles_all)
|
||||
|
||||
def test_should_return_role_names_only(self):
|
||||
# given
|
||||
all_roles = DiscordRoles([
|
||||
ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ROLE_CHARLIE_2
|
||||
])
|
||||
role_a = create_role(name="alpha")
|
||||
role_b = create_role(name="bravo")
|
||||
role_c1 = create_role(name="charlie")
|
||||
role_c2 = create_role(name="Charlie")
|
||||
roles_all = RolesSet([role_a, role_b, role_c1, role_c2])
|
||||
# when
|
||||
roles = all_roles.subset(role_names={"bravo", "charlie"})
|
||||
roles_subset = roles_all.subset(role_names={"bravo", "charlie"})
|
||||
# then
|
||||
self.assertSetEqual(roles.ids(), {2, 3, 4})
|
||||
self.assertSetEqual(roles_subset, RolesSet([role_b, role_c1, role_c2]))
|
||||
|
||||
|
||||
class TestHasRoles(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.all_roles = DiscordRoles(ALL_ROLES)
|
||||
class TestRolesSetHasRoles(NoSocketsTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
role_a = create_role(id=1)
|
||||
role_b = create_role(id=2)
|
||||
role_c = create_role(id=3)
|
||||
cls.all_roles = RolesSet([role_a, role_b, role_c])
|
||||
|
||||
def test_true_if_all_roles_exit(self):
|
||||
self.assertTrue(self.all_roles.has_roles([1, 2]))
|
||||
|
||||
def test_true_if_all_roles_exit_str(self):
|
||||
self.assertTrue(self.all_roles.has_roles(['1', '2']))
|
||||
self.assertTrue(self.all_roles.has_roles(["1", "2"]))
|
||||
|
||||
def test_false_if_role_does_not_exit(self):
|
||||
self.assertFalse(self.all_roles.has_roles([99]))
|
||||
@ -183,74 +207,104 @@ class TestHasRoles(TestCase):
|
||||
self.assertTrue(self.all_roles.has_roles([]))
|
||||
|
||||
|
||||
class TestGetMatchingRolesByName(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.all_roles = DiscordRoles(ALL_ROLES)
|
||||
|
||||
class TestRolesSetGetMatchingRolesByName(NoSocketsTestCase):
|
||||
def test_return_role_if_matches(self):
|
||||
role_name = 'alpha'
|
||||
expected = ROLE_ALPHA
|
||||
result = self.all_roles.role_by_name(role_name)
|
||||
self.assertEqual(result, expected)
|
||||
# given
|
||||
role_a = create_role(name="alpha")
|
||||
role_b = create_role(name="bravo")
|
||||
roles = RolesSet([role_a, role_b])
|
||||
# when
|
||||
result = roles.role_by_name("alpha")
|
||||
# then
|
||||
self.assertEqual(result, role_a)
|
||||
|
||||
def test_return_role_if_matches_and_limit_max_length(self):
|
||||
role_name = 'x' * 120
|
||||
expected = create_role(77, 'x' * 100)
|
||||
roles = DiscordRoles([expected])
|
||||
# given
|
||||
role_name = "x" * 120
|
||||
role = create_role(name="x" * 100)
|
||||
roles = RolesSet([role])
|
||||
# when
|
||||
result = roles.role_by_name(role_name)
|
||||
self.assertEqual(result, expected)
|
||||
# then
|
||||
self.assertEqual(result, role)
|
||||
|
||||
def test_return_empty_if_not_matches(self):
|
||||
role_name = 'lima'
|
||||
expected = {}
|
||||
result = self.all_roles.role_by_name(role_name)
|
||||
self.assertEqual(result, expected)
|
||||
# given
|
||||
role_a = create_role(name="alpha")
|
||||
role_b = create_role(name="bravo")
|
||||
roles = RolesSet([role_a, role_b])
|
||||
# when
|
||||
result = roles.role_by_name("unknown")
|
||||
# then
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestUnion(TestCase):
|
||||
|
||||
class TestRolesSetUnion(NoSocketsTestCase):
|
||||
def test_distinct_sets(self):
|
||||
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
roles_2 = DiscordRoles([ROLE_CHARLIE, ROLE_MIKE])
|
||||
roles_3 = roles_1.union(roles_2)
|
||||
expected = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE])
|
||||
self.assertEqual(roles_3, expected)
|
||||
# given
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
roles_1 = RolesSet([role_a])
|
||||
roles_2 = RolesSet([role_b])
|
||||
# when
|
||||
result = roles_1.union(roles_2)
|
||||
# then
|
||||
self.assertEqual(result, RolesSet([role_a, role_b]))
|
||||
|
||||
def test_overlapping_sets(self):
|
||||
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
roles_2 = DiscordRoles([ROLE_BRAVO, ROLE_MIKE])
|
||||
roles_3 = roles_1.union(roles_2)
|
||||
expected = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE])
|
||||
self.assertEqual(roles_3, expected)
|
||||
# given
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
role_c = create_role()
|
||||
roles_1 = RolesSet([role_a, role_b])
|
||||
roles_2 = RolesSet([role_b, role_c])
|
||||
# when
|
||||
result = roles_1.union(roles_2)
|
||||
self.assertEqual(result, RolesSet([role_a, role_b, role_c]))
|
||||
|
||||
def test_identical_sets(self):
|
||||
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
roles_2 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
roles_3 = roles_1.union(roles_2)
|
||||
expected = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
self.assertEqual(roles_3, expected)
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
roles_1 = RolesSet([role_a, role_b])
|
||||
roles_2 = RolesSet([role_a, role_b])
|
||||
# when
|
||||
result = roles_1.union(roles_2)
|
||||
self.assertEqual(result, RolesSet([role_a, role_b]))
|
||||
|
||||
|
||||
class TestDifference(TestCase):
|
||||
|
||||
class TestRolesSetDifference(NoSocketsTestCase):
|
||||
def test_distinct_sets(self):
|
||||
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
roles_2 = DiscordRoles([ROLE_CHARLIE, ROLE_MIKE])
|
||||
roles_3 = roles_1.difference(roles_2)
|
||||
expected = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
self.assertEqual(roles_3, expected)
|
||||
# given
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
role_c = create_role()
|
||||
role_d = create_role()
|
||||
roles_1 = RolesSet([role_a, role_b])
|
||||
roles_2 = RolesSet([role_c, role_d])
|
||||
# when
|
||||
result = roles_1.difference(roles_2)
|
||||
# then
|
||||
self.assertEqual(result, RolesSet([role_a, role_b]))
|
||||
|
||||
def test_overlapping_sets(self):
|
||||
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
roles_2 = DiscordRoles([ROLE_BRAVO, ROLE_MIKE])
|
||||
roles_3 = roles_1.difference(roles_2)
|
||||
expected = DiscordRoles([ROLE_ALPHA])
|
||||
self.assertEqual(roles_3, expected)
|
||||
# given
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
role_c = create_role()
|
||||
roles_1 = RolesSet([role_a, role_b])
|
||||
roles_2 = RolesSet([role_b, role_c])
|
||||
# when
|
||||
result = roles_1.difference(roles_2)
|
||||
# then
|
||||
self.assertEqual(result, RolesSet([role_a]))
|
||||
|
||||
def test_identical_sets(self):
|
||||
roles_1 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
roles_2 = DiscordRoles([ROLE_ALPHA, ROLE_BRAVO])
|
||||
roles_3 = roles_1.difference(roles_2)
|
||||
expected = DiscordRoles([])
|
||||
self.assertEqual(roles_3, expected)
|
||||
# given
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
roles_1 = RolesSet([role_a, role_b])
|
||||
roles_2 = RolesSet([role_a, role_b])
|
||||
# when
|
||||
result = roles_1.difference(roles_2)
|
||||
# then
|
||||
self.assertEqual(result, RolesSet([]))
|
||||
|
@ -0,0 +1,156 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
|
||||
from ..models import Guild, GuildMember, Role, User
|
||||
from .factories import create_guild, create_guild_member, create_role, create_user
|
||||
|
||||
|
||||
def _fetch_example_objects() -> dict:
|
||||
path = Path(__file__).parent / "example_objects.json"
|
||||
with path.open("r", encoding="utf-8") as fp:
|
||||
return json.load(fp)
|
||||
|
||||
|
||||
class TestUser(TestCase):
|
||||
def test_should_create_new_object(self):
|
||||
# when
|
||||
obj = User(id="42", username=123, discriminator=456)
|
||||
# then
|
||||
self.assertEqual(obj.id, 42)
|
||||
self.assertEqual(obj.username, "123")
|
||||
self.assertTrue(obj.discriminator, "456")
|
||||
|
||||
def test_should_create_from_dict(self):
|
||||
# given
|
||||
data = example_objects["users"]["80351110224678912"]
|
||||
# when
|
||||
obj = User.from_dict(data)
|
||||
# then
|
||||
self.assertEqual(obj.id, 80351110224678912)
|
||||
self.assertEqual(obj.username, "Nelly")
|
||||
self.assertEqual(obj.discriminator, "1337")
|
||||
|
||||
|
||||
class TestRole(TestCase):
|
||||
def test_should_create_new_object_with_defaults(self):
|
||||
# when
|
||||
obj = Role(id="42", name="x" * 110)
|
||||
# then
|
||||
self.assertEqual(obj.id, 42)
|
||||
self.assertEqual(obj.name, "x" * 100)
|
||||
self.assertFalse(obj.managed)
|
||||
|
||||
def test_should_create_new_object(self):
|
||||
# when
|
||||
obj = Role(id=42, name="name", managed=1)
|
||||
# then
|
||||
self.assertEqual(obj.id, 42)
|
||||
self.assertEqual(obj.name, "name")
|
||||
self.assertTrue(obj.managed)
|
||||
|
||||
def test_should_create_from_dict(self):
|
||||
# given
|
||||
data = example_objects["roles"]["41771983423143936"]
|
||||
# when
|
||||
obj = Role.from_dict(data)
|
||||
# then
|
||||
self.assertEqual(obj.id, 41771983423143936)
|
||||
self.assertEqual(obj.name, "WE DEM BOYZZ!!!!!!")
|
||||
self.assertFalse(obj.managed)
|
||||
|
||||
def test_should_convert_to_dict(self):
|
||||
# given
|
||||
role = create_role(id=42, name="Special Name", managed=True)
|
||||
# when/then
|
||||
self.assertDictEqual(
|
||||
role.asdict(), {"id": 42, "name": "Special Name", "managed": True}
|
||||
)
|
||||
|
||||
def test_sanitize_role_name(self):
|
||||
# given
|
||||
role_name_input = "x" * 110
|
||||
role_name_expected = "x" * 100
|
||||
# when
|
||||
result = Role.sanitize_name(role_name_input)
|
||||
# then
|
||||
self.assertEqual(result, role_name_expected)
|
||||
|
||||
|
||||
class TestGuild(TestCase):
|
||||
def test_should_create_new_object(self):
|
||||
# given
|
||||
role_a = create_role()
|
||||
# when
|
||||
obj = Guild(id="42", name=123, roles=[role_a])
|
||||
# then
|
||||
self.assertEqual(obj.id, 42)
|
||||
self.assertEqual(obj.name, "123")
|
||||
self.assertEqual(obj.roles, frozenset([role_a]))
|
||||
|
||||
def test_should_create_from_dict(self):
|
||||
# given
|
||||
data = example_objects["guilds"]["2909267986263572999"]
|
||||
# when
|
||||
obj = Guild.from_dict(data)
|
||||
# then
|
||||
self.assertEqual(obj.id, 2909267986263572999)
|
||||
self.assertEqual(obj.name, "Mason's Test Server")
|
||||
(first_role,) = obj.roles
|
||||
self.assertEqual(first_role.id, 2909267986263572999)
|
||||
|
||||
def test_should_raise_error_when_role_type_is_wrong(self):
|
||||
with self.assertRaises(TypeError):
|
||||
create_guild(roles=[create_role(), "invalid"])
|
||||
|
||||
|
||||
class TestGuildMember(TestCase):
|
||||
def test_should_create_new_object(self):
|
||||
# given
|
||||
user = create_user()
|
||||
# when
|
||||
obj = GuildMember(user=user, nick="x" * 40, roles=[1, 2])
|
||||
# then
|
||||
self.assertEqual(obj.user, user)
|
||||
self.assertEqual(obj.nick, "x" * 32)
|
||||
self.assertEqual(obj.roles, frozenset([1, 2]))
|
||||
|
||||
def test_should_create_from_dict_empty(self):
|
||||
# given
|
||||
data = example_objects["guildMembers"]["1"]
|
||||
# when
|
||||
obj = GuildMember.from_dict(data)
|
||||
# then
|
||||
self.assertIsNone(obj.user)
|
||||
self.assertSetEqual(obj.roles, set())
|
||||
self.assertIsNone(obj.nick)
|
||||
|
||||
def test_should_create_from_dict_full(self):
|
||||
# given
|
||||
data = example_objects["guildMembers"]["2"]
|
||||
# when
|
||||
obj = GuildMember.from_dict(data)
|
||||
# then
|
||||
self.assertEqual(obj.user.username, "Nelly")
|
||||
self.assertSetEqual(obj.roles, {197150972374548480, 41771983423143936})
|
||||
self.assertEqual(obj.nick, "Nelly the great")
|
||||
|
||||
def test_should_raise_error_when_user_type_is_wrong(self):
|
||||
with self.assertRaises(TypeError):
|
||||
create_guild_member(user="invalid")
|
||||
|
||||
def test_should_raise_error_when_role_type_is_wrong(self):
|
||||
with self.assertRaises(TypeError):
|
||||
GuildMember(roles=[1, 2, "invalid"])
|
||||
|
||||
def test_sanitize_nick(self):
|
||||
# given
|
||||
nick_input = "x" * 40
|
||||
nick_expected = "x" * 32
|
||||
# when
|
||||
result = GuildMember.sanitize_nick(nick_input)
|
||||
# then
|
||||
self.assertEqual(result, nick_expected)
|
||||
|
||||
|
||||
example_objects = _fetch_example_objects()
|
@ -1,30 +1,33 @@
|
||||
import logging
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from requests_oauthlib import OAuth2Session
|
||||
from requests.exceptions import HTTPError
|
||||
from requests_oauthlib import OAuth2Session
|
||||
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
|
||||
from allianceauth.services.hooks import NameFormatter
|
||||
|
||||
from . import __title__
|
||||
from .app_settings import (
|
||||
DISCORD_APP_ID,
|
||||
DISCORD_APP_SECRET,
|
||||
DISCORD_BOT_TOKEN,
|
||||
DISCORD_CALLBACK_URL,
|
||||
DISCORD_GUILD_ID,
|
||||
DISCORD_SYNC_NAMES
|
||||
DISCORD_SYNC_NAMES,
|
||||
)
|
||||
from .core import calculate_roles_for_user, create_bot_client
|
||||
from .core import group_to_role as core_group_to_role
|
||||
from .core import server_name as core_server_name
|
||||
from .core import user_formatted_nick
|
||||
from .discord_client import (
|
||||
DISCORD_OAUTH_BASE_URL,
|
||||
DISCORD_OAUTH_TOKEN_URL,
|
||||
DiscordApiBackoff,
|
||||
DiscordClient,
|
||||
)
|
||||
from .discord_client import DiscordClient
|
||||
from .discord_client.exceptions import DiscordClientException, DiscordApiBackoff
|
||||
from .discord_client.helpers import match_or_create_roles_from_names
|
||||
from .utils import LoggerAddTag
|
||||
|
||||
|
||||
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
||||
|
||||
|
||||
@ -56,79 +59,68 @@ class DiscordUserManager(models.Manager):
|
||||
Returns: True on success, else False or raises exception
|
||||
"""
|
||||
try:
|
||||
nickname = self.user_formatted_nick(user) if DISCORD_SYNC_NAMES else None
|
||||
group_names = self.user_group_names(user)
|
||||
nickname = user_formatted_nick(user) if DISCORD_SYNC_NAMES else None
|
||||
access_token = self._exchange_auth_code_for_token(authorization_code)
|
||||
user_client = DiscordClient(access_token, is_rate_limited=is_rate_limited)
|
||||
discord_user = user_client.current_user()
|
||||
user_id = discord_user['id']
|
||||
bot_client = self._bot_client(is_rate_limited=is_rate_limited)
|
||||
|
||||
if group_names:
|
||||
role_ids = match_or_create_roles_from_names(
|
||||
client=bot_client,
|
||||
guild_id=DISCORD_GUILD_ID,
|
||||
role_names=group_names
|
||||
).ids()
|
||||
else:
|
||||
role_ids = None
|
||||
|
||||
created = bot_client.add_guild_member(
|
||||
guild_id=DISCORD_GUILD_ID,
|
||||
user_id=user_id,
|
||||
access_token=access_token,
|
||||
role_ids=role_ids,
|
||||
nick=nickname
|
||||
bot_client = create_bot_client(is_rate_limited=is_rate_limited)
|
||||
roles, changed = calculate_roles_for_user(
|
||||
user=user, client=bot_client, discord_uid=discord_user.id
|
||||
)
|
||||
if created is not False:
|
||||
if created is None:
|
||||
logger.debug(
|
||||
"User %s with Discord ID %s is already a member. Forcing a Refresh",
|
||||
if changed is None:
|
||||
# Handle new member
|
||||
created = bot_client.add_guild_member(
|
||||
guild_id=DISCORD_GUILD_ID,
|
||||
user_id=discord_user.id,
|
||||
access_token=access_token,
|
||||
role_ids=list(roles.ids()),
|
||||
nick=nickname
|
||||
)
|
||||
if not created:
|
||||
logger.warning(
|
||||
"Failed to add user %s with Discord ID %s to Discord server",
|
||||
user,
|
||||
user_id,
|
||||
discord_user.id,
|
||||
)
|
||||
|
||||
# Force an update cause the discord API won't do it for us.
|
||||
if role_ids:
|
||||
role_ids = list(role_ids)
|
||||
|
||||
updated = bot_client.modify_guild_member(
|
||||
guild_id=DISCORD_GUILD_ID,
|
||||
user_id=user_id,
|
||||
role_ids=role_ids,
|
||||
nick=nickname
|
||||
)
|
||||
|
||||
if not updated:
|
||||
# Could not update the new user so fail.
|
||||
logger.warning(
|
||||
"Failed to add user %s with Discord ID %s to Discord server",
|
||||
user,
|
||||
user_id,
|
||||
)
|
||||
return False
|
||||
|
||||
self.update_or_create(
|
||||
user=user,
|
||||
defaults={
|
||||
'uid': user_id,
|
||||
'username': discord_user['username'][:32],
|
||||
'discriminator': discord_user['discriminator'][:4],
|
||||
'activated': now()
|
||||
}
|
||||
)
|
||||
logger.info(
|
||||
"Added user %s with Discord ID %s to Discord server", user, user_id
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
else:
|
||||
logger.warning(
|
||||
"Failed to add user %s with Discord ID %s to Discord server",
|
||||
# Handle existing member
|
||||
logger.debug(
|
||||
"User %s with Discord ID %s is already a member. Forcing a Refresh",
|
||||
user,
|
||||
user_id,
|
||||
discord_user.id,
|
||||
)
|
||||
return False
|
||||
# Force an update cause the discord API won't do it for us.
|
||||
updated = bot_client.modify_guild_member(
|
||||
guild_id=DISCORD_GUILD_ID,
|
||||
user_id=discord_user.id,
|
||||
role_ids=list(roles.ids()),
|
||||
nick=nickname
|
||||
)
|
||||
if not updated:
|
||||
# Could not update the new user so fail.
|
||||
logger.warning(
|
||||
"Failed to add user %s with Discord ID %s to Discord server",
|
||||
user,
|
||||
discord_user.id,
|
||||
)
|
||||
return False
|
||||
|
||||
self.update_or_create(
|
||||
user=user,
|
||||
defaults={
|
||||
'uid': discord_user.id,
|
||||
'username': discord_user.username[:32],
|
||||
'discriminator': discord_user.discriminator[:4],
|
||||
'activated': now()
|
||||
}
|
||||
)
|
||||
logger.info(
|
||||
"Added user %s with Discord ID %s to Discord server",
|
||||
user,
|
||||
discord_user.id
|
||||
)
|
||||
return True
|
||||
|
||||
except (HTTPError, ConnectionError, DiscordApiBackoff) as ex:
|
||||
logger.exception(
|
||||
@ -136,31 +128,6 @@ class DiscordUserManager(models.Manager):
|
||||
)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def user_formatted_nick(user: User) -> str:
|
||||
"""returns the name of the given users main character with name formatting
|
||||
or None if user has no main
|
||||
"""
|
||||
from .auth_hooks import DiscordService
|
||||
|
||||
if user.profile.main_character:
|
||||
return NameFormatter(DiscordService(), user).format_name()
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def user_group_names(user: User, state_name: str = None) -> list:
|
||||
"""returns list of group names plus state the given user is a member of"""
|
||||
if not state_name:
|
||||
state_name = user.profile.state.name
|
||||
group_names = (
|
||||
[group.name for group in user.groups.all()] + [state_name]
|
||||
)
|
||||
logger.debug(
|
||||
"Group names for roles updates of user %s are: %s", user, group_names
|
||||
)
|
||||
return group_names
|
||||
|
||||
def user_has_account(self, user: User) -> bool:
|
||||
"""Returns True if the user has an Discord account, else False
|
||||
|
||||
@ -178,60 +145,41 @@ class DiscordUserManager(models.Manager):
|
||||
'permissions': str(cls.BOT_PERMISSIONS)
|
||||
|
||||
})
|
||||
return f'{DiscordClient.OAUTH_BASE_URL}?{params}'
|
||||
return f'{DISCORD_OAUTH_BASE_URL}?{params}'
|
||||
|
||||
@classmethod
|
||||
def generate_oauth_redirect_url(cls) -> str:
|
||||
oauth = OAuth2Session(
|
||||
DISCORD_APP_ID, redirect_uri=DISCORD_CALLBACK_URL, scope=cls.SCOPES
|
||||
)
|
||||
url, state = oauth.authorization_url(DiscordClient.OAUTH_BASE_URL)
|
||||
url, _ = oauth.authorization_url(DISCORD_OAUTH_BASE_URL)
|
||||
return url
|
||||
|
||||
@staticmethod
|
||||
def _exchange_auth_code_for_token(authorization_code: str) -> str:
|
||||
oauth = OAuth2Session(DISCORD_APP_ID, redirect_uri=DISCORD_CALLBACK_URL)
|
||||
token = oauth.fetch_token(
|
||||
DiscordClient.OAUTH_TOKEN_URL,
|
||||
DISCORD_OAUTH_TOKEN_URL,
|
||||
client_secret=DISCORD_APP_SECRET,
|
||||
code=authorization_code
|
||||
)
|
||||
logger.debug("Received token from OAuth")
|
||||
return token['access_token']
|
||||
|
||||
@classmethod
|
||||
def server_name(cls, use_cache: bool = True) -> str:
|
||||
"""returns the name of the current Discord server
|
||||
or an empty string if the name could not be retrieved
|
||||
@staticmethod
|
||||
def group_to_role(group: Group) -> dict:
|
||||
"""Fetch the Discord role matching the given Django group by name.
|
||||
|
||||
Params:
|
||||
- use_cache: When set False will force an API call to get the server name
|
||||
Returns:
|
||||
- Discord role as dict
|
||||
- empty dict if no matching role found
|
||||
"""
|
||||
try:
|
||||
server_name = cls._bot_client().guild_name(
|
||||
guild_id=DISCORD_GUILD_ID, use_cache=use_cache
|
||||
)
|
||||
except (HTTPError, DiscordClientException):
|
||||
server_name = ""
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Unexpected error when trying to retrieve the server name from Discord",
|
||||
exc_info=True
|
||||
)
|
||||
server_name = ""
|
||||
|
||||
return server_name
|
||||
|
||||
@classmethod
|
||||
def group_to_role(cls, group: Group) -> dict:
|
||||
"""returns the Discord role matching the given Django group by name
|
||||
or an empty dict() if no matching role exist
|
||||
"""
|
||||
return cls._bot_client().match_role_from_name(
|
||||
guild_id=DISCORD_GUILD_ID, role_name=group.name
|
||||
)
|
||||
role = core_group_to_role(group)
|
||||
return role.asdict() if role else dict()
|
||||
|
||||
@staticmethod
|
||||
def _bot_client(is_rate_limited: bool = True) -> DiscordClient:
|
||||
"""returns a bot client for access to the Discord API"""
|
||||
return DiscordClient(DISCORD_BOT_TOKEN, is_rate_limited=is_rate_limited)
|
||||
def server_name(use_cache: bool = True) -> str:
|
||||
"""Fetches the name of the current Discord server.
|
||||
This method is kept to ensure backwards compatibility of this API.
|
||||
"""
|
||||
return core_server_name(use_cache)
|
||||
|
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
@ -6,13 +7,17 @@ from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from allianceauth.groupmanagement.models import ReservedGroupName
|
||||
from allianceauth.notifications import notify
|
||||
|
||||
from . import __title__
|
||||
from .app_settings import DISCORD_GUILD_ID
|
||||
from .discord_client import DiscordApiBackoff, DiscordClient, DiscordRoles
|
||||
from .discord_client.helpers import match_or_create_roles_from_names
|
||||
from .core import (
|
||||
create_bot_client,
|
||||
default_bot_client,
|
||||
calculate_roles_for_user,
|
||||
user_formatted_nick
|
||||
)
|
||||
from .discord_client import DiscordApiBackoff
|
||||
from .managers import DiscordUserManager
|
||||
from .utils import LoggerAddTag
|
||||
|
||||
@ -21,14 +26,13 @@ logger = LoggerAddTag(logging.getLogger(__name__), __title__)
|
||||
|
||||
|
||||
class DiscordUser(models.Model):
|
||||
|
||||
USER_RELATED_NAME = 'discord'
|
||||
"""The Discord user account of an Auth user."""
|
||||
|
||||
user = models.OneToOneField(
|
||||
User,
|
||||
primary_key=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name=USER_RELATED_NAME,
|
||||
related_name='discord',
|
||||
help_text='Auth user owning this Discord account'
|
||||
)
|
||||
uid = models.BigIntegerField(
|
||||
@ -80,24 +84,21 @@ class DiscordUser(models.Model):
|
||||
- False on error or raises exception
|
||||
"""
|
||||
if not nickname:
|
||||
nickname = DiscordUser.objects.user_formatted_nick(self.user)
|
||||
if nickname:
|
||||
client = DiscordUser.objects._bot_client()
|
||||
success = client.modify_guild_member(
|
||||
guild_id=DISCORD_GUILD_ID,
|
||||
user_id=self.uid,
|
||||
nick=nickname
|
||||
)
|
||||
if success:
|
||||
logger.info('Nickname for %s has been updated', self.user)
|
||||
else:
|
||||
logger.warning('Failed to update nickname for %s', self.user)
|
||||
return success
|
||||
|
||||
else:
|
||||
nickname = user_formatted_nick(self.user)
|
||||
if not nickname:
|
||||
return False
|
||||
success = default_bot_client.modify_guild_member(
|
||||
guild_id=DISCORD_GUILD_ID,
|
||||
user_id=self.uid,
|
||||
nick=nickname
|
||||
)
|
||||
if success:
|
||||
logger.info('Nickname for %s has been updated', self.user)
|
||||
else:
|
||||
logger.warning('Failed to update nickname for %s', self.user)
|
||||
return success
|
||||
|
||||
def update_groups(self, state_name: str = None) -> bool:
|
||||
def update_groups(self, state_name: str = None) -> Optional[bool]:
|
||||
"""update groups for a user based on his current group memberships.
|
||||
Will add or remove roles of a user as needed.
|
||||
|
||||
@ -109,57 +110,18 @@ class DiscordUser(models.Model):
|
||||
- None if user is no longer a member of the Discord server
|
||||
- False on error or raises exception
|
||||
"""
|
||||
client = DiscordUser.objects._bot_client()
|
||||
member_roles = self._determine_member_roles(client)
|
||||
if member_roles is None:
|
||||
new_roles, is_changed = calculate_roles_for_user(
|
||||
user=self.user,
|
||||
client=default_bot_client,
|
||||
discord_uid=self.uid,
|
||||
state_name=state_name
|
||||
)
|
||||
if is_changed is None:
|
||||
logger.debug('User is not a member of this guild %s', self.user)
|
||||
return None
|
||||
return self._update_roles_if_needed(client, state_name, member_roles)
|
||||
|
||||
def _determine_member_roles(self, client: DiscordClient) -> DiscordRoles:
|
||||
"""Determine the roles of the current member / user."""
|
||||
member_info = client.guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid)
|
||||
if member_info is None:
|
||||
return None # User is no longer a member
|
||||
guild_roles = DiscordRoles(client.guild_roles(guild_id=DISCORD_GUILD_ID))
|
||||
logger.debug('Current guild roles: %s', guild_roles.ids())
|
||||
if 'roles' in member_info:
|
||||
if not guild_roles.has_roles(member_info['roles']):
|
||||
guild_roles = DiscordRoles(
|
||||
client.guild_roles(guild_id=DISCORD_GUILD_ID, use_cache=False)
|
||||
)
|
||||
if not guild_roles.has_roles(member_info['roles']):
|
||||
raise RuntimeError(
|
||||
'Member {} has unknown roles: {}'.format(
|
||||
self.user,
|
||||
set(member_info['roles']).difference(guild_roles.ids())
|
||||
)
|
||||
)
|
||||
return guild_roles.subset(member_info['roles'])
|
||||
raise RuntimeError('member_info from %s is not valid' % self.user)
|
||||
|
||||
def _update_roles_if_needed(
|
||||
self, client: DiscordClient, state_name: str, member_roles: DiscordRoles
|
||||
) -> bool:
|
||||
"""Update the roles of this member/user if needed."""
|
||||
requested_roles = match_or_create_roles_from_names(
|
||||
client=client,
|
||||
guild_id=DISCORD_GUILD_ID,
|
||||
role_names=DiscordUser.objects.user_group_names(
|
||||
user=self.user, state_name=state_name
|
||||
)
|
||||
)
|
||||
logger.debug(
|
||||
'Requested roles for user %s: %s', self.user, requested_roles.ids()
|
||||
)
|
||||
logger.debug('Current roles user %s: %s', self.user, member_roles.ids())
|
||||
reserved_role_names = ReservedGroupName.objects.values_list("name", flat=True)
|
||||
member_roles_reserved = member_roles.subset(role_names=reserved_role_names)
|
||||
member_roles_managed = member_roles.subset(managed_only=True)
|
||||
member_roles_persistent = member_roles_managed.union(member_roles_reserved)
|
||||
if requested_roles != member_roles.difference(member_roles_persistent):
|
||||
if is_changed:
|
||||
logger.debug('Need to update roles for user %s', self.user)
|
||||
new_roles = requested_roles.union(member_roles_persistent)
|
||||
success = client.modify_guild_member(
|
||||
success = default_bot_client.modify_guild_member(
|
||||
guild_id=DISCORD_GUILD_ID,
|
||||
user_id=self.uid,
|
||||
role_ids=list(new_roles.ids())
|
||||
@ -172,7 +134,7 @@ class DiscordUser(models.Model):
|
||||
logger.info('No need to update roles for user %s', self.user)
|
||||
return True
|
||||
|
||||
def update_username(self) -> bool:
|
||||
def update_username(self) -> Optional[bool]:
|
||||
"""Updates the username incl. the discriminator
|
||||
from the Discord server and saves it
|
||||
|
||||
@ -181,8 +143,9 @@ class DiscordUser(models.Model):
|
||||
- None if user is no longer a member of the Discord server
|
||||
- False on error or raises exception
|
||||
"""
|
||||
client = DiscordUser.objects._bot_client()
|
||||
user_info = client.guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid)
|
||||
user_info = default_bot_client.guild_member(
|
||||
guild_id=DISCORD_GUILD_ID, user_id=self.uid
|
||||
)
|
||||
if user_info is None:
|
||||
success = None
|
||||
elif (
|
||||
@ -206,7 +169,7 @@ class DiscordUser(models.Model):
|
||||
notify_user: bool = False,
|
||||
is_rate_limited: bool = True,
|
||||
handle_api_exceptions: bool = False
|
||||
) -> bool:
|
||||
) -> Optional[bool]:
|
||||
"""Deletes the Discount user both on the server and locally
|
||||
|
||||
Params:
|
||||
@ -221,7 +184,7 @@ class DiscordUser(models.Model):
|
||||
"""
|
||||
try:
|
||||
_user = self.user
|
||||
client = DiscordUser.objects._bot_client(is_rate_limited=is_rate_limited)
|
||||
client = create_bot_client(is_rate_limited=is_rate_limited)
|
||||
success = client.remove_guild_member(
|
||||
guild_id=DISCORD_GUILD_ID, user_id=self.uid
|
||||
)
|
||||
@ -241,15 +204,13 @@ class DiscordUser(models.Model):
|
||||
)
|
||||
logger.info('Account for user %s was deleted.', _user)
|
||||
return True
|
||||
else:
|
||||
logger.debug('Account for user %s was already deleted.', _user)
|
||||
return None
|
||||
logger.debug('Account for user %s was already deleted.', _user)
|
||||
return None
|
||||
|
||||
else:
|
||||
logger.warning(
|
||||
'Failed to remove user %s from the Discord server', _user
|
||||
)
|
||||
return False
|
||||
logger.warning(
|
||||
'Failed to remove user %s from the Discord server', _user
|
||||
)
|
||||
return False
|
||||
|
||||
except (HTTPError, ConnectionError, DiscordApiBackoff) as ex:
|
||||
if handle_api_exceptions:
|
||||
@ -257,5 +218,4 @@ class DiscordUser(models.Model):
|
||||
'Failed to remove user %s from Discord server: %s',self.user, ex
|
||||
)
|
||||
return False
|
||||
else:
|
||||
raise ex
|
||||
raise ex
|
||||
|
@ -1,19 +1,6 @@
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from ..discord_client.tests import ( # noqa
|
||||
TEST_GUILD_ID,
|
||||
TEST_USER_ID,
|
||||
TEST_USER_NAME,
|
||||
TEST_USER_DISCRIMINATOR,
|
||||
create_role,
|
||||
ROLE_ALPHA,
|
||||
ROLE_BRAVO,
|
||||
ROLE_CHARLIE,
|
||||
ROLE_CHARLIE_2,
|
||||
ROLE_MIKE,
|
||||
ALL_ROLES,
|
||||
create_user_info
|
||||
)
|
||||
|
||||
DEFAULT_AUTH_GROUP = 'Member'
|
||||
MODULE_PATH = 'allianceauth.services.modules.discord'
|
||||
|
@ -1,26 +1,32 @@
|
||||
from django.test import TestCase, RequestFactory
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import RequestFactory
|
||||
from django.utils.timezone import now
|
||||
|
||||
from allianceauth.authentication.models import CharacterOwnership
|
||||
from allianceauth.eveonline.models import (
|
||||
EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||
EveAllianceInfo,
|
||||
EveCharacter,
|
||||
EveCorporationInfo,
|
||||
)
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
from ....admin import (
|
||||
MainAllianceFilter,
|
||||
MainCorporationsFilter,
|
||||
ServicesUserAdmin,
|
||||
user_main_organization,
|
||||
user_profile_pic,
|
||||
user_username,
|
||||
user_main_organization,
|
||||
ServicesUserAdmin,
|
||||
MainCorporationsFilter,
|
||||
MainAllianceFilter
|
||||
)
|
||||
from ..admin import DiscordUserAdmin
|
||||
from ..models import DiscordUser
|
||||
from . import MODULE_PATH
|
||||
|
||||
|
||||
class TestDataMixin(TestCase):
|
||||
class TestDataMixin(NoSocketsTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -168,7 +174,7 @@ class TestDataMixin(TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestColumnRendering(TestDataMixin, TestCase):
|
||||
class TestColumnRendering(TestDataMixin, NoSocketsTestCase):
|
||||
|
||||
def test_user_profile_pic_u1(self):
|
||||
expected = (
|
||||
@ -229,7 +235,7 @@ class TestColumnRendering(TestDataMixin, TestCase):
|
||||
# actions
|
||||
|
||||
|
||||
class TestFilters(TestDataMixin, TestCase):
|
||||
class TestFilters(TestDataMixin, NoSocketsTestCase):
|
||||
|
||||
def test_filter_main_corporations(self):
|
||||
|
||||
@ -287,3 +293,16 @@ class TestFilters(TestDataMixin, TestCase):
|
||||
queryset = changelist.get_queryset(request)
|
||||
expected = [self.user_1.discord]
|
||||
self.assertSetEqual(set(queryset), set(expected))
|
||||
|
||||
|
||||
@patch(MODULE_PATH + ".admin.DiscordUser.delete_user")
|
||||
class TestDeleteQueryset(TestDataMixin, NoSocketsTestCase):
|
||||
def test_should_delete_all_objects(self, mock_delete_user):
|
||||
# given
|
||||
request = self.factory.get('/')
|
||||
request.user = self.user_1
|
||||
queryset = DiscordUser.objects.filter(user__in=[self.user_2, self.user_3])
|
||||
# when
|
||||
self.modeladmin.delete_queryset(request, queryset)
|
||||
# then
|
||||
self.assertEqual(mock_delete_user.call_count, 2)
|
||||
|
16
allianceauth/services/modules/discord/tests/test_api.py
Normal file
16
allianceauth/services/modules/discord/tests/test_api.py
Normal file
@ -0,0 +1,16 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
from ..api import discord_guild_id
|
||||
from . import MODULE_PATH
|
||||
|
||||
|
||||
class TestDiscordGuildId(NoSocketsTestCase):
|
||||
@patch(MODULE_PATH + ".api.DISCORD_GUILD_ID", "123")
|
||||
def test_should_return_guild_id_when_configured(self):
|
||||
self.assertEqual(discord_guild_id(), 123)
|
||||
|
||||
@patch(MODULE_PATH + ".api.DISCORD_GUILD_ID", "")
|
||||
def test_should_return_none_when_not_configured(self):
|
||||
self.assertIsNone(discord_guild_id())
|
@ -1,23 +1,23 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.test import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from allianceauth.notifications.models import Notification
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
from . import TEST_USER_NAME, TEST_USER_ID, add_permissions_to_members, MODULE_PATH
|
||||
from ..auth_hooks import DiscordService
|
||||
from ..discord_client import DiscordClient
|
||||
from ..discord_client.tests.factories import TEST_USER_ID, TEST_USER_NAME
|
||||
from ..models import DiscordUser
|
||||
from ..utils import set_logger_to_file
|
||||
|
||||
from . import MODULE_PATH, add_permissions_to_members
|
||||
|
||||
logger = set_logger_to_file(MODULE_PATH + '.auth_hooks', __file__)
|
||||
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
class TestDiscordService(TestCase):
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||
class TestDiscordService(NoSocketsTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.member = AuthUtils.create_member(TEST_USER_NAME)
|
||||
@ -64,11 +64,11 @@ class TestDiscordService(TestCase):
|
||||
|
||||
@patch(MODULE_PATH + '.models.notify')
|
||||
@patch(MODULE_PATH + '.tasks.DiscordUser')
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
@patch(MODULE_PATH + '.models.create_bot_client')
|
||||
def test_validate_user(
|
||||
self, mock_DiscordClient, mock_DiscordUser, mock_notify
|
||||
self, mock_create_bot_client, mock_DiscordUser, mock_notify
|
||||
):
|
||||
mock_DiscordClient.return_value.remove_guild_member.return_value = True
|
||||
mock_create_bot_client.return_value.remove_guild_member.return_value = True
|
||||
|
||||
# Test member is not deleted
|
||||
service = self.service()
|
||||
@ -92,33 +92,38 @@ class TestDiscordService(TestCase):
|
||||
service.sync_nicknames_bulk([self.member])
|
||||
self.assertTrue(mock_update_nicknames_bulk.delay.called)
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
def test_delete_user_is_member(self, mock_DiscordClient):
|
||||
mock_DiscordClient.return_value.remove_guild_member.return_value = True
|
||||
|
||||
@patch(MODULE_PATH + '.models.create_bot_client')
|
||||
def test_delete_user_is_member(self, mock_create_bot_client):
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.return_value = True
|
||||
service = self.service()
|
||||
# when
|
||||
service.delete_user(self.member, notify_user=True)
|
||||
|
||||
self.assertTrue(mock_DiscordClient.return_value.remove_guild_member.called)
|
||||
# then
|
||||
self.assertTrue(mock_create_bot_client.return_value.remove_guild_member.called)
|
||||
self.assertFalse(DiscordUser.objects.filter(user=self.member).exists())
|
||||
self.assertTrue(Notification.objects.filter(user=self.member).exists())
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
def test_delete_user_is_not_member(self, mock_DiscordClient):
|
||||
mock_DiscordClient.return_value.remove_guild_member.return_value = True
|
||||
|
||||
@patch(MODULE_PATH + '.models.create_bot_client')
|
||||
def test_delete_user_is_not_member(self, mock_create_bot_client):
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.return_value = True
|
||||
service = self.service()
|
||||
# when
|
||||
service.delete_user(self.none_member)
|
||||
# then
|
||||
self.assertFalse(mock_create_bot_client.return_value.remove_guild_member.called)
|
||||
|
||||
self.assertFalse(mock_DiscordClient.return_value.remove_guild_member.called)
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
def test_render_services_ctrl_with_username(self, mock_DiscordClient):
|
||||
@patch(MODULE_PATH + '.auth_hooks.server_name')
|
||||
def test_render_services_ctrl_with_username(self, mock_server_name):
|
||||
# given
|
||||
mock_server_name.return_value = "My server"
|
||||
service = self.service()
|
||||
request = self.factory.get('/services/')
|
||||
request.user = self.member
|
||||
|
||||
# when
|
||||
response = service.render_services_ctrl(request)
|
||||
# then
|
||||
self.assertTemplateUsed(service.service_ctrl_template)
|
||||
self.assertIn('/discord/reset/', response)
|
||||
self.assertIn('/discord/deactivate/', response)
|
||||
@ -130,15 +135,18 @@ class TestDiscordService(TestCase):
|
||||
response = service.render_services_ctrl(request)
|
||||
self.assertIn('/discord/activate/', response)
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
def test_render_services_ctrl_wo_username(self, mock_DiscordClient):
|
||||
@patch(MODULE_PATH + '.auth_hooks.server_name')
|
||||
def test_render_services_ctrl_wo_username(self, mock_server_name):
|
||||
# given
|
||||
mock_server_name.return_value = "My server"
|
||||
my_member = AuthUtils.create_member('John Doe')
|
||||
DiscordUser.objects.create(user=my_member, uid=111222333)
|
||||
service = self.service()
|
||||
request = self.factory.get('/services/')
|
||||
request.user = my_member
|
||||
|
||||
# when
|
||||
response = service.render_services_ctrl(request)
|
||||
# then
|
||||
self.assertTemplateUsed(service.service_ctrl_template)
|
||||
self.assertIn('/discord/reset/', response)
|
||||
self.assertIn('/discord/deactivate/', response)
|
||||
|
221
allianceauth/services/modules/discord/tests/test_core.py
Normal file
221
allianceauth/services/modules/discord/tests/test_core.py
Normal file
@ -0,0 +1,221 @@
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from allianceauth.groupmanagement.models import ReservedGroupName
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
from ..core import (
|
||||
_user_group_names,
|
||||
calculate_roles_for_user,
|
||||
group_to_role,
|
||||
server_name,
|
||||
user_formatted_nick,
|
||||
)
|
||||
from ..discord_client import DiscordApiBackoff, DiscordClient, RolesSet
|
||||
from ..discord_client.tests.factories import TEST_USER_NAME, create_role
|
||||
from . import MODULE_PATH, TEST_MAIN_ID, TEST_MAIN_NAME
|
||||
|
||||
|
||||
class TestUserGroupNames(NoSocketsTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.group_1 = Group.objects.create(name="Group 1")
|
||||
cls.group_2 = Group.objects.create(name="Group 2")
|
||||
|
||||
def setUp(self):
|
||||
self.user = AuthUtils.create_member(TEST_USER_NAME)
|
||||
|
||||
def test_return_groups_and_state_names_for_user(self):
|
||||
self.user.groups.add(self.group_1)
|
||||
result = _user_group_names(self.user)
|
||||
expected = ["Group 1", "Member"]
|
||||
self.assertSetEqual(set(result), set(expected))
|
||||
|
||||
def test_return_state_only_if_user_has_no_groups(self):
|
||||
result = _user_group_names(self.user)
|
||||
expected = ["Member"]
|
||||
self.assertSetEqual(set(result), set(expected))
|
||||
|
||||
|
||||
class TestUserFormattedNick(NoSocketsTestCase):
|
||||
def setUp(self):
|
||||
self.user = AuthUtils.create_user(TEST_USER_NAME)
|
||||
|
||||
def test_return_nick_when_user_has_main(self):
|
||||
AuthUtils.add_main_character_2(self.user, TEST_MAIN_NAME, TEST_MAIN_ID)
|
||||
result = user_formatted_nick(self.user)
|
||||
expected = TEST_MAIN_NAME
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_return_none_if_user_has_no_main(self):
|
||||
result = user_formatted_nick(self.user)
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + ".core.default_bot_client", spec=True)
|
||||
class TestRoleForGroup(NoSocketsTestCase):
|
||||
def test_return_role_if_found(self, mock_bot_client):
|
||||
# given
|
||||
role = create_role(name="alpha")
|
||||
mock_bot_client.match_role_from_name.side_effect = (
|
||||
lambda guild_id, role_name: role if role.name == role_name else None
|
||||
)
|
||||
group = Group.objects.create(name="alpha")
|
||||
# when/then
|
||||
self.assertEqual(group_to_role(group), role)
|
||||
|
||||
def test_return_empty_dict_if_not_found(self, mock_bot_client):
|
||||
# given
|
||||
role = create_role(name="alpha")
|
||||
mock_bot_client.match_role_from_name.side_effect = (
|
||||
lambda guild_id, role_name: role if role.name == role_name else None
|
||||
)
|
||||
group = Group.objects.create(name="unknown")
|
||||
# when/then
|
||||
self.assertIsNone(group_to_role(group))
|
||||
|
||||
|
||||
@patch(MODULE_PATH + ".core.default_bot_client", spec=True)
|
||||
@patch(MODULE_PATH + ".core.logger", spec=True)
|
||||
class TestServerName(NoSocketsTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user = AuthUtils.create_user(TEST_USER_NAME)
|
||||
|
||||
def test_returns_name_when_api_returns_it(self, mock_logger, mock_bot_client):
|
||||
# given
|
||||
my_server_name = "El Dorado"
|
||||
mock_bot_client.guild_name.return_value = my_server_name
|
||||
# when
|
||||
self.assertEqual(server_name(), my_server_name)
|
||||
# then
|
||||
self.assertFalse(mock_logger.warning.called)
|
||||
|
||||
def test_returns_empty_string_when_api_throws_http_error(
|
||||
self, mock_logger, mock_bot_client
|
||||
):
|
||||
mock_exception = HTTPError("Test exception")
|
||||
mock_exception.response = Mock(**{"status_code": 440})
|
||||
mock_bot_client.guild_name.side_effect = mock_exception
|
||||
|
||||
self.assertEqual(server_name(), "")
|
||||
self.assertFalse(mock_logger.warning.called)
|
||||
|
||||
def test_returns_empty_string_when_api_throws_service_error(
|
||||
self, mock_logger, mock_bot_client
|
||||
):
|
||||
mock_bot_client.guild_name.side_effect = DiscordApiBackoff(1000)
|
||||
|
||||
self.assertEqual(server_name(), "")
|
||||
self.assertFalse(mock_logger.warning.called)
|
||||
|
||||
def test_returns_empty_string_when_api_throws_unexpected_error(
|
||||
self, mock_logger, mock_bot_client
|
||||
):
|
||||
mock_bot_client.guild_name.side_effect = RuntimeError
|
||||
|
||||
self.assertEqual(server_name(), "")
|
||||
self.assertTrue(mock_logger.warning.called)
|
||||
|
||||
|
||||
class TestCalculateRolesForUser(NoSocketsTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user = AuthUtils.create_user(TEST_USER_NAME)
|
||||
|
||||
def test_should_return_roles_for_new_member(self):
|
||||
# given
|
||||
roles = RolesSet([create_role()])
|
||||
my_client = Mock(spec=DiscordClient)
|
||||
my_client.guild_member_roles.return_value = RolesSet([])
|
||||
my_client.match_or_create_roles_from_names_2.return_value = roles
|
||||
# when
|
||||
roles_calculated, changed = calculate_roles_for_user(self.user, my_client, 42)
|
||||
# then
|
||||
self.assertTrue(changed)
|
||||
self.assertEqual(roles_calculated, roles)
|
||||
|
||||
def test_should_return_changed_roles_for_existing_member(self):
|
||||
# given
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
roles_current = RolesSet([role_a])
|
||||
roles_matching = RolesSet([role_a, role_b])
|
||||
my_client = Mock(spec=DiscordClient)
|
||||
my_client.guild_member_roles.return_value = roles_current
|
||||
my_client.match_or_create_roles_from_names_2.return_value = roles_matching
|
||||
# when
|
||||
roles_calculated, changed = calculate_roles_for_user(self.user, my_client, 42)
|
||||
# then
|
||||
self.assertTrue(changed)
|
||||
self.assertEqual(roles_calculated, roles_matching)
|
||||
|
||||
def test_should_indicate_when_roles_are_unchanged(self):
|
||||
# given
|
||||
role_a = create_role()
|
||||
roles_current = RolesSet([role_a])
|
||||
roles_matching = RolesSet([role_a])
|
||||
my_client = Mock(spec=DiscordClient)
|
||||
my_client.guild_member_roles.return_value = roles_current
|
||||
my_client.match_or_create_roles_from_names_2.return_value = roles_matching
|
||||
# when
|
||||
roles_calculated, changed = calculate_roles_for_user(self.user, my_client, 42)
|
||||
# then
|
||||
self.assertFalse(changed)
|
||||
self.assertEqual(roles_calculated, roles_matching)
|
||||
|
||||
def test_should_indicate_when_user_is_no_guild_member(self):
|
||||
# given
|
||||
role_a = create_role()
|
||||
roles_matching = RolesSet([role_a])
|
||||
my_client = Mock(spec=DiscordClient)
|
||||
my_client.guild_member_roles.return_value = None
|
||||
my_client.match_or_create_roles_from_names_2.return_value = roles_matching
|
||||
# when
|
||||
roles_calculated, changed = calculate_roles_for_user(self.user, my_client, 42)
|
||||
# then
|
||||
self.assertIsNone(changed)
|
||||
self.assertEqual(roles_calculated, roles_matching)
|
||||
|
||||
def test_should_preserve_managed_roles_for_existing_member(self):
|
||||
# given
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
role_m = create_role(managed=True)
|
||||
roles_current = RolesSet([role_a, role_m])
|
||||
roles_matching = RolesSet([role_b])
|
||||
my_client = Mock(spec=DiscordClient)
|
||||
my_client.guild_member_roles.return_value = roles_current
|
||||
my_client.match_or_create_roles_from_names_2.return_value = roles_matching
|
||||
# when
|
||||
roles_calculated, changed = calculate_roles_for_user(self.user, my_client, 42)
|
||||
# then
|
||||
self.assertTrue(changed)
|
||||
self.assertEqual(roles_calculated, RolesSet([role_b, role_m]))
|
||||
|
||||
def test_should_preserve_reserved_roles_for_existing_member(self):
|
||||
# given
|
||||
role_a = create_role()
|
||||
role_b = create_role()
|
||||
role_c1 = create_role(name="charlie")
|
||||
role_c2 = create_role(name="Charlie")
|
||||
roles_current = RolesSet([role_a, role_c1, role_c2])
|
||||
roles_matching = RolesSet([role_b])
|
||||
my_client = Mock(spec=DiscordClient)
|
||||
my_client.guild_member_roles.return_value = roles_current
|
||||
my_client.match_or_create_roles_from_names_2.return_value = roles_matching
|
||||
ReservedGroupName.objects.create(
|
||||
name="charlie", reason="dummy", created_by="xyz"
|
||||
)
|
||||
# when
|
||||
roles_calculated, changed = calculate_roles_for_user(self.user, my_client, 42)
|
||||
# then
|
||||
self.assertTrue(changed)
|
||||
self.assertEqual(roles_calculated, RolesSet([role_b, role_c1, role_c2]))
|
@ -4,54 +4,67 @@ Testing all components of the service, with the exception of the Discord API.
|
||||
|
||||
Please note that these tests require Redis and will flush it
|
||||
"""
|
||||
from collections import namedtuple
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
from unittest.mock import patch, Mock
|
||||
from unittest.mock import Mock, patch
|
||||
from uuid import uuid1
|
||||
|
||||
from django_webtest import WebTest
|
||||
from requests.exceptions import HTTPError
|
||||
import requests_mock
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TransactionTestCase, TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.test import TransactionTestCase, override_settings
|
||||
from django_webtest import WebTest
|
||||
|
||||
from allianceauth.authentication.models import State
|
||||
from allianceauth.eveonline.models import EveCharacter
|
||||
from allianceauth.groupmanagement.models import ReservedGroupName
|
||||
from allianceauth.notifications.models import Notification
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from allianceauth.utils.cache import get_redis_client
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
from . import (
|
||||
TEST_GUILD_ID,
|
||||
TEST_USER_NAME,
|
||||
TEST_USER_ID,
|
||||
TEST_USER_DISCRIMINATOR,
|
||||
TEST_MAIN_NAME,
|
||||
TEST_MAIN_ID,
|
||||
MODULE_PATH,
|
||||
add_permissions_to_members,
|
||||
ROLE_ALPHA,
|
||||
ROLE_BRAVO,
|
||||
ROLE_CHARLIE,
|
||||
ROLE_MIKE,
|
||||
create_role,
|
||||
create_user_info
|
||||
)
|
||||
from ..discord_client.app_settings import DISCORD_API_BASE_URL
|
||||
from ..discord_client.exceptions import DiscordApiBackoff
|
||||
from ..models import DiscordUser
|
||||
from .. import tasks
|
||||
from ..core import create_bot_client
|
||||
from ..discord_client import DiscordApiBackoff
|
||||
from ..discord_client.app_settings import DISCORD_API_BASE_URL
|
||||
from ..discord_client.tests.factories import (
|
||||
TEST_GUILD_ID,
|
||||
TEST_USER_ID,
|
||||
TEST_USER_NAME,
|
||||
create_discord_error_response_unknown_member,
|
||||
create_discord_guild_member_object,
|
||||
create_discord_guild_object,
|
||||
create_discord_role_object,
|
||||
create_discord_user_object,
|
||||
)
|
||||
from ..models import DiscordUser
|
||||
from . import MODULE_PATH, TEST_MAIN_ID, TEST_MAIN_NAME, add_permissions_to_members
|
||||
|
||||
logger = logging.getLogger('allianceauth')
|
||||
|
||||
ROLE_MEMBER = create_role(99, 'Member')
|
||||
ROLE_BLUE = create_role(98, 'Blue')
|
||||
ROLE_ALPHA = create_discord_role_object(id=1, name="alpha")
|
||||
ROLE_BRAVO = create_discord_role_object(id=2, name="bravo")
|
||||
ROLE_CHARLIE = create_discord_role_object(id=3, name="charlie")
|
||||
ROLE_CHARLIE_2 = create_discord_role_object(id=4, name="Charlie") # Discord roles are case sensitive
|
||||
ROLE_MIKE = create_discord_role_object(id=13, name="mike", managed=True)
|
||||
ROLE_MEMBER = create_discord_role_object(99, 'Member')
|
||||
ROLE_BLUE = create_discord_role_object(98, 'Blue')
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class DiscordRequest:
|
||||
"""Helper for comparing requests made to the Discord API."""
|
||||
method: str
|
||||
url: str
|
||||
text: str = dataclasses.field(compare=False, default=None)
|
||||
|
||||
def json(self):
|
||||
return json.loads(self.text)
|
||||
|
||||
|
||||
# Putting all requests to Discord into objects so we can compare them better
|
||||
DiscordRequest = namedtuple('DiscordRequest', ['method', 'url'])
|
||||
user_get_current_request = DiscordRequest(
|
||||
method='GET',
|
||||
url=f'{DISCORD_API_BASE_URL}users/@me'
|
||||
@ -102,8 +115,9 @@ def reset_testdata():
|
||||
Notification.objects.all().delete()
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.core.DISCORD_GUILD_ID', TEST_GUILD_ID)
|
||||
@patch(MODULE_PATH + '.models.DISCORD_GUILD_ID', TEST_GUILD_ID)
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=False)
|
||||
@requests_mock.Mocker()
|
||||
class TestServiceFeatures(TransactionTestCase):
|
||||
fixtures = ['disable_analytics.json']
|
||||
@ -191,7 +205,7 @@ class TestServiceFeatures(TransactionTestCase):
|
||||
requests_mocker.patch(modify_guild_member_request.url, status_code=204)
|
||||
|
||||
# exhausting rate limit
|
||||
client = DiscordUser.objects._bot_client()
|
||||
client = create_bot_client()
|
||||
client._redis.set(
|
||||
name=client._KEY_GLOBAL_RATE_LIMIT_REMAINING,
|
||||
value=0,
|
||||
@ -207,7 +221,6 @@ class TestServiceFeatures(TransactionTestCase):
|
||||
requests_made = [
|
||||
DiscordRequest(r.method, r.url) for r in requests_mocker.request_history
|
||||
]
|
||||
|
||||
self.assertListEqual(requests_made, list())
|
||||
|
||||
def test_when_member_is_demoted_to_guest_then_his_account_is_deleted(
|
||||
@ -245,7 +258,7 @@ class TestServiceFeatures(TransactionTestCase):
|
||||
# request mocks
|
||||
requests_mocker.get(
|
||||
guild_member_request.url,
|
||||
json={'user': create_user_info(), 'roles': ['3', '13', '99']}
|
||||
json=create_discord_guild_member_object(roles=[3, 13, 99])
|
||||
)
|
||||
requests_mocker.get(
|
||||
guild_roles_request.url,
|
||||
@ -281,10 +294,7 @@ class TestServiceFeatures(TransactionTestCase):
|
||||
):
|
||||
requests_mocker.get(
|
||||
guild_member_request.url,
|
||||
json={
|
||||
'user': create_user_info(),
|
||||
'roles': ['13', '99']
|
||||
}
|
||||
json=create_discord_guild_member_object(roles=[13, 99])
|
||||
)
|
||||
requests_mocker.get(
|
||||
guild_roles_request.url,
|
||||
@ -313,10 +323,7 @@ class TestServiceFeatures(TransactionTestCase):
|
||||
):
|
||||
requests_mocker.get(
|
||||
guild_member_request.url,
|
||||
json={
|
||||
'user': {'id': str(TEST_USER_ID), 'username': TEST_MAIN_NAME},
|
||||
'roles': ['13', '99']
|
||||
}
|
||||
json=create_discord_guild_member_object(roles=['13', '99'])
|
||||
)
|
||||
requests_mocker.get(
|
||||
guild_roles_request.url,
|
||||
@ -342,11 +349,11 @@ class TestServiceFeatures(TransactionTestCase):
|
||||
self.assertTrue(DiscordUser.objects.user_has_account(self.user))
|
||||
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||
@patch(MODULE_PATH + '.managers.DISCORD_GUILD_ID', TEST_GUILD_ID)
|
||||
@patch(MODULE_PATH + '.models.DISCORD_GUILD_ID', TEST_GUILD_ID)
|
||||
@requests_mock.Mocker()
|
||||
class StateTestCase(TestCase):
|
||||
class StateTestCase(NoSocketsTestCase):
|
||||
|
||||
def setUp(self):
|
||||
clear_cache()
|
||||
@ -430,6 +437,7 @@ class StateTestCase(TestCase):
|
||||
self.user.discord
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.core.DISCORD_GUILD_ID', TEST_GUILD_ID)
|
||||
@patch(MODULE_PATH + '.managers.DISCORD_GUILD_ID', TEST_GUILD_ID)
|
||||
@patch(MODULE_PATH + '.models.DISCORD_GUILD_ID', TEST_GUILD_ID)
|
||||
@requests_mock.Mocker()
|
||||
@ -448,24 +456,25 @@ class TestUserFeatures(WebTest):
|
||||
)
|
||||
add_permissions_to_members()
|
||||
|
||||
@patch(MODULE_PATH + '.views.messages')
|
||||
@patch(MODULE_PATH + '.managers.OAuth2Session')
|
||||
@patch(MODULE_PATH + '.views.messages', spec=True)
|
||||
@patch(MODULE_PATH + '.managers.OAuth2Session', spec=True)
|
||||
def test_user_activation_normal(
|
||||
self, requests_mocker, mock_OAuth2Session, mock_messages
|
||||
):
|
||||
# setup
|
||||
requests_mocker.get(
|
||||
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'}
|
||||
guild_infos_request.url, json=create_discord_guild_object()
|
||||
)
|
||||
requests_mocker.get(
|
||||
user_get_current_request.url,
|
||||
json=create_user_info(
|
||||
TEST_USER_ID, TEST_USER_NAME, TEST_USER_DISCRIMINATOR
|
||||
)
|
||||
user_get_current_request.url, json=create_discord_user_object()
|
||||
)
|
||||
requests_mocker.get(
|
||||
guild_roles_request.url,
|
||||
json=[ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE, ROLE_MEMBER]
|
||||
guild_roles_request.url, json=[ROLE_ALPHA, ROLE_BRAVO, ROLE_MEMBER]
|
||||
)
|
||||
requests_mocker.get(
|
||||
guild_member_request.url,
|
||||
status_code=404,
|
||||
json=create_discord_error_response_unknown_member()
|
||||
)
|
||||
requests_mocker.put(add_guild_member_request.url, status_code=201)
|
||||
|
||||
@ -503,33 +512,93 @@ class TestUserFeatures(WebTest):
|
||||
for r in requests_mocker.request_history:
|
||||
obj = DiscordRequest(r.method, r.url)
|
||||
requests_made.append(obj)
|
||||
self.assertIn(add_guild_member_request, requests_made)
|
||||
|
||||
expected = [
|
||||
guild_infos_request,
|
||||
user_get_current_request,
|
||||
guild_roles_request,
|
||||
add_guild_member_request
|
||||
]
|
||||
self.assertListEqual(requests_made, expected)
|
||||
@patch(MODULE_PATH + '.views.messages', spec=True)
|
||||
@patch(MODULE_PATH + '.managers.OAuth2Session', spec=True)
|
||||
def test_should_activate_existing_user_and_keep_managed_and_reserved_roles(
|
||||
self, requests_mocker, mock_OAuth2Session, mock_messages
|
||||
):
|
||||
# setup
|
||||
requests_mocker.get(
|
||||
guild_infos_request.url, json=create_discord_guild_object()
|
||||
)
|
||||
requests_mocker.get(
|
||||
user_get_current_request.url, json=create_discord_user_object()
|
||||
)
|
||||
requests_mocker.get(
|
||||
guild_roles_request.url, json=[
|
||||
ROLE_ALPHA, ROLE_CHARLIE, ROLE_MEMBER, ROLE_MIKE
|
||||
]
|
||||
)
|
||||
requests_mocker.get(
|
||||
guild_member_request.url,
|
||||
json=create_discord_guild_member_object(roles=[1, 3, 13])
|
||||
)
|
||||
requests_mocker.patch(modify_guild_member_request.url, status_code=204)
|
||||
ReservedGroupName.objects.create(
|
||||
name="charlie", reason="dummy", created_by="xyz"
|
||||
)
|
||||
|
||||
@patch(MODULE_PATH + '.views.messages')
|
||||
@patch(MODULE_PATH + '.managers.OAuth2Session')
|
||||
authentication_code = 'auth_code'
|
||||
oauth_url = 'https://www.example.com/oauth'
|
||||
state = ''
|
||||
mock_OAuth2Session.return_value.authorization_url.return_value = \
|
||||
oauth_url, state
|
||||
|
||||
# login
|
||||
self.app.set_user(self.member)
|
||||
|
||||
# user opens services page
|
||||
services_page = self.app.get(reverse('services:services'))
|
||||
self.assertEqual(services_page.status_code, 200)
|
||||
|
||||
# user clicks Discord service activation link on page
|
||||
response = services_page.click(href=reverse('discord:activate'))
|
||||
|
||||
# check we got a redirect to Discord OAuth
|
||||
self.assertRedirects(
|
||||
response, expected_url=oauth_url, fetch_redirect_response=False
|
||||
)
|
||||
|
||||
# simulate Discord callback
|
||||
response = self.app.get(
|
||||
reverse('discord:callback'), params={'code': authentication_code}
|
||||
)
|
||||
|
||||
# user got a success message
|
||||
self.assertTrue(mock_messages.success.called)
|
||||
self.assertFalse(mock_messages.error.called)
|
||||
|
||||
my_request = None
|
||||
for r in requests_mocker.request_history:
|
||||
obj = DiscordRequest(r.method, r.url, r.text)
|
||||
if obj == modify_guild_member_request:
|
||||
my_request = obj
|
||||
break
|
||||
else:
|
||||
self.fail("Request not found")
|
||||
self.assertSetEqual(set(my_request.json()["roles"]), {3, 13, 99})
|
||||
|
||||
@patch(MODULE_PATH + '.views.messages', spec=True)
|
||||
@patch(MODULE_PATH + '.managers.OAuth2Session', spec=True)
|
||||
def test_user_activation_failed(
|
||||
self, requests_mocker, mock_OAuth2Session, mock_messages
|
||||
):
|
||||
# setup
|
||||
requests_mocker.get(
|
||||
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'}
|
||||
guild_infos_request.url, json=create_discord_guild_object()
|
||||
)
|
||||
requests_mocker.get(
|
||||
user_get_current_request.url,
|
||||
json=create_user_info(
|
||||
TEST_USER_ID, TEST_USER_NAME, TEST_USER_DISCRIMINATOR
|
||||
)
|
||||
user_get_current_request.url, json=create_discord_user_object()
|
||||
)
|
||||
requests_mocker.get(
|
||||
guild_roles_request.url,
|
||||
json=[ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE, ROLE_MEMBER]
|
||||
guild_roles_request.url, json=[ROLE_ALPHA, ROLE_BRAVO, ROLE_MEMBER]
|
||||
)
|
||||
requests_mocker.get(
|
||||
guild_member_request.url,
|
||||
status_code=404,
|
||||
json=create_discord_error_response_unknown_member()
|
||||
)
|
||||
|
||||
mock_exception = HTTPError('error')
|
||||
@ -571,20 +640,13 @@ class TestUserFeatures(WebTest):
|
||||
for r in requests_mocker.request_history:
|
||||
obj = DiscordRequest(r.method, r.url)
|
||||
requests_made.append(obj)
|
||||
self.assertIn(add_guild_member_request, requests_made)
|
||||
|
||||
expected = [
|
||||
guild_infos_request,
|
||||
user_get_current_request,
|
||||
guild_roles_request,
|
||||
add_guild_member_request
|
||||
]
|
||||
self.assertListEqual(requests_made, expected)
|
||||
|
||||
@patch(MODULE_PATH + '.views.messages')
|
||||
@patch(MODULE_PATH + '.views.messages', spec=True)
|
||||
def test_user_deactivation_normal(self, requests_mocker, mock_messages):
|
||||
# setup
|
||||
requests_mocker.get(
|
||||
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'}
|
||||
guild_infos_request.url, json=create_discord_guild_object()
|
||||
)
|
||||
requests_mocker.delete(remove_guild_member_request.url, status_code=204)
|
||||
DiscordUser.objects.create(user=self.member, uid=TEST_USER_ID)
|
||||
@ -610,15 +672,13 @@ class TestUserFeatures(WebTest):
|
||||
for r in requests_mocker.request_history:
|
||||
obj = DiscordRequest(r.method, r.url)
|
||||
requests_made.append(obj)
|
||||
self.assertIn(remove_guild_member_request, requests_made)
|
||||
|
||||
expected = [guild_infos_request, remove_guild_member_request]
|
||||
self.assertListEqual(requests_made, expected)
|
||||
|
||||
@patch(MODULE_PATH + '.views.messages')
|
||||
@patch(MODULE_PATH + '.views.messages', spec=True)
|
||||
def test_user_deactivation_fails(self, requests_mocker, mock_messages):
|
||||
# setup
|
||||
requests_mocker.get(
|
||||
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'}
|
||||
guild_infos_request.url, json=create_discord_guild_object()
|
||||
)
|
||||
mock_exception = HTTPError('error')
|
||||
mock_exception.response = Mock()
|
||||
@ -648,11 +708,9 @@ class TestUserFeatures(WebTest):
|
||||
for r in requests_mocker.request_history:
|
||||
obj = DiscordRequest(r.method, r.url)
|
||||
requests_made.append(obj)
|
||||
self.assertIn(remove_guild_member_request, requests_made)
|
||||
|
||||
expected = [guild_infos_request, remove_guild_member_request]
|
||||
self.assertListEqual(requests_made, expected)
|
||||
|
||||
@patch(MODULE_PATH + '.views.messages')
|
||||
@patch(MODULE_PATH + '.views.messages', spec=True)
|
||||
def test_user_add_new_server(self, requests_mocker, mock_messages):
|
||||
# setup
|
||||
mock_exception = HTTPError(Mock(**{"response.status_code": 400}))
|
||||
@ -684,14 +742,13 @@ class TestUserFeatures(WebTest):
|
||||
services_page = self.app.get(reverse('services:services'))
|
||||
self.assertEqual(services_page.status_code, 200)
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||
@patch(MODULE_PATH + ".core.default_bot_client", spec=True)
|
||||
def test_server_name_is_updated_by_task(
|
||||
self, requests_mocker
|
||||
self, requests_mocker, mock_bot_client
|
||||
):
|
||||
# setup
|
||||
requests_mocker.get(
|
||||
guild_infos_request.url, json={'id': TEST_GUILD_ID, 'name': 'Test Guild'}
|
||||
)
|
||||
mock_bot_client.guild_name.return_value = "Test Guild"
|
||||
# run task to update usernames
|
||||
tasks.update_all_usernames()
|
||||
|
||||
|
@ -1,364 +1,395 @@
|
||||
from unittest.mock import patch, Mock
|
||||
import urllib
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
from . import (
|
||||
from ..app_settings import DISCORD_APP_ID, DISCORD_APP_SECRET, DISCORD_CALLBACK_URL
|
||||
from ..discord_client import (
|
||||
DISCORD_OAUTH_BASE_URL,
|
||||
DISCORD_OAUTH_TOKEN_URL,
|
||||
DiscordApiBackoff,
|
||||
DiscordClient,
|
||||
RolesSet,
|
||||
)
|
||||
from ..discord_client.tests.factories import (
|
||||
TEST_GUILD_ID,
|
||||
TEST_USER_NAME,
|
||||
TEST_USER_ID,
|
||||
TEST_MAIN_NAME,
|
||||
TEST_MAIN_ID,
|
||||
MODULE_PATH,
|
||||
ROLE_ALPHA,
|
||||
ROLE_BRAVO,
|
||||
ROLE_CHARLIE,
|
||||
TEST_USER_NAME,
|
||||
create_role,
|
||||
create_user,
|
||||
)
|
||||
from ..discord_client.tests import create_matched_role
|
||||
from ..app_settings import (
|
||||
DISCORD_APP_ID,
|
||||
DISCORD_APP_SECRET,
|
||||
DISCORD_CALLBACK_URL,
|
||||
)
|
||||
from ..discord_client import DiscordClient, DiscordApiBackoff
|
||||
from ..models import DiscordUser
|
||||
from ..utils import set_logger_to_file
|
||||
|
||||
from . import MODULE_PATH, TEST_MAIN_NAME
|
||||
|
||||
logger = set_logger_to_file(MODULE_PATH + '.managers', __file__)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DISCORD_GUILD_ID', TEST_GUILD_ID)
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
@patch(MODULE_PATH + '.models.DiscordUser.objects._exchange_auth_code_for_token')
|
||||
@patch(MODULE_PATH + '.models.DiscordUser.objects.user_group_names')
|
||||
@patch(MODULE_PATH + '.models.DiscordUser.objects.user_formatted_nick')
|
||||
class TestAddUser(TestCase):
|
||||
@patch(MODULE_PATH + '.managers.create_bot_client', spec=True)
|
||||
@patch(
|
||||
MODULE_PATH + '.models.DiscordUser.objects._exchange_auth_code_for_token', spec=True
|
||||
)
|
||||
@patch(MODULE_PATH + '.managers.calculate_roles_for_user', spec=True)
|
||||
@patch(MODULE_PATH + '.managers.user_formatted_nick', spec=True)
|
||||
class TestAddUser(NoSocketsTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.user = AuthUtils.create_user(TEST_USER_NAME)
|
||||
self.user_info = {
|
||||
'id': TEST_USER_ID,
|
||||
'name': TEST_USER_NAME,
|
||||
'username': TEST_USER_NAME,
|
||||
'discriminator': '1234',
|
||||
}
|
||||
self.access_token = 'accesstoken'
|
||||
|
||||
def test_can_create_user_no_roles_no_nick(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_calculate_roles_for_user,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
mock_create_bot_client,
|
||||
mock_DiscordClient,
|
||||
):
|
||||
# given
|
||||
discord_user = create_user(id=TEST_USER_ID)
|
||||
mock_user_formatted_nick.return_value = None
|
||||
mock_user_group_names.return_value = []
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = []
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = True
|
||||
|
||||
mock_DiscordClient.return_value.current_user.return_value = discord_user
|
||||
mock_calculate_roles_for_user.return_value = RolesSet([]), None
|
||||
mock_create_bot_client.return_value.add_guild_member.return_value = True
|
||||
# when
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
args, kwargs = mock_DiscordClient.return_value.add_guild_member.call_args
|
||||
_, kwargs = mock_create_bot_client.return_value.add_guild_member.call_args
|
||||
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
|
||||
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
|
||||
self.assertEqual(kwargs['access_token'], self.access_token)
|
||||
self.assertIsNone(kwargs['role_ids'])
|
||||
self.assertFalse(kwargs['role_ids'])
|
||||
self.assertIsNone(kwargs['nick'])
|
||||
|
||||
def test_can_create_user_with_roles_no_nick(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_calculate_roles_for_user,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
mock_create_bot_client,
|
||||
mock_DiscordClient,
|
||||
):
|
||||
roles = [
|
||||
create_matched_role(ROLE_ALPHA),
|
||||
create_matched_role(ROLE_BRAVO),
|
||||
create_matched_role(ROLE_CHARLIE)
|
||||
]
|
||||
# given
|
||||
role_a = create_role(id=1)
|
||||
role_b = create_role(id=2)
|
||||
roles_calculated = RolesSet([role_a, role_b])
|
||||
discord_user = create_user(id=TEST_USER_ID)
|
||||
mock_user_formatted_nick.return_value = None
|
||||
mock_user_group_names.return_value = ['a', 'b', 'c']
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = roles
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = True
|
||||
|
||||
mock_DiscordClient.return_value.current_user.return_value = discord_user
|
||||
mock_calculate_roles_for_user.return_value = roles_calculated, None
|
||||
mock_create_bot_client.return_value.add_guild_member.return_value = True
|
||||
# when
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
args, kwargs = mock_DiscordClient.return_value.add_guild_member.call_args
|
||||
_, kwargs = mock_create_bot_client.return_value.add_guild_member.call_args
|
||||
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
|
||||
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
|
||||
self.assertEqual(kwargs['access_token'], self.access_token)
|
||||
self.assertSetEqual(set(kwargs['role_ids']), {1, 2, 3})
|
||||
self.assertSetEqual(set(kwargs['role_ids']), {1, 2})
|
||||
self.assertIsNone(kwargs['nick'])
|
||||
|
||||
def test_can_activate_existing_user_with_roles_no_nick(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_calculate_roles_for_user,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
mock_create_bot_client,
|
||||
mock_DiscordClient,
|
||||
):
|
||||
roles = [
|
||||
create_matched_role(ROLE_ALPHA),
|
||||
create_matched_role(ROLE_BRAVO),
|
||||
create_matched_role(ROLE_CHARLIE)
|
||||
]
|
||||
# given
|
||||
role_a = create_role(id=1)
|
||||
role_b = create_role(id=2)
|
||||
roles_calculated = RolesSet([role_a, role_b])
|
||||
discord_user = create_user(id=TEST_USER_ID)
|
||||
mock_user_formatted_nick.return_value = None
|
||||
mock_user_group_names.return_value = ['a', 'b', 'c']
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = roles
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = None
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
|
||||
mock_DiscordClient.return_value.current_user.return_value = discord_user
|
||||
mock_calculate_roles_for_user.return_value = roles_calculated, False
|
||||
mock_create_bot_client.return_value.add_guild_member.return_value = None
|
||||
mock_create_bot_client.return_value.modify_guild_member.return_value = True
|
||||
# when
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
|
||||
_, kwargs = mock_create_bot_client.return_value.modify_guild_member.call_args
|
||||
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
|
||||
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
|
||||
self.assertSetEqual(set(kwargs['role_ids']), {1, 2, 3})
|
||||
self.assertSetEqual(set(kwargs['role_ids']), {1, 2})
|
||||
self.assertIsNone(kwargs['nick'])
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', True)
|
||||
def test_can_create_user_no_roles_with_nick(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_calculate_roles_for_user,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
mock_create_bot_client,
|
||||
mock_DiscordClient,
|
||||
):
|
||||
# given
|
||||
discord_user = create_user(id=TEST_USER_ID)
|
||||
mock_user_formatted_nick.return_value = TEST_MAIN_NAME
|
||||
mock_user_group_names.return_value = []
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = []
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = True
|
||||
|
||||
mock_DiscordClient.return_value.current_user.return_value = discord_user
|
||||
mock_calculate_roles_for_user.return_value = RolesSet([]), None
|
||||
mock_create_bot_client.return_value.add_guild_member.return_value = True
|
||||
# when
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
args, kwargs = mock_DiscordClient.return_value.add_guild_member.call_args
|
||||
_, kwargs = mock_create_bot_client.return_value.add_guild_member.call_args
|
||||
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
|
||||
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
|
||||
self.assertEqual(kwargs['access_token'], self.access_token)
|
||||
self.assertIsNone(kwargs['role_ids'])
|
||||
self.assertFalse(kwargs['role_ids'])
|
||||
self.assertEqual(kwargs['nick'], TEST_MAIN_NAME)
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', True)
|
||||
def test_can_activate_existing_user_no_roles_with_nick(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_calculate_roles_for_user,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
mock_create_bot_client,
|
||||
mock_DiscordClient,
|
||||
):
|
||||
# given
|
||||
discord_user = create_user(id=TEST_USER_ID)
|
||||
mock_user_formatted_nick.return_value = TEST_MAIN_NAME
|
||||
mock_user_group_names.return_value = []
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = []
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = None
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
|
||||
mock_DiscordClient.return_value.current_user.return_value = discord_user
|
||||
mock_calculate_roles_for_user.return_value = RolesSet([]), False
|
||||
mock_create_bot_client.return_value.add_guild_member.return_value = None
|
||||
mock_create_bot_client.return_value.modify_guild_member.return_value = True
|
||||
# when
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
|
||||
_, kwargs = mock_create_bot_client.return_value.modify_guild_member.call_args
|
||||
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
|
||||
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
|
||||
self.assertIsNone(kwargs['role_ids'])
|
||||
self.assertFalse(kwargs['role_ids'])
|
||||
self.assertEqual(kwargs['nick'], TEST_MAIN_NAME)
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', False)
|
||||
def test_can_create_user_no_roles_and_without_nick_if_turned_off(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_calculate_roles_for_user,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
mock_create_bot_client,
|
||||
mock_DiscordClient,
|
||||
):
|
||||
# given
|
||||
discord_user = create_user(id=TEST_USER_ID)
|
||||
mock_user_formatted_nick.return_value = TEST_MAIN_NAME
|
||||
mock_user_group_names.return_value = []
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = []
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = True
|
||||
|
||||
mock_DiscordClient.return_value.current_user.return_value = discord_user
|
||||
mock_calculate_roles_for_user.return_value = RolesSet([]), None
|
||||
mock_create_bot_client.return_value.add_guild_member.return_value = True
|
||||
# when
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
args, kwargs = mock_DiscordClient.return_value.add_guild_member.call_args
|
||||
_, kwargs = mock_create_bot_client.return_value.add_guild_member.call_args
|
||||
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
|
||||
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
|
||||
self.assertEqual(kwargs['access_token'], self.access_token)
|
||||
self.assertIsNone(kwargs['role_ids'])
|
||||
self.assertFalse(kwargs['role_ids'])
|
||||
self.assertIsNone(kwargs['nick'])
|
||||
|
||||
def test_can_activate_existing_guild_member(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_calculate_roles_for_user,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
mock_create_bot_client,
|
||||
mock_DiscordClient,
|
||||
):
|
||||
# given
|
||||
discord_user = create_user(id=TEST_USER_ID)
|
||||
roles_calculated = RolesSet([create_role()])
|
||||
mock_user_formatted_nick.return_value = None
|
||||
mock_user_group_names.return_value = []
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = []
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = None
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
|
||||
mock_DiscordClient.return_value.current_user.return_value = discord_user
|
||||
mock_calculate_roles_for_user.return_value = roles_calculated, False
|
||||
mock_create_bot_client.return_value.add_guild_member.return_value = None
|
||||
mock_create_bot_client.return_value.modify_guild_member.return_value = True
|
||||
# when
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
self.assertTrue(mock_create_bot_client.return_value.modify_guild_member.called)
|
||||
|
||||
def test_can_activate_existing_member_with_roles(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_calculate_roles_for_user,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_create_bot_client,
|
||||
mock_DiscordClient,
|
||||
):
|
||||
# given
|
||||
discord_user = create_user(id=TEST_USER_ID)
|
||||
roles_calculated = RolesSet([create_role(id=1)])
|
||||
mock_user_formatted_nick.return_value = None
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = discord_user
|
||||
mock_calculate_roles_for_user.return_value = roles_calculated, False
|
||||
mock_create_bot_client.return_value.add_guild_member.return_value = None
|
||||
mock_create_bot_client.return_value.modify_guild_member.return_value = True
|
||||
# when
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
_, kwargs = mock_create_bot_client.return_value.modify_guild_member.call_args
|
||||
self.assertSetEqual(set(kwargs['role_ids']), {1})
|
||||
|
||||
def test_can_activate_existing_guild_member_failure(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_calculate_roles_for_user,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
mock_create_bot_client,
|
||||
mock_DiscordClient,
|
||||
):
|
||||
# given
|
||||
discord_user = create_user(id=TEST_USER_ID)
|
||||
roles_calculated = RolesSet([create_role()])
|
||||
mock_user_formatted_nick.return_value = None
|
||||
mock_user_group_names.return_value = []
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = []
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = None
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = False
|
||||
|
||||
mock_DiscordClient.return_value.current_user.return_value = discord_user
|
||||
mock_calculate_roles_for_user.return_value = roles_calculated, False
|
||||
mock_create_bot_client.return_value.add_guild_member.return_value = None
|
||||
mock_create_bot_client.return_value.modify_guild_member.return_value = False
|
||||
# when
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
self.assertFalse(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
self.assertTrue(mock_create_bot_client.return_value.modify_guild_member.called)
|
||||
|
||||
def test_return_false_when_user_creation_fails(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_calculate_roles_for_user,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
mock_create_bot_client,
|
||||
mock_DiscordClient,
|
||||
):
|
||||
# given
|
||||
discord_user = create_user(id=TEST_USER_ID)
|
||||
mock_user_formatted_nick.return_value = None
|
||||
mock_user_group_names.return_value = []
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = []
|
||||
mock_DiscordClient.return_value.add_guild_member.return_value = False
|
||||
|
||||
mock_DiscordClient.return_value.current_user.return_value = discord_user
|
||||
mock_calculate_roles_for_user.return_value = RolesSet([]), None
|
||||
mock_create_bot_client.return_value.add_guild_member.return_value = False
|
||||
# when
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
self.assertFalse(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
self.assertTrue(mock_create_bot_client.return_value.add_guild_member.called)
|
||||
|
||||
def test_return_false_when_on_api_backoff(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_calculate_roles_for_user,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
mock_create_bot_client,
|
||||
mock_DiscordClient,
|
||||
):
|
||||
# given
|
||||
discord_user = create_user(id=TEST_USER_ID)
|
||||
mock_user_formatted_nick.return_value = None
|
||||
mock_user_group_names.return_value = []
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = []
|
||||
mock_DiscordClient.return_value.add_guild_member.side_effect = \
|
||||
mock_DiscordClient.return_value.current_user.return_value = discord_user
|
||||
mock_calculate_roles_for_user.return_value = RolesSet([]), None
|
||||
mock_create_bot_client.return_value.add_guild_member.side_effect = \
|
||||
DiscordApiBackoff(999)
|
||||
|
||||
# when
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
self.assertFalse(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
self.assertTrue(mock_create_bot_client.return_value.add_guild_member.called)
|
||||
|
||||
def test_return_false_on_http_error(
|
||||
self,
|
||||
mock_user_formatted_nick,
|
||||
mock_user_group_names,
|
||||
mock_calculate_roles_for_user,
|
||||
mock_exchange_auth_code_for_token,
|
||||
mock_DiscordClient
|
||||
mock_create_bot_client,
|
||||
mock_DiscordClient,
|
||||
):
|
||||
# given
|
||||
discord_user = create_user(id=TEST_USER_ID)
|
||||
mock_user_formatted_nick.return_value = None
|
||||
mock_user_group_names.return_value = []
|
||||
mock_exchange_auth_code_for_token.return_value = self.access_token
|
||||
mock_DiscordClient.return_value.current_user.return_value = self.user_info
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = []
|
||||
mock_DiscordClient.return_value.current_user.return_value = discord_user
|
||||
mock_calculate_roles_for_user.return_value = RolesSet([]), None
|
||||
mock_exception = HTTPError('error')
|
||||
mock_exception.response = Mock()
|
||||
mock_exception.response.status_code = 500
|
||||
mock_DiscordClient.return_value.add_guild_member.side_effect = mock_exception
|
||||
|
||||
mock_create_bot_client.return_value.add_guild_member.side_effect = mock_exception
|
||||
# when
|
||||
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
self.assertFalse(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
|
||||
self.assertTrue(mock_create_bot_client.return_value.add_guild_member.called)
|
||||
|
||||
|
||||
class TestOauthHelpers(TestCase):
|
||||
class TestOauthHelpers(NoSocketsTestCase):
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DISCORD_APP_ID', '123456')
|
||||
def test_generate_bot_add_url(self):
|
||||
bot_add_url = DiscordUser.objects.generate_bot_add_url()
|
||||
|
||||
auth_url = DiscordClient.OAUTH_BASE_URL
|
||||
auth_url = DISCORD_OAUTH_BASE_URL
|
||||
real_bot_add_url = (
|
||||
f'{auth_url}?client_id=123456&scope=bot'
|
||||
f'&permissions={DiscordUser.objects.BOT_PERMISSIONS}'
|
||||
@ -368,12 +399,12 @@ class TestOauthHelpers(TestCase):
|
||||
def test_generate_oauth_redirect_url(self):
|
||||
oauth_url = DiscordUser.objects.generate_oauth_redirect_url()
|
||||
|
||||
self.assertIn(DiscordClient.OAUTH_BASE_URL, oauth_url)
|
||||
self.assertIn(DISCORD_OAUTH_BASE_URL, oauth_url)
|
||||
self.assertIn('+'.join(DiscordUser.objects.SCOPES), oauth_url)
|
||||
self.assertIn(DISCORD_APP_ID, oauth_url)
|
||||
self.assertIn(urllib.parse.quote_plus(DISCORD_CALLBACK_URL), oauth_url)
|
||||
|
||||
@patch(MODULE_PATH + '.managers.OAuth2Session')
|
||||
@patch(MODULE_PATH + '.managers.OAuth2Session', spec=True)
|
||||
def test_process_callback_code(self, oauth):
|
||||
instance = oauth.return_value
|
||||
instance.fetch_token.return_value = {'access_token': 'mywonderfultoken'}
|
||||
@ -386,52 +417,13 @@ class TestOauthHelpers(TestCase):
|
||||
self.assertEqual(kwargs['redirect_uri'], DISCORD_CALLBACK_URL)
|
||||
self.assertTrue(instance.fetch_token.called)
|
||||
args, kwargs = instance.fetch_token.call_args
|
||||
self.assertEqual(args[0], DiscordClient.OAUTH_TOKEN_URL)
|
||||
self.assertEqual(args[0], DISCORD_OAUTH_TOKEN_URL)
|
||||
self.assertEqual(kwargs['client_secret'], DISCORD_APP_SECRET)
|
||||
self.assertEqual(kwargs['code'], '12345')
|
||||
self.assertEqual(token, 'mywonderfultoken')
|
||||
|
||||
|
||||
class TestUserFormattedNick(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.user = AuthUtils.create_user(TEST_USER_NAME)
|
||||
|
||||
def test_return_nick_when_user_has_main(self):
|
||||
AuthUtils.add_main_character_2(self.user, TEST_MAIN_NAME, TEST_MAIN_ID)
|
||||
result = DiscordUser.objects.user_formatted_nick(self.user)
|
||||
expected = TEST_MAIN_NAME
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_return_none_if_user_has_no_main(self):
|
||||
result = DiscordUser.objects.user_formatted_nick(self.user)
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestUserGroupNames(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.group_1 = Group.objects.create(name='Group 1')
|
||||
cls.group_2 = Group.objects.create(name='Group 2')
|
||||
|
||||
def setUp(self):
|
||||
self.user = AuthUtils.create_member(TEST_USER_NAME)
|
||||
|
||||
def test_return_groups_and_state_names_for_user(self):
|
||||
self.user.groups.add(self.group_1)
|
||||
result = DiscordUser.objects.user_group_names(self.user)
|
||||
expected = ['Group 1', 'Member']
|
||||
self.assertSetEqual(set(result), set(expected))
|
||||
|
||||
def test_return_state_only_if_user_has_no_groups(self):
|
||||
result = DiscordUser.objects.user_group_names(self.user)
|
||||
expected = ['Member']
|
||||
self.assertSetEqual(set(result), set(expected))
|
||||
|
||||
|
||||
class TestUserHasAccount(TestCase):
|
||||
class TestUserHasAccount(NoSocketsTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -453,59 +445,22 @@ class TestUserHasAccount(TestCase):
|
||||
self.assertFalse(DiscordUser.objects.user_has_account('abc'))
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
@patch(MODULE_PATH + '.managers.logger')
|
||||
class TestServerName(TestCase):
|
||||
class TestOtherMethods(NoSocketsTestCase):
|
||||
@patch(MODULE_PATH + '.managers.core_group_to_role', spec=True)
|
||||
def test_should_call_group_to_role(self, mock_core_group_to_role):
|
||||
# given
|
||||
role = create_role(id=1, name="alpha", managed=False)
|
||||
mock_core_group_to_role.return_value = role
|
||||
# when
|
||||
result = DiscordUser.objects.group_to_role(Mock())
|
||||
# then
|
||||
self.assertEqual(result["id"], 1)
|
||||
self.assertEqual(result["name"], "alpha")
|
||||
self.assertEqual(result["managed"], False)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user = AuthUtils.create_user(TEST_USER_NAME)
|
||||
|
||||
def test_returns_name_when_api_returns_it(self, mock_logger, mock_DiscordClient):
|
||||
server_name = "El Dorado"
|
||||
mock_DiscordClient.return_value.guild_name.return_value = server_name
|
||||
|
||||
self.assertEqual(DiscordUser.objects.server_name(), server_name)
|
||||
self.assertFalse(mock_logger.warning.called)
|
||||
|
||||
def test_returns_empty_string_when_api_throws_http_error(
|
||||
self, mock_logger, mock_DiscordClient
|
||||
):
|
||||
mock_exception = HTTPError('Test exception')
|
||||
mock_exception.response = Mock(**{"status_code": 440})
|
||||
mock_DiscordClient.return_value.guild_name.side_effect = mock_exception
|
||||
|
||||
self.assertEqual(DiscordUser.objects.server_name(), "")
|
||||
self.assertFalse(mock_logger.warning.called)
|
||||
|
||||
def test_returns_empty_string_when_api_throws_service_error(
|
||||
self, mock_logger, mock_DiscordClient
|
||||
):
|
||||
mock_DiscordClient.return_value.guild_name.side_effect = DiscordApiBackoff(1000)
|
||||
|
||||
self.assertEqual(DiscordUser.objects.server_name(), "")
|
||||
self.assertFalse(mock_logger.warning.called)
|
||||
|
||||
def test_returns_empty_string_when_api_throws_unexpected_error(
|
||||
self, mock_logger, mock_DiscordClient
|
||||
):
|
||||
mock_DiscordClient.return_value.guild_name.side_effect = RuntimeError
|
||||
|
||||
self.assertEqual(DiscordUser.objects.server_name(), "")
|
||||
self.assertTrue(mock_logger.warning.called)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
class TestRoleForGroup(TestCase):
|
||||
def test_return_role_if_found(self, mock_DiscordClient):
|
||||
mock_DiscordClient.return_value.match_role_from_name.return_value = ROLE_ALPHA
|
||||
|
||||
group = Group.objects.create(name='alpha')
|
||||
self.assertEqual(DiscordUser.objects.group_to_role(group), ROLE_ALPHA)
|
||||
|
||||
def test_return_empty_dict_if_not_found(self, mock_DiscordClient):
|
||||
mock_DiscordClient.return_value.match_role_from_name.return_value = dict()
|
||||
|
||||
group = Group.objects.create(name='unknown')
|
||||
self.assertEqual(DiscordUser.objects.group_to_role(group), dict())
|
||||
@patch(MODULE_PATH + '.managers.core_server_name', spec=True)
|
||||
def test_should_call_server_name(self, mock_core_server_name):
|
||||
# when
|
||||
DiscordUser.objects.server_name()
|
||||
# then
|
||||
self.assertTrue(mock_core_server_name.called)
|
||||
|
@ -1,34 +1,20 @@
|
||||
from unittest.mock import patch, Mock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from allianceauth.groupmanagement.models import ReservedGroupName
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
from . import (
|
||||
TEST_USER_NAME,
|
||||
TEST_USER_ID,
|
||||
TEST_MAIN_NAME,
|
||||
TEST_MAIN_ID,
|
||||
MODULE_PATH,
|
||||
ROLE_ALPHA,
|
||||
ROLE_BRAVO,
|
||||
ROLE_CHARLIE,
|
||||
ROLE_CHARLIE_2,
|
||||
ROLE_MIKE,
|
||||
)
|
||||
from ..discord_client import DiscordClient, DiscordApiBackoff
|
||||
from ..discord_client.tests import create_matched_role
|
||||
from ..discord_client import DiscordApiBackoff, RolesSet
|
||||
from ..discord_client.tests.factories import TEST_USER_ID, TEST_USER_NAME, create_role
|
||||
from ..models import DiscordUser
|
||||
from ..utils import set_logger_to_file
|
||||
|
||||
from . import MODULE_PATH, TEST_MAIN_ID, TEST_MAIN_NAME
|
||||
|
||||
logger = set_logger_to_file(MODULE_PATH + '.models', __file__)
|
||||
|
||||
|
||||
class TestBasicsAndHelpers(TestCase):
|
||||
class TestBasicsAndHelpers(NoSocketsTestCase):
|
||||
|
||||
def test_str(self):
|
||||
user = AuthUtils.create_user(TEST_USER_NAME)
|
||||
@ -43,8 +29,8 @@ class TestBasicsAndHelpers(TestCase):
|
||||
self.assertEqual(repr(discord_user), expected)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
class TestUpdateNick(TestCase):
|
||||
@patch(MODULE_PATH + '.models.default_bot_client', spec=True)
|
||||
class TestUpdateNick(NoSocketsTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.user = AuthUtils.create_user(TEST_USER_NAME)
|
||||
@ -52,40 +38,54 @@ class TestUpdateNick(TestCase):
|
||||
user=self.user, uid=TEST_USER_ID
|
||||
)
|
||||
|
||||
def test_can_update(self, mock_DiscordClient):
|
||||
AuthUtils.add_main_character_2(self.user, TEST_MAIN_NAME, TEST_MAIN_ID)
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
|
||||
def test_can_update(self, mock_default_bot_client):
|
||||
# given
|
||||
AuthUtils.add_main_character_2(
|
||||
self.user, TEST_MAIN_NAME, TEST_MAIN_ID, disconnect_signals=True
|
||||
)
|
||||
mock_default_bot_client.modify_guild_member.return_value = True
|
||||
# when
|
||||
result = self.discord_user.update_nickname()
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
|
||||
def test_dont_update_if_user_has_no_main(self, mock_DiscordClient):
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = False
|
||||
self.assertTrue(mock_default_bot_client.modify_guild_member.called)
|
||||
|
||||
def test_dont_update_if_user_has_no_main(self, mock_default_bot_client):
|
||||
# given
|
||||
mock_default_bot_client.modify_guild_member.return_value = False
|
||||
# when
|
||||
result = self.discord_user.update_nickname()
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
self.assertFalse(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
|
||||
def test_return_none_if_user_no_longer_a_member(self, mock_DiscordClient):
|
||||
AuthUtils.add_main_character_2(self.user, TEST_MAIN_NAME, TEST_MAIN_ID)
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = None
|
||||
self.assertFalse(mock_default_bot_client.modify_guild_member.called)
|
||||
|
||||
def test_return_none_if_user_no_longer_a_member(self, mock_default_bot_client):
|
||||
# given
|
||||
AuthUtils.add_main_character_2(
|
||||
self.user, TEST_MAIN_NAME, TEST_MAIN_ID, disconnect_signals=True
|
||||
)
|
||||
mock_default_bot_client.modify_guild_member.return_value = None
|
||||
# when
|
||||
result = self.discord_user.update_nickname()
|
||||
# then
|
||||
self.assertIsNone(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
|
||||
def test_return_false_if_api_returns_false(self, mock_DiscordClient):
|
||||
AuthUtils.add_main_character_2(self.user, TEST_MAIN_NAME, TEST_MAIN_ID)
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = False
|
||||
self.assertTrue(mock_default_bot_client.modify_guild_member.called)
|
||||
|
||||
def test_return_false_if_api_returns_false(self, mock_default_bot_client):
|
||||
# given
|
||||
AuthUtils.add_main_character_2(
|
||||
self.user, TEST_MAIN_NAME, TEST_MAIN_ID, disconnect_signals=True
|
||||
)
|
||||
mock_default_bot_client.modify_guild_member.return_value = False
|
||||
# when
|
||||
result = self.discord_user.update_nickname()
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
self.assertTrue(mock_default_bot_client.modify_guild_member.called)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
class TestUpdateUsername(TestCase):
|
||||
@patch(MODULE_PATH + '.models.default_bot_client', spec=True)
|
||||
class TestUpdateUsername(NoSocketsTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -100,7 +100,8 @@ class TestUpdateUsername(TestCase):
|
||||
discriminator='1234'
|
||||
)
|
||||
|
||||
def test_can_update(self, mock_DiscordClient):
|
||||
def test_can_update(self, mock_default_bot_client):
|
||||
# given
|
||||
new_username = 'New name'
|
||||
new_discriminator = '9876'
|
||||
user_info = {
|
||||
@ -110,61 +111,77 @@ class TestUpdateUsername(TestCase):
|
||||
'discriminator': new_discriminator,
|
||||
}
|
||||
}
|
||||
mock_DiscordClient.return_value.guild_member.return_value = user_info
|
||||
|
||||
mock_default_bot_client.guild_member.return_value = user_info
|
||||
# when
|
||||
result = self.discord_user.update_username()
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.guild_member.called)
|
||||
self.assertTrue(mock_default_bot_client.guild_member.called)
|
||||
self.discord_user.refresh_from_db()
|
||||
self.assertEqual(self.discord_user.username, new_username)
|
||||
self.assertEqual(self.discord_user.discriminator, new_discriminator)
|
||||
|
||||
def test_return_none_if_user_no_longer_a_member(self, mock_DiscordClient):
|
||||
mock_DiscordClient.return_value.guild_member.return_value = None
|
||||
def test_return_none_if_user_no_longer_a_member(self, mock_default_bot_client):
|
||||
# given
|
||||
mock_default_bot_client.guild_member.return_value = None
|
||||
# when
|
||||
result = self.discord_user.update_username()
|
||||
# then
|
||||
self.assertIsNone(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.guild_member.called)
|
||||
self.assertTrue(mock_default_bot_client.guild_member.called)
|
||||
|
||||
def test_return_false_if_api_returns_false(self, mock_DiscordClient):
|
||||
mock_DiscordClient.return_value.guild_member.return_value = False
|
||||
def test_return_false_if_api_returns_false(self, mock_default_bot_client):
|
||||
# given
|
||||
mock_default_bot_client.guild_member.return_value = False
|
||||
# when
|
||||
result = self.discord_user.update_username()
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.guild_member.called)
|
||||
self.assertTrue(mock_default_bot_client.guild_member.called)
|
||||
|
||||
def test_return_false_if_api_returns_corrput_data_1(self, mock_DiscordClient):
|
||||
mock_DiscordClient.return_value.guild_member.return_value = {'invalid': True}
|
||||
def test_return_false_if_api_returns_corrput_data_1(self, mock_default_bot_client):
|
||||
# given
|
||||
mock_default_bot_client.guild_member.return_value = {'invalid': True}
|
||||
# when
|
||||
result = self.discord_user.update_username()
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.guild_member.called)
|
||||
self.assertTrue(mock_default_bot_client.guild_member.called)
|
||||
|
||||
def test_return_false_if_api_returns_corrput_data_2(self, mock_DiscordClient):
|
||||
def test_return_false_if_api_returns_corrput_data_2(self, mock_default_bot_client):
|
||||
# given
|
||||
user_info = {
|
||||
'user': {
|
||||
'id': str(TEST_USER_ID),
|
||||
'discriminator': '1234',
|
||||
}
|
||||
}
|
||||
mock_DiscordClient.return_value.guild_member.return_value = user_info
|
||||
mock_default_bot_client.guild_member.return_value = user_info
|
||||
# when
|
||||
result = self.discord_user.update_username()
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.guild_member.called)
|
||||
self.assertTrue(mock_default_bot_client.guild_member.called)
|
||||
|
||||
def test_return_false_if_api_returns_corrput_data_3(self, mock_DiscordClient):
|
||||
def test_return_false_if_api_returns_corrput_data_3(self, mock_default_bot_client):
|
||||
# given
|
||||
user_info = {
|
||||
'user': {
|
||||
'id': str(TEST_USER_ID),
|
||||
'username': TEST_USER_NAME,
|
||||
}
|
||||
}
|
||||
mock_DiscordClient.return_value.guild_member.return_value = user_info
|
||||
mock_default_bot_client.guild_member.return_value = user_info
|
||||
# when
|
||||
result = self.discord_user.update_username()
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.guild_member.called)
|
||||
self.assertTrue(mock_default_bot_client.guild_member.called)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.models.notify')
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
class TestDeleteUser(TestCase):
|
||||
@patch(MODULE_PATH + '.models.notify', spec=True)
|
||||
@patch(MODULE_PATH + '.models.create_bot_client', spec=True)
|
||||
class TestDeleteUser(NoSocketsTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -176,272 +193,168 @@ class TestDeleteUser(TestCase):
|
||||
user=self.user, uid=TEST_USER_ID
|
||||
)
|
||||
|
||||
def test_can_delete_user(self, mock_DiscordClient, mock_notify):
|
||||
mock_DiscordClient.return_value.remove_guild_member.return_value = True
|
||||
def test_can_delete_user(self, mock_create_bot_client, mock_notify):
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.return_value = True
|
||||
# when
|
||||
result = self.discord_user.delete_user()
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertFalse(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.remove_guild_member.called)
|
||||
self.assertTrue(mock_create_bot_client.return_value.remove_guild_member.called)
|
||||
self.assertFalse(mock_notify.called)
|
||||
|
||||
def test_can_delete_user_and_notify_user(self, mock_DiscordClient, mock_notify):
|
||||
mock_DiscordClient.return_value.remove_guild_member.return_value = True
|
||||
def test_can_delete_user_and_notify_user(self, mock_create_bot_client, mock_notify):
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.return_value = True
|
||||
# when
|
||||
result = self.discord_user.delete_user(notify_user=True)
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(mock_notify.called)
|
||||
|
||||
def test_can_delete_user_when_member_is_unknown(
|
||||
self, mock_DiscordClient, mock_notify
|
||||
self, mock_create_bot_client, mock_notify
|
||||
):
|
||||
mock_DiscordClient.return_value.remove_guild_member.return_value = None
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.return_value = None
|
||||
# when
|
||||
result = self.discord_user.delete_user()
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertFalse(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.remove_guild_member.called)
|
||||
self.assertTrue(mock_create_bot_client.return_value.remove_guild_member.called)
|
||||
self.assertFalse(mock_notify.called)
|
||||
|
||||
def test_return_false_when_api_fails(self, mock_DiscordClient, mock_notify):
|
||||
mock_DiscordClient.return_value.remove_guild_member.return_value = False
|
||||
def test_return_false_when_api_fails(self, mock_create_bot_client, mock_notify):
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.return_value = False
|
||||
# when
|
||||
result = self.discord_user.delete_user()
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_dont_notify_if_user_was_already_deleted_and_return_none(
|
||||
self, mock_DiscordClient, mock_notify
|
||||
self, mock_create_bot_client, mock_notify
|
||||
):
|
||||
mock_DiscordClient.return_value.remove_guild_member.return_value = None
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.return_value = None
|
||||
DiscordUser.objects.get(pk=self.discord_user.pk).delete()
|
||||
# when
|
||||
result = self.discord_user.delete_user()
|
||||
# then
|
||||
self.assertIsNone(result)
|
||||
self.assertFalse(
|
||||
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
|
||||
)
|
||||
self.assertTrue(mock_DiscordClient.return_value.remove_guild_member.called)
|
||||
self.assertTrue(mock_create_bot_client.return_value.remove_guild_member.called)
|
||||
self.assertFalse(mock_notify.called)
|
||||
|
||||
def test_raise_exception_on_api_backoff(
|
||||
self, mock_DiscordClient, mock_notify
|
||||
self, mock_create_bot_client, mock_notify
|
||||
):
|
||||
mock_DiscordClient.return_value.remove_guild_member.side_effect = \
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.side_effect = \
|
||||
DiscordApiBackoff(999)
|
||||
# when/then
|
||||
with self.assertRaises(DiscordApiBackoff):
|
||||
self.discord_user.delete_user()
|
||||
|
||||
def test_return_false_on_api_backoff_and_exception_handling_on(
|
||||
self, mock_DiscordClient, mock_notify
|
||||
self, mock_create_bot_client, mock_notify
|
||||
):
|
||||
mock_DiscordClient.return_value.remove_guild_member.side_effect = \
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.side_effect = \
|
||||
DiscordApiBackoff(999)
|
||||
# when
|
||||
result = self.discord_user.delete_user(handle_api_exceptions=True)
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_raise_exception_on_http_error(
|
||||
self, mock_DiscordClient, mock_notify
|
||||
self, mock_create_bot_client, mock_notify
|
||||
):
|
||||
# given
|
||||
mock_exception = HTTPError('error')
|
||||
mock_exception.response = Mock()
|
||||
mock_exception.response.status_code = 500
|
||||
mock_DiscordClient.return_value.remove_guild_member.side_effect = \
|
||||
mock_create_bot_client.return_value.remove_guild_member.side_effect = \
|
||||
mock_exception
|
||||
|
||||
# when/then
|
||||
with self.assertRaises(HTTPError):
|
||||
self.discord_user.delete_user()
|
||||
|
||||
def test_return_false_on_http_error_and_exception_handling_on(
|
||||
self, mock_DiscordClient, mock_notify
|
||||
self, mock_create_bot_client, mock_notify
|
||||
):
|
||||
# given
|
||||
mock_exception = HTTPError('error')
|
||||
mock_exception.response = Mock()
|
||||
mock_exception.response.status_code = 500
|
||||
mock_DiscordClient.return_value.remove_guild_member.side_effect = \
|
||||
mock_create_bot_client.return_value.remove_guild_member.side_effect = \
|
||||
mock_exception
|
||||
# when
|
||||
result = self.discord_user.delete_user(handle_api_exceptions=True)
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
@patch(MODULE_PATH + '.models.DiscordUser.objects.user_group_names')
|
||||
class TestUpdateGroups(TestCase):
|
||||
@patch(MODULE_PATH + '.models.default_bot_client', spec=True)
|
||||
@patch(MODULE_PATH + '.models.calculate_roles_for_user', spec=True)
|
||||
class TestUpdateGroups(NoSocketsTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.user = AuthUtils.create_user(TEST_USER_NAME)
|
||||
self.discord_user = DiscordUser.objects.create(
|
||||
user=self.user, uid=TEST_USER_ID
|
||||
)
|
||||
self.guild_roles = [ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE]
|
||||
self.roles_requested = [
|
||||
create_matched_role(ROLE_ALPHA), create_matched_role(ROLE_BRAVO)
|
||||
]
|
||||
user = AuthUtils.create_user(TEST_USER_NAME)
|
||||
self.discord_user = DiscordUser.objects.create(user=user, uid=TEST_USER_ID)
|
||||
|
||||
def test_update_if_needed(
|
||||
self,
|
||||
mock_user_group_names,
|
||||
mock_DiscordClient
|
||||
):
|
||||
roles_current = [1]
|
||||
mock_user_group_names.return_value = []
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = self.roles_requested
|
||||
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
|
||||
mock_DiscordClient.return_value.guild_member.return_value = \
|
||||
{'roles': roles_current}
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
|
||||
result = self.discord_user.update_groups()
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
|
||||
self.assertEqual(set(kwargs['role_ids']), {1, 2})
|
||||
|
||||
def test_should_update_and_preserve_managed_and_reserved_roles(
|
||||
self,
|
||||
mock_user_group_names,
|
||||
mock_DiscordClient
|
||||
def test_should_update_when_roles_have_changed(
|
||||
self, mock_calculate_roles_for_user, mock_client
|
||||
):
|
||||
# given
|
||||
roles_current = [1, 3, 4, 13]
|
||||
mock_user_group_names.return_value = []
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = self.roles_requested
|
||||
mock_DiscordClient.return_value.guild_roles.return_value = [
|
||||
ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE, ROLE_CHARLIE_2
|
||||
]
|
||||
mock_DiscordClient.return_value.guild_member.return_value = {
|
||||
'roles': roles_current
|
||||
}
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
ReservedGroupName.objects.create(
|
||||
name="charlie", reason="dummy", created_by="xyz"
|
||||
)
|
||||
mock_calculate_roles_for_user.return_value = RolesSet([create_role()]), True
|
||||
mock_client.modify_guild_member.return_value = True
|
||||
# when
|
||||
result = self.discord_user.update_groups()
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
|
||||
self.assertEqual(set(kwargs['role_ids']), {1, 2, 3, 4, 13})
|
||||
self.assertTrue(mock_client.modify_guild_member.called)
|
||||
|
||||
def test_dont_update_if_not_needed(
|
||||
self,
|
||||
mock_user_group_names,
|
||||
mock_DiscordClient
|
||||
def test_should_not_update_when_roles_have_not_changed(
|
||||
self, mock_calculate_roles_for_user, mock_client
|
||||
):
|
||||
roles_current = [1, 2, 13]
|
||||
mock_user_group_names.return_value = []
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = self.roles_requested
|
||||
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
|
||||
mock_DiscordClient.return_value.guild_member.return_value = \
|
||||
{'roles': roles_current}
|
||||
|
||||
# given
|
||||
mock_calculate_roles_for_user.return_value = RolesSet([create_role()]), False
|
||||
mock_client.modify_guild_member.return_value = True
|
||||
# when
|
||||
result = self.discord_user.update_groups()
|
||||
# then
|
||||
self.assertTrue(result)
|
||||
self.assertFalse(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
self.assertFalse(mock_client.modify_guild_member.called)
|
||||
|
||||
def test_update_if_user_has_no_roles_on_discord(
|
||||
self,
|
||||
mock_user_group_names,
|
||||
mock_DiscordClient
|
||||
def test_should_not_update_when_user_not_guild_member(
|
||||
self, mock_calculate_roles_for_user, mock_client
|
||||
):
|
||||
roles_current = []
|
||||
mock_user_group_names.return_value = []
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = self.roles_requested
|
||||
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
|
||||
mock_DiscordClient.return_value.guild_member.return_value = \
|
||||
{'roles': roles_current}
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
|
||||
result = self.discord_user.update_groups()
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
|
||||
self.assertEqual(set(kwargs['role_ids']), {1, 2})
|
||||
|
||||
def test_return_none_if_user_no_longer_a_member(
|
||||
self,
|
||||
mock_user_group_names,
|
||||
mock_DiscordClient
|
||||
):
|
||||
mock_DiscordClient.return_value.guild_member.return_value = None
|
||||
|
||||
# given
|
||||
mock_calculate_roles_for_user.return_value = RolesSet([create_role()]), None
|
||||
mock_client.modify_guild_member.return_value = True
|
||||
# when
|
||||
result = self.discord_user.update_groups()
|
||||
# then
|
||||
self.assertIsNone(result)
|
||||
self.assertFalse(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
self.assertFalse(mock_client.modify_guild_member.called)
|
||||
|
||||
def test_return_false_if_api_returns_false(
|
||||
self,
|
||||
mock_user_group_names,
|
||||
mock_DiscordClient
|
||||
def test_should_return_false_when_update_failed(
|
||||
self, mock_calculate_roles_for_user, mock_client
|
||||
):
|
||||
roles_current = [1]
|
||||
mock_user_group_names.return_value = []
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = self.roles_requested
|
||||
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
|
||||
mock_DiscordClient.return_value.guild_member.return_value = \
|
||||
{'roles': roles_current}
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = False
|
||||
|
||||
# given
|
||||
mock_calculate_roles_for_user.return_value = RolesSet([create_role()]), True
|
||||
mock_client.modify_guild_member.return_value = False
|
||||
# when
|
||||
result = self.discord_user.update_groups()
|
||||
# then
|
||||
self.assertFalse(result)
|
||||
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
|
||||
|
||||
def test_raise_exception_if_member_has_unknown_roles(
|
||||
self,
|
||||
mock_user_group_names,
|
||||
mock_DiscordClient
|
||||
):
|
||||
roles_current = [99]
|
||||
mock_user_group_names.return_value = []
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = self.roles_requested
|
||||
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
|
||||
mock_DiscordClient.return_value.guild_member.return_value = \
|
||||
{'roles': roles_current}
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
|
||||
with self.assertRaises(RuntimeError):
|
||||
self.discord_user.update_groups()
|
||||
|
||||
def test_refresh_guild_roles_user_roles_dont_not_match(
|
||||
self,
|
||||
mock_user_group_names,
|
||||
mock_DiscordClient
|
||||
):
|
||||
def my_guild_roles(guild_id, use_cache=True):
|
||||
if use_cache:
|
||||
return [ROLE_ALPHA, ROLE_BRAVO, ROLE_MIKE]
|
||||
else:
|
||||
return [ROLE_ALPHA, ROLE_BRAVO, ROLE_CHARLIE, ROLE_MIKE]
|
||||
|
||||
roles_current = [3]
|
||||
mock_user_group_names.return_value = []
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = self.roles_requested
|
||||
mock_DiscordClient.return_value.guild_roles.side_effect = my_guild_roles
|
||||
mock_DiscordClient.return_value.guild_member.return_value = \
|
||||
{'roles': roles_current}
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
result = self.discord_user.update_groups()
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(mock_DiscordClient.return_value.guild_roles.call_count, 2)
|
||||
|
||||
def test_raise_exception_if_member_info_is_invalid(
|
||||
self,
|
||||
mock_user_group_names,
|
||||
mock_DiscordClient
|
||||
):
|
||||
mock_user_group_names.return_value = []
|
||||
mock_DiscordClient.return_value.match_or_create_roles_from_names\
|
||||
.return_value = self.roles_requested
|
||||
mock_DiscordClient.return_value.guild_roles.return_value = self.guild_roles
|
||||
mock_DiscordClient.return_value.guild_member.return_value = \
|
||||
{'user': 'dummy'}
|
||||
mock_DiscordClient.return_value.modify_guild_member.return_value = True
|
||||
|
||||
with self.assertRaises(RuntimeError):
|
||||
self.discord_user.update_groups()
|
||||
self.assertTrue(mock_client.modify_guild_member.called)
|
||||
|
@ -3,18 +3,18 @@ from unittest.mock import MagicMock, patch
|
||||
from celery.exceptions import Retry
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import Group
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
from . import TEST_USER_NAME, TEST_USER_ID, TEST_MAIN_NAME, TEST_MAIN_ID
|
||||
from ..models import DiscordUser
|
||||
from ..discord_client import DiscordApiBackoff
|
||||
from .. import tasks
|
||||
from ..discord_client import DiscordApiBackoff
|
||||
from ..discord_client.tests.factories import TEST_USER_ID, TEST_USER_NAME
|
||||
from ..models import DiscordUser
|
||||
from ..utils import set_logger_to_file
|
||||
|
||||
from . import TEST_MAIN_ID, TEST_MAIN_NAME
|
||||
|
||||
MODULE_PATH = 'allianceauth.services.modules.discord.tasks'
|
||||
logger = set_logger_to_file(MODULE_PATH, __file__)
|
||||
@ -22,7 +22,7 @@ logger = set_logger_to_file(MODULE_PATH, __file__)
|
||||
|
||||
@patch(MODULE_PATH + '.DiscordUser.update_groups')
|
||||
@patch(MODULE_PATH + ".logger")
|
||||
class TestUpdateGroups(TestCase):
|
||||
class TestUpdateGroups(NoSocketsTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -110,7 +110,7 @@ class TestUpdateGroups(TestCase):
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.DiscordUser.update_nickname')
|
||||
class TestUpdateNickname(TestCase):
|
||||
class TestUpdateNickname(NoSocketsTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -163,7 +163,7 @@ class TestUpdateNickname(TestCase):
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.DiscordUser.update_username')
|
||||
class TestUpdateUsername(TestCase):
|
||||
class TestUpdateUsername(NoSocketsTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -179,7 +179,7 @@ class TestUpdateUsername(TestCase):
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.DiscordUser.delete_user')
|
||||
class TestDeleteUser(TestCase):
|
||||
class TestDeleteUser(NoSocketsTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -213,7 +213,7 @@ class TestDeleteUser(TestCase):
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.DiscordUser.update_groups')
|
||||
class TestTaskPerformUserAction(TestCase):
|
||||
class TestTaskPerformUserAction(NoSocketsTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -236,7 +236,7 @@ class TestTaskPerformUserAction(TestCase):
|
||||
|
||||
@patch(MODULE_PATH + '.DiscordUser.objects.server_name')
|
||||
@patch(MODULE_PATH + ".logger")
|
||||
class TestTaskUpdateServername(TestCase):
|
||||
class TestTaskUpdateServername(NoSocketsTestCase):
|
||||
|
||||
def test_normal(self, mock_logger, mock_server_name):
|
||||
tasks.update_servername()
|
||||
@ -281,7 +281,7 @@ class TestTaskUpdateServername(TestCase):
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.DiscordUser.objects.server_name')
|
||||
class TestTaskPerformUsersAction(TestCase):
|
||||
class TestTaskPerformUsersAction(NoSocketsTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -300,8 +300,8 @@ class TestTaskPerformUsersAction(TestCase):
|
||||
tasks._task_perform_users_action(mock_task, 'server_name')
|
||||
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True)
|
||||
class TestBulkTasks(TestCase):
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||
class TestBulkTasks(NoSocketsTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
@ -1,4 +1,5 @@
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from ..utils import clean_setting
|
||||
|
@ -1,28 +1,28 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.test import RequestFactory
|
||||
from django.urls import reverse
|
||||
|
||||
from allianceauth.tests.auth_utils import AuthUtils
|
||||
from allianceauth.utils.testing import NoSocketsTestCase
|
||||
|
||||
from . import MODULE_PATH, add_permissions_to_members, TEST_USER_NAME, TEST_USER_ID
|
||||
from ..discord_client import DiscordClient
|
||||
from ..discord_client.tests.factories import TEST_USER_ID, TEST_USER_NAME
|
||||
from ..models import DiscordUser
|
||||
from ..utils import set_logger_to_file
|
||||
from ..views import (
|
||||
discord_callback,
|
||||
reset_discord,
|
||||
activate_discord,
|
||||
deactivate_discord,
|
||||
discord_add_bot,
|
||||
activate_discord
|
||||
discord_callback,
|
||||
reset_discord,
|
||||
)
|
||||
|
||||
from . import MODULE_PATH, add_permissions_to_members
|
||||
|
||||
logger = set_logger_to_file(MODULE_PATH + '.views', __file__)
|
||||
|
||||
|
||||
class SetupClassMixin(TestCase):
|
||||
class SetupClassMixin(NoSocketsTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -33,7 +33,7 @@ class SetupClassMixin(TestCase):
|
||||
cls.services_url = reverse('services:services')
|
||||
|
||||
|
||||
class TestActivateDiscord(SetupClassMixin, TestCase):
|
||||
class TestActivateDiscord(SetupClassMixin, NoSocketsTestCase):
|
||||
|
||||
@patch(MODULE_PATH + '.views.DiscordUser.objects.generate_oauth_redirect_url')
|
||||
def test_redirects_to_correct_url(self, mock_generate_oauth_redirect_url):
|
||||
@ -47,31 +47,37 @@ class TestActivateDiscord(SetupClassMixin, TestCase):
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.views.messages')
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
|
||||
class TestDeactivateDiscord(SetupClassMixin, TestCase):
|
||||
@patch(MODULE_PATH + '.models.create_bot_client')
|
||||
class TestDeactivateDiscord(SetupClassMixin, NoSocketsTestCase):
|
||||
|
||||
def setUp(self):
|
||||
DiscordUser.objects.create(user=self.user, uid=TEST_USER_ID)
|
||||
|
||||
def test_when_successful_show_success_message(
|
||||
self, mock_DiscordClient, mock_messages
|
||||
self, mock_create_bot_client, mock_messages
|
||||
):
|
||||
mock_DiscordClient.return_value.remove_guild_member.return_value = True
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.return_value = True
|
||||
request = self.factory.get(reverse('discord:deactivate'))
|
||||
request.user = self.user
|
||||
# when
|
||||
response = deactivate_discord(request)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, self.services_url)
|
||||
self.assertTrue(mock_messages.success.called)
|
||||
self.assertFalse(mock_messages.error.called)
|
||||
|
||||
def test_when_unsuccessful_show_error_message(
|
||||
self, mock_DiscordClient, mock_messages
|
||||
self, mock_create_bot_client, mock_messages
|
||||
):
|
||||
mock_DiscordClient.return_value.remove_guild_member.return_value = False
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.return_value = False
|
||||
request = self.factory.get(reverse('discord:deactivate'))
|
||||
request.user = self.user
|
||||
# when
|
||||
response = deactivate_discord(request)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, self.services_url)
|
||||
self.assertFalse(mock_messages.success.called)
|
||||
@ -79,30 +85,36 @@ class TestDeactivateDiscord(SetupClassMixin, TestCase):
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.views.messages')
|
||||
@patch(MODULE_PATH + '.managers.DiscordClient')
|
||||
class TestResetDiscord(SetupClassMixin, TestCase):
|
||||
@patch(MODULE_PATH + '.models.create_bot_client')
|
||||
class TestResetDiscord(SetupClassMixin, NoSocketsTestCase):
|
||||
|
||||
def setUp(self):
|
||||
DiscordUser.objects.create(user=self.user, uid=TEST_USER_ID)
|
||||
|
||||
def test_when_successful_redirect_to_activate(
|
||||
self, mock_DiscordClient, mock_messages
|
||||
self, mock_create_bot_client, mock_messages
|
||||
):
|
||||
mock_DiscordClient.return_value.remove_guild_member.return_value = True
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.return_value = True
|
||||
request = self.factory.get(reverse('discord:reset'))
|
||||
request.user = self.user
|
||||
# when
|
||||
response = reset_discord(request)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse("discord:activate"))
|
||||
self.assertFalse(mock_messages.error.called)
|
||||
|
||||
def test_when_unsuccessful_message_error_and_redirect_to_service(
|
||||
self, mock_DiscordClient, mock_messages
|
||||
self, mock_create_bot_client, mock_messages
|
||||
):
|
||||
mock_DiscordClient.return_value.remove_guild_member.return_value = False
|
||||
# given
|
||||
mock_create_bot_client.return_value.remove_guild_member.return_value = False
|
||||
request = self.factory.get(reverse('discord:reset'))
|
||||
request.user = self.user
|
||||
# when
|
||||
response = reset_discord(request)
|
||||
# then
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, self.services_url)
|
||||
self.assertTrue(mock_messages.error.called)
|
||||
@ -110,7 +122,7 @@ class TestResetDiscord(SetupClassMixin, TestCase):
|
||||
|
||||
@patch(MODULE_PATH + '.views.messages')
|
||||
@patch(MODULE_PATH + '.views.DiscordUser.objects.add_user')
|
||||
class TestDiscordCallback(SetupClassMixin, TestCase):
|
||||
class TestDiscordCallback(SetupClassMixin, NoSocketsTestCase):
|
||||
|
||||
def setUp(self):
|
||||
DiscordUser.objects.create(user=self.user, uid=TEST_USER_ID)
|
||||
@ -155,7 +167,7 @@ class TestDiscordCallback(SetupClassMixin, TestCase):
|
||||
|
||||
|
||||
@patch(MODULE_PATH + '.views.DiscordUser.objects.generate_bot_add_url')
|
||||
class TestDiscordAddBot(TestCase):
|
||||
class TestDiscordAddBot(NoSocketsTestCase):
|
||||
|
||||
def test_add_bot(self, mock_generate_bot_add_url):
|
||||
bot_url = 'https://www.example.com/bot'
|
||||
|
35
allianceauth/utils/testing.py
Normal file
35
allianceauth/utils/testing.py
Normal file
@ -0,0 +1,35 @@
|
||||
import socket
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class SocketAccessError(Exception):
|
||||
"""Error raised when a test script accesses the network"""
|
||||
|
||||
|
||||
class NoSocketsTestCase(TestCase):
|
||||
"""Variation of Django's TestCase class that prevents any network use.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class TestMyStuff(NoSocketsTestCase):
|
||||
def test_should_do_what_i_need(self):
|
||||
...
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.socket_original = socket.socket
|
||||
socket.socket = cls.guard
|
||||
return super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
socket.socket = cls.socket_original
|
||||
return super().tearDownClass()
|
||||
|
||||
@staticmethod
|
||||
def guard(*args, **kwargs):
|
||||
raise SocketAccessError("Attempted to access network")
|
9
allianceauth/utils/tests/test_testing.py
Normal file
9
allianceauth/utils/tests/test_testing.py
Normal file
@ -0,0 +1,9 @@
|
||||
import requests
|
||||
from allianceauth.utils.testing import NoSocketsTestCase, SocketAccessError
|
||||
|
||||
|
||||
class TestNoSocketsTestCase(NoSocketsTestCase):
|
||||
def test_raises_exception_on_attempted_network_access(self):
|
||||
|
||||
with self.assertRaises(SocketAccessError):
|
||||
requests.get("https://www.google.com")
|
36
docs/development/tech_docu/api/discord_client.rst
Normal file
36
docs/development/tech_docu/api/discord_client.rst
Normal file
@ -0,0 +1,36 @@
|
||||
======================
|
||||
Discord Client
|
||||
======================
|
||||
|
||||
AA contains a web client for interacting with the Discord API. This client can be used independently from an installed Discord service in AA.
|
||||
|
||||
Location: ``allianceauth.services.modules.discord.discord_client``
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
|
||||
client
|
||||
======
|
||||
|
||||
.. automodule:: allianceauth.services.modules.discord.discord_client.client
|
||||
:members:
|
||||
|
||||
models
|
||||
======
|
||||
|
||||
.. automodule:: allianceauth.services.modules.discord.discord_client.models
|
||||
:members:
|
||||
:undoc-members:
|
||||
:member-order: bysource
|
||||
|
||||
exceptions
|
||||
==========
|
||||
|
||||
.. automodule:: allianceauth.services.modules.discord.discord_client.exceptions
|
||||
:members:
|
||||
|
||||
settings
|
||||
========
|
||||
|
||||
.. automodule:: allianceauth.services.modules.discord.discord_client.app_settings
|
||||
:members:
|
27
docs/development/tech_docu/api/discord_service.rst
Normal file
27
docs/development/tech_docu/api/discord_service.rst
Normal file
@ -0,0 +1,27 @@
|
||||
======================
|
||||
Discord Service
|
||||
======================
|
||||
|
||||
This page contains the technical documentation for the Discord service.
|
||||
|
||||
Location: ``allianceauth.services.modules.discord``
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
|
||||
api
|
||||
======
|
||||
|
||||
.. automodule:: allianceauth.services.modules.discord.api
|
||||
:members:
|
||||
:exclude-members: DiscordUser
|
||||
|
||||
.. autoclass:: DiscordUser
|
||||
:no-members: delete_user
|
||||
|
||||
|
||||
settings
|
||||
========
|
||||
|
||||
.. automodule:: allianceauth.services.modules.discord.app_settings
|
||||
:members:
|
@ -6,6 +6,8 @@ To reduce redundancy and help speed up development we encourage developers to ut
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
discord_client
|
||||
discord_service
|
||||
esi
|
||||
evelinks
|
||||
eveonline
|
||||
|
@ -6,9 +6,22 @@ Utilities and helper functions.
|
||||
|
||||
Location: ``allianceauth.utils``
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
|
||||
cache
|
||||
===========
|
||||
|
||||
.. automodule:: allianceauth.utils.cache
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
testing
|
||||
===========
|
||||
|
||||
.. automodule:: allianceauth.utils.testing
|
||||
:members:
|
||||
:exclude-members: NoSocketsTestCase
|
||||
|
||||
.. autoclass:: NoSocketsTestCase
|
||||
:no-members:
|
||||
|
Loading…
x
Reference in New Issue
Block a user