Add state info to dashboard, improve stat change notifications, improve auth tests

This commit is contained in:
ErikKalkoken 2020-07-03 14:58:45 +02:00
parent e44c2935f9
commit f1a21bb856
11 changed files with 214 additions and 182 deletions

View File

@ -1,7 +1,7 @@
# This will make sure the app is always imported when # This will make sure the app is always imported when
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
__version__ = '2.7.3a1' __version__ = '2.7.3a2'
__title__ = 'Alliance Auth' __title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth' __url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = '%s v%s' % (__title__, __version__) NAME = '%s v%s' % (__title__, __version__)

View File

@ -75,8 +75,8 @@ class UserProfile(models.Model):
self.save(update_fields=['state']) self.save(update_fields=['state'])
notify( notify(
self.user, self.user,
_('State Changed'), _('State changed to: %s' % state),
_('Your user state has been changed to %(state)s') _('Your user\'s state is now: %(state)s')
% ({'state': state}), % ({'state': state}),
'info' 'info'
) )

View File

@ -14,7 +14,11 @@
<div class="col-sm-6 text-center"> <div class="col-sm-6 text-center">
<div class="panel panel-primary" style="height:100%"> <div class="panel panel-primary" style="height:100%">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="panel-title">{% trans "Main Character" %}</h3> <h3 class="panel-title">
{% blocktrans with state=request.user.profile.state %}
Main Character (State: {{ state }})
{% endblocktrans %}
</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% if request.user.profile.main_character %} {% if request.user.profile.main_character %}

View File

@ -1,10 +1,13 @@
from unittest.mock import Mock, patch
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.test import TestCase from django.test import TestCase
from allianceauth.eveonline.models import EveCharacter
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from esi.models import Token
from ..backends import StateBackend from ..backends import StateBackend
from ..models import CharacterOwnership, UserProfile, OwnershipRecord
MODULE_PATH = 'allianceauth.authentication' MODULE_PATH = 'allianceauth.authentication'
@ -76,10 +79,71 @@ class TestStatePermissions(TestCase):
self.assertTrue(user.has_perm(PERMISSION_2)) self.assertTrue(user.has_perm(PERMISSION_2))
class TestAuthenticate(TestCase):
@classmethod
def setUpTestData(cls):
cls.main_character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.alt_character = EveCharacter.objects.create(
character_id=2,
character_name='Alt Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.unclaimed_character = EveCharacter.objects.create(
character_id=3,
character_name='Unclaimed Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
cls.old_user = AuthUtils.create_user('old_user', disconnect_signals=True)
AuthUtils.disconnect_signals()
CharacterOwnership.objects.create(user=cls.user, character=cls.main_character, owner_hash='1')
CharacterOwnership.objects.create(user=cls.user, character=cls.alt_character, owner_hash='2')
UserProfile.objects.update_or_create(user=cls.user, defaults={'main_character': cls.main_character})
AuthUtils.connect_signals()
def test_authenticate_main_character(self):
t = Token(character_id=self.main_character.character_id, character_owner_hash='1')
user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user)
def test_authenticate_alt_character(self):
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user)
def test_authenticate_unclaimed_character(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3')
user = StateBackend().authenticate(token=t)
self.assertNotEqual(user, self.user)
self.assertEqual(user.username, 'Unclaimed_Character')
self.assertEqual(user.profile.main_character, self.unclaimed_character)
def test_authenticate_character_record(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4')
OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4')
user = StateBackend().authenticate(token=t)
self.assertEqual(user, self.old_user)
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
self.assertTrue(user.profile.main_character)
def test_iterate_username(self):
t = Token(character_id=self.unclaimed_character.character_id,
character_name=self.unclaimed_character.character_name, character_owner_hash='3')
username = StateBackend().authenticate(token=t).username
t.character_owner_hash = '4'
username_1 = StateBackend().authenticate(token=t).username
t.character_owner_hash = '5'
username_2 = StateBackend().authenticate(token=t).username
self.assertNotEqual(username, username_1, username_2)
self.assertTrue(username_1.endswith('_1'))
self.assertTrue(username_2.endswith('_2'))

View File

@ -0,0 +1,35 @@
from io import StringIO
from django.core.management import call_command
from django.test import TestCase
from allianceauth.tests.auth_utils import AuthUtils
from ..models import CharacterOwnership, UserProfile
class ManagementCommandTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'test character', '1', '2', 'test corp', 'test')
character = UserProfile.objects.get(user=cls.user).main_character
CharacterOwnership.objects.create(user=cls.user, character=character, owner_hash='test')
def setUp(self):
self.stdout = StringIO()
def test_ownership(self):
call_command('checkmains', stdout=self.stdout)
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
self.assertNotIn(self.user.username, self.stdout.getvalue())
self.assertIn('All main characters', self.stdout.getvalue())
def test_no_ownership(self):
user = AuthUtils.create_user('v1 user', disconnect_signals=True)
AuthUtils.add_main_character(user, 'v1 character', '10', '20', 'test corp', 'test')
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
call_command('checkmains', stdout=self.stdout)
self.assertEqual(UserProfile.objects.filter(main_character__isnull=True).count(), 1)
self.assertIn(user.username, self.stdout.getvalue())

View File

@ -0,0 +1,68 @@
from unittest import mock
from urllib import parse
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.http.response import HttpResponse
from django.shortcuts import reverse
from django.test import TestCase
from django.test.client import RequestFactory
from allianceauth.eveonline.models import EveCharacter
from allianceauth.tests.auth_utils import AuthUtils
from ..decorators import main_character_required
from ..models import CharacterOwnership
MODULE_PATH = 'allianceauth.authentication'
class DecoratorTestCase(TestCase):
@staticmethod
@main_character_required
def dummy_view(*args, **kwargs):
return HttpResponse(status=200)
@classmethod
def setUpTestData(cls):
cls.main_user = AuthUtils.create_user('main_user', disconnect_signals=True)
cls.no_main_user = AuthUtils.create_user(
'no_main_user', disconnect_signals=True
)
main_character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
CharacterOwnership.objects.create(
user=cls.main_user, character=main_character, owner_hash='1'
)
cls.main_user.profile.main_character = main_character
def setUp(self):
self.request = RequestFactory().get('/test/')
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_login_redirect(self, m):
setattr(self.request, 'user', AnonymousUser())
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
self.assertEqual(parse.urlparse(url).path, reverse(settings.LOGIN_URL))
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_main_character_redirect(self, m):
setattr(self.request, 'user', self.no_main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
self.assertEqual(url, reverse('authentication:dashboard'))
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_successful_request(self, m):
setattr(self.request, 'user', self.main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 200)

View File

@ -1,147 +1,20 @@
from unittest import mock from unittest import mock
from io import StringIO
from urllib import parse
from django.conf import settings from django.contrib.auth.models import User
from django.contrib.auth.models import AnonymousUser, User
from django.core.management import call_command
from django.http.response import HttpResponse
from django.shortcuts import reverse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory
from allianceauth.authentication.decorators import main_character_required
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\ from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo,\
EveAllianceInfo EveAllianceInfo
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from esi.errors import IncompleteResponseError from esi.errors import IncompleteResponseError
from esi.models import Token from esi.models import Token
from ..backends import StateBackend from ..models import CharacterOwnership, State, get_guest_state
from ..models import CharacterOwnership, UserProfile, State, get_guest_state,\
OwnershipRecord
from ..tasks import check_character_ownership from ..tasks import check_character_ownership
MODULE_PATH = 'allianceauth.authentication' MODULE_PATH = 'allianceauth.authentication'
class DecoratorTestCase(TestCase):
@staticmethod
@main_character_required
def dummy_view(*args, **kwargs):
return HttpResponse(status=200)
@classmethod
def setUpTestData(cls):
cls.main_user = AuthUtils.create_user('main_user', disconnect_signals=True)
cls.no_main_user = AuthUtils.create_user('no_main_user', disconnect_signals=True)
main_character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
CharacterOwnership.objects.create(user=cls.main_user, character=main_character, owner_hash='1')
cls.main_user.profile.main_character = main_character
def setUp(self):
self.request = RequestFactory().get('/test/')
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_login_redirect(self, m):
setattr(self.request, 'user', AnonymousUser())
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
self.assertEqual(parse.urlparse(url).path, reverse(settings.LOGIN_URL))
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_main_character_redirect(self, m):
setattr(self.request, 'user', self.no_main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
self.assertEqual(url, reverse('authentication:dashboard'))
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_successful_request(self, m):
setattr(self.request, 'user', self.main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 200)
class BackendTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.main_character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.alt_character = EveCharacter.objects.create(
character_id=2,
character_name='Alt Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.unclaimed_character = EveCharacter.objects.create(
character_id=3,
character_name='Unclaimed Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
cls.old_user = AuthUtils.create_user('old_user', disconnect_signals=True)
AuthUtils.disconnect_signals()
CharacterOwnership.objects.create(user=cls.user, character=cls.main_character, owner_hash='1')
CharacterOwnership.objects.create(user=cls.user, character=cls.alt_character, owner_hash='2')
UserProfile.objects.update_or_create(user=cls.user, defaults={'main_character': cls.main_character})
AuthUtils.connect_signals()
def test_authenticate_main_character(self):
t = Token(character_id=self.main_character.character_id, character_owner_hash='1')
user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user)
def test_authenticate_alt_character(self):
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user)
def test_authenticate_unclaimed_character(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3')
user = StateBackend().authenticate(token=t)
self.assertNotEqual(user, self.user)
self.assertEqual(user.username, 'Unclaimed_Character')
self.assertEqual(user.profile.main_character, self.unclaimed_character)
def test_authenticate_character_record(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4')
record = OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4')
user = StateBackend().authenticate(token=t)
self.assertEqual(user, self.old_user)
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
self.assertTrue(user.profile.main_character)
def test_iterate_username(self):
t = Token(character_id=self.unclaimed_character.character_id,
character_name=self.unclaimed_character.character_name, character_owner_hash='3')
username = StateBackend().authenticate(token=t).username
t.character_owner_hash = '4'
username_1 = StateBackend().authenticate(token=t).username
t.character_owner_hash = '5'
username_2 = StateBackend().authenticate(token=t).username
self.assertNotEqual(username, username_1, username_2)
self.assertTrue(username_1.endswith('_1'))
self.assertTrue(username_2.endswith('_2'))
class CharacterOwnershipTestCase(TestCase): class CharacterOwnershipTestCase(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -378,30 +251,3 @@ class CharacterOwnershipCheckTestCase(TestCase):
filter.return_value.exists.return_value = False filter.return_value.exists.return_value = False
check_character_ownership(self.ownership) check_character_ownership(self.ownership)
self.assertTrue(filter.return_value.delete.called) self.assertTrue(filter.return_value.delete.called)
class ManagementCommandTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'test character', '1', '2', 'test corp', 'test')
character = UserProfile.objects.get(user=cls.user).main_character
CharacterOwnership.objects.create(user=cls.user, character=character, owner_hash='test')
def setUp(self):
self.stdout = StringIO()
def test_ownership(self):
call_command('checkmains', stdout=self.stdout)
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
self.assertNotIn(self.user.username, self.stdout.getvalue())
self.assertIn('All main characters', self.stdout.getvalue())
def test_no_ownership(self):
user = AuthUtils.create_user('v1 user', disconnect_signals=True)
AuthUtils.add_main_character(user, 'v1 character', '10', '20', 'test corp', 'test')
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
call_command('checkmains', stdout=self.stdout)
self.assertEqual(UserProfile.objects.filter(main_character__isnull=True).count(), 1)
self.assertIn(user.username, self.stdout.getvalue())

View File

@ -33,7 +33,8 @@ class DiscordService(ServicesHook):
if self.user_has_account(user): if self.user_has_account(user):
logger.debug('Deleting user %s %s account', user, self.name) logger.debug('Deleting user %s %s account', user, self.name)
tasks.delete_user.apply_async( tasks.delete_user.apply_async(
kwargs={'user_pk': user.pk}, priority=SINGLE_TASK_PRIORITY kwargs={'user_pk': user.pk, 'notify_user': notify_user},
priority=SINGLE_TASK_PRIORITY
) )
def render_services_ctrl(self, request): def render_services_ctrl(self, request):

View File

@ -6,7 +6,6 @@ from requests.exceptions import HTTPError
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from allianceauth.authentication.models import UserProfile
from allianceauth.services.tasks import QueueOnce from allianceauth.services.tasks import QueueOnce
from . import __title__ from . import __title__

View File

@ -3,6 +3,7 @@ from unittest.mock import patch
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from django.test.utils import override_settings from django.test.utils import override_settings
from allianceauth.notifications.models import Notification
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from . import TEST_USER_NAME, TEST_USER_ID, add_permissions_to_members, MODULE_PATH from . import TEST_USER_NAME, TEST_USER_ID, add_permissions_to_members, MODULE_PATH
@ -30,6 +31,7 @@ class TestDiscordService(TestCase):
self.service = DiscordService self.service = DiscordService
add_permissions_to_members() add_permissions_to_members()
self.factory = RequestFactory() self.factory = RequestFactory()
Notification.objects.all().delete()
def test_service_enabled(self): def test_service_enabled(self):
service = self.service() service = self.service()
@ -95,10 +97,11 @@ class TestDiscordService(TestCase):
mock_DiscordClient.return_value.remove_guild_member.return_value = True mock_DiscordClient.return_value.remove_guild_member.return_value = True
service = self.service() service = self.service()
service.delete_user(self.member) service.delete_user(self.member, notify_user=True)
self.assertTrue(mock_DiscordClient.return_value.remove_guild_member.called) self.assertTrue(mock_DiscordClient.return_value.remove_guild_member.called)
self.assertFalse(DiscordUser.objects.filter(user=self.member).exists()) 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) @patch(MODULE_PATH + '.managers.DiscordClient', spec=DiscordClient)
def test_delete_user_is_not_member(self, mock_DiscordClient): def test_delete_user_is_not_member(self, mock_DiscordClient):

View File

@ -21,6 +21,7 @@ from django.test.utils import override_settings
from allianceauth.authentication.models import State from allianceauth.authentication.models import State
from allianceauth.eveonline.models import EveCharacter from allianceauth.eveonline.models import EveCharacter
from allianceauth.notifications.models import Notification
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from . import ( from . import (
@ -97,6 +98,7 @@ def reset_testdata():
State.objects.all().delete() State.objects.all().delete()
EveCharacter.objects.all().delete() EveCharacter.objects.all().delete()
AuthUtils.connect_signals() AuthUtils.connect_signals()
Notification.objects.all().delete()
@patch(MODULE_PATH + '.models.DISCORD_GUILD_ID', TEST_GUILD_ID) @patch(MODULE_PATH + '.models.DISCORD_GUILD_ID', TEST_GUILD_ID)
@ -109,9 +111,10 @@ class TestServiceFeatures(TransactionTestCase):
super().setUpClass() super().setUpClass()
cls.maxDiff = None cls.maxDiff = None
def setUp(self): def setUp(self):
"""All tests: Given a user with member state, service permission and active Discord account""" """All tests: Given a user with member state,
service permission and active Discord account
"""
clear_cache() clear_cache()
reset_testdata() reset_testdata()
self.group_charlie = Group.objects.create(name='charlie') self.group_charlie = Group.objects.create(name='charlie')
@ -200,8 +203,7 @@ class TestServiceFeatures(TransactionTestCase):
# should not have called the API # should not have called the API
requests_made = [ requests_made = [
requests_made.append(DiscordRequest(r.method, r.url)) DiscordRequest(r.method, r.url) for r in requests_mocker.request_history
for r in requests_mocker.request_history
] ]
self.assertListEqual(requests_made, list()) self.assertListEqual(requests_made, list())
@ -232,13 +234,16 @@ class TestServiceFeatures(TransactionTestCase):
] ]
self.assertIn(remove_guild_member_request, requests_made) self.assertIn(remove_guild_member_request, requests_made)
# verify user has been notified
self.assertTrue(Notification.objects.filter(user=self.user).exists())
def test_when_member_changes_to_blue_state_then_roles_are_updated_accordingly( def test_when_member_changes_to_blue_state_then_roles_are_updated_accordingly(
self, requests_mocker self, requests_mocker
): ):
# request mocks # request mocks
requests_mocker.get( requests_mocker.get(
guild_member_request.url, guild_member_request.url,
json={'user': create_user_info(),'roles': ['3', '13', '99']} json={'user': create_user_info(), 'roles': ['3', '13', '99']}
) )
requests_mocker.get( requests_mocker.get(
guild_roles_request.url, guild_roles_request.url,
@ -322,7 +327,7 @@ class TestServiceFeatures(TransactionTestCase):
self.user.groups.add(self.group_charlie) self.user.groups.add(self.group_charlie)
self.user.refresh_from_db() self.user.refresh_from_db()
# verify roles for user where updated # verify roles for user where updated
roles_updated = False roles_updated = False
for r in requests_mocker.request_history: for r in requests_mocker.request_history:
my_request = DiscordRequest(r.method, r.url) my_request = DiscordRequest(r.method, r.url)
@ -346,8 +351,14 @@ class StateTestCase(TestCase):
reset_testdata() reset_testdata()
self.user = AuthUtils.create_user('test_user', disconnect_signals=True) self.user = AuthUtils.create_user('test_user', disconnect_signals=True)
AuthUtils.add_main_character(self.user, 'Perm Test Character', '99', corp_id='100', alliance_id='200', AuthUtils.add_main_character(
corp_name='Perm Test Corp', alliance_name='Perm Test Alliance') self.user,
'Perm Test Character', '99',
corp_id='100',
alliance_id='200',
corp_name='Perm Test Corp',
alliance_name='Perm Test Alliance'
)
self.test_character = EveCharacter.objects.get(character_id='99') self.test_character = EveCharacter.objects.get(character_id='99')
self.member_state = State.objects.create( self.member_state = State.objects.create(
name='Test Member', name='Test Member',
@ -358,7 +369,9 @@ class StateTestCase(TestCase):
self.member_state.member_characters.add(self.test_character) self.member_state.member_characters.add(self.test_character)
def _add_discord_user(self): def _add_discord_user(self):
self.discord_user = DiscordUser.objects.create(user=self.user, uid="12345678910") self.discord_user = DiscordUser.objects.create(
user=self.user, uid="12345678910"
)
def _refresh_user(self): def _refresh_user(self):
self.user = User.objects.get(pk=self.user.pk) self.user = User.objects.get(pk=self.user.pk)
@ -636,4 +649,3 @@ class TestUserFeatures(WebTest):
# check we got can see the page and the "link server" button # check we got can see the page and the "link server" button
self.assertEqual(response.status_int, 200) self.assertEqual(response.status_int, 200)
self.assertIsNotNone(response.html.find(id='btnLinkDiscordServer')) self.assertIsNotNone(response.html.find(id='btnLinkDiscordServer'))