diff --git a/allianceauth/eveonline/models.py b/allianceauth/eveonline/models.py index 390c00b4..1a7f5c39 100644 --- a/allianceauth/eveonline/models.py +++ b/allianceauth/eveonline/models.py @@ -1,13 +1,27 @@ -from django.db import models +import logging from typing import Union -from .managers import EveCharacterManager, EveCharacterProviderManager -from .managers import EveCorporationManager, EveCorporationProviderManager -from .managers import EveAllianceManager, EveAllianceProviderManager +from django.core.exceptions import ObjectDoesNotExist +from django.db import models +from esi.models import Token + +from allianceauth.notifications import notify + from . import providers from .evelinks import eveimageserver +from .managers import ( + EveAllianceManager, + EveAllianceProviderManager, + EveCharacterManager, + EveCharacterProviderManager, + EveCorporationManager, + EveCorporationProviderManager, +) + +logger = logging.getLogger(__name__) _DEFAULT_IMAGE_SIZE = 32 +DOOMHEIM_CORPORATION_ID = 1000001 class EveFactionInfo(models.Model): @@ -68,13 +82,12 @@ class EveAllianceInfo(models.Model): for corp_id in alliance.corp_ids: if not EveCorporationInfo.objects.filter(corporation_id=corp_id).exists(): EveCorporationInfo.objects.create_corporation(corp_id) - EveCorporationInfo.objects.filter( - corporation_id__in=alliance.corp_ids).update(alliance=self + EveCorporationInfo.objects.filter(corporation_id__in=alliance.corp_ids).update( + alliance=self ) - EveCorporationInfo.objects\ - .filter(alliance=self)\ - .exclude(corporation_id__in=alliance.corp_ids)\ - .update(alliance=None) + EveCorporationInfo.objects.filter(alliance=self).exclude( + corporation_id__in=alliance.corp_ids + ).update(alliance=None) def update_alliance(self, alliance: providers.Alliance = None): if alliance is None: @@ -182,6 +195,7 @@ class EveCorporationInfo(models.Model): class EveCharacter(models.Model): + """Character in Eve Online""" character_id = models.PositiveIntegerField(unique=True) character_name = models.CharField(max_length=254, unique=True) corporation_id = models.PositiveIntegerField() @@ -198,12 +212,20 @@ class EveCharacter(models.Model): class Meta: indexes = [ - models.Index(fields=['corporation_id',]), - models.Index(fields=['alliance_id',]), - models.Index(fields=['corporation_name',]), - models.Index(fields=['alliance_name',]), - models.Index(fields=['faction_id',]), - ] + models.Index(fields=['corporation_id',]), + models.Index(fields=['alliance_id',]), + models.Index(fields=['corporation_name',]), + models.Index(fields=['alliance_name',]), + models.Index(fields=['faction_id',]), + ] + + def __str__(self): + return self.character_name + + @property + def is_biomassed(self) -> bool: + """Whether this character is dead or not.""" + return self.corporation_id == DOOMHEIM_CORPORATION_ID @property def alliance(self) -> Union[EveAllianceInfo, None]: @@ -249,10 +271,36 @@ class EveCharacter(models.Model): self.faction_id = character.faction.id self.faction_name = character.faction.name self.save() + if self.is_biomassed: + self._remove_tokens_of_biomassed_character() return self - def __str__(self): - return self.character_name + def _remove_tokens_of_biomassed_character(self) -> None: + """Remove tokens of this biomassed character.""" + try: + user = self.character_ownership.user + except ObjectDoesNotExist: + return + tokens_to_delete = Token.objects.filter(character_id=self.character_id) + tokens_count = tokens_to_delete.count() + if not tokens_count: + return + tokens_to_delete.delete() + logger.info( + "%d tokens from user %s for biomassed character %s [id:%s] deleted.", + tokens_count, + user, + self, + self.character_id, + ) + notify( + user=user, + title=f"Character {self} biomassed", + message=( + f"Your former character {self} has been biomassed " + "and has been removed from the list of your alts." + ) + ) @staticmethod def generic_portrait_url( @@ -336,7 +384,6 @@ class EveCharacter(models.Model): """image URL for alliance of this character or empty string""" return self.alliance_logo_url(256) - def faction_logo_url(self, size=_DEFAULT_IMAGE_SIZE) -> str: """image URL for alliance of this character or empty string""" if self.faction_id: diff --git a/allianceauth/eveonline/providers.py b/allianceauth/eveonline/providers.py index f399613e..fb1749dd 100644 --- a/allianceauth/eveonline/providers.py +++ b/allianceauth/eveonline/providers.py @@ -170,7 +170,7 @@ class EveProvider: """ :return: an ItemType object for the given ID """ - raise NotImplemented() + raise NotImplementedError() class EveSwaggerProvider(EveProvider): @@ -207,7 +207,8 @@ class EveSwaggerProvider(EveProvider): def __str__(self): return 'esi' - def get_alliance(self, alliance_id): + def get_alliance(self, alliance_id: int) -> Alliance: + """Fetch alliance from ESI.""" try: data = self.client.Alliance.get_alliances_alliance_id(alliance_id=alliance_id).result() corps = self.client.Alliance.get_alliances_alliance_id_corporations(alliance_id=alliance_id).result() @@ -223,7 +224,8 @@ class EveSwaggerProvider(EveProvider): except HTTPNotFound: raise ObjectNotFound(alliance_id, 'alliance') - def get_corp(self, corp_id): + def get_corp(self, corp_id: int) -> Corporation: + """Fetch corporation from ESI.""" try: data = self.client.Corporation.get_corporations_corporation_id(corporation_id=corp_id).result() model = Corporation( @@ -239,29 +241,43 @@ class EveSwaggerProvider(EveProvider): except HTTPNotFound: raise ObjectNotFound(corp_id, 'corporation') - def get_character(self, character_id): + def get_character(self, character_id: int) -> Character: + """Fetch character from ESI.""" try: - data = self.client.Character.get_characters_character_id(character_id=character_id).result() + character_name = self._fetch_character_name(character_id) affiliation = self.client.Character.post_characters_affiliation(characters=[character_id]).result()[0] - model = Character( id=character_id, - name=data['name'], + name=character_name, corp_id=affiliation['corporation_id'], alliance_id=affiliation['alliance_id'] if 'alliance_id' in affiliation else None, faction_id=affiliation['faction_id'] if 'faction_id' in affiliation else None, ) return model - except (HTTPNotFound, HTTPUnprocessableEntity): + except (HTTPNotFound, HTTPUnprocessableEntity, ObjectNotFound): raise ObjectNotFound(character_id, 'character') + def _fetch_character_name(self, character_id: int) -> str: + """Fetch character name from ESI.""" + data = self.client.Universe.post_universe_names(ids=[character_id]).result() + character = data.pop() if data else None + if ( + not character + or character["category"] != "character" + or character["id"] != character_id + ): + raise ObjectNotFound(character_id, 'character') + return character["name"] + def get_all_factions(self): + """Fetch all factions from ESI.""" if not self._faction_list: self._faction_list = self.client.Universe.get_universe_factions().result() return self._faction_list - def get_faction(self, faction_id): - faction_id=int(faction_id) + def get_faction(self, faction_id: int): + """Fetch faction from ESI.""" + faction_id = int(faction_id) try: if not self._faction_list: _ = self.get_all_factions() @@ -273,7 +289,8 @@ class EveSwaggerProvider(EveProvider): except (HTTPNotFound, HTTPUnprocessableEntity, KeyError): raise ObjectNotFound(faction_id, 'faction') - def get_itemtype(self, type_id): + def get_itemtype(self, type_id: int) -> ItemType: + """Fetch inventory item from ESI.""" try: data = self.client.Universe.get_universe_types_type_id(type_id=type_id).result() return ItemType(id=type_id, name=data['name']) diff --git a/allianceauth/eveonline/tasks.py b/allianceauth/eveonline/tasks.py index 13825f9b..a6b11a24 100644 --- a/allianceauth/eveonline/tasks.py +++ b/allianceauth/eveonline/tasks.py @@ -1,12 +1,11 @@ import logging from celery import shared_task -from .models import EveAllianceInfo -from .models import EveCharacter -from .models import EveCorporationInfo +from .models import EveAllianceInfo, EveCharacter, EveCorporationInfo from . import providers + logger = logging.getLogger(__name__) TASK_PRIORITY = 7 @@ -32,8 +31,8 @@ def update_alliance(alliance_id): @shared_task -def update_character(character_id): - """Update given character from ESI""" +def update_character(character_id: int) -> None: + """Update given character from ESI.""" EveCharacter.objects.update_character(character_id) @@ -65,17 +64,17 @@ def update_character_chunk(character_ids_chunk: list): .post_characters_affiliation(characters=character_ids_chunk).result() character_names = providers.provider.client.Universe\ .post_universe_names(ids=character_ids_chunk).result() - except: + except OSError: logger.info("Failed to bulk update characters. Attempting single updates") for character_id in character_ids_chunk: update_character.apply_async( - args=[character_id], priority=TASK_PRIORITY - ) + args=[character_id], priority=TASK_PRIORITY + ) return affiliations = { - affiliation.get('character_id'): affiliation - for affiliation in affiliations_raw + affiliation.get('character_id'): affiliation + for affiliation in affiliations_raw } # add character names to affiliations for character in character_names: @@ -108,5 +107,5 @@ def update_character_chunk(character_ids_chunk: list): if corp_changed or alliance_changed or name_changed: update_character.apply_async( - args=[character.get('character_id')], priority=TASK_PRIORITY - ) + args=[character.get('character_id')], priority=TASK_PRIORITY + ) diff --git a/allianceauth/eveonline/tests/esi_client_stub.py b/allianceauth/eveonline/tests/esi_client_stub.py new file mode 100644 index 00000000..1eadfbd3 --- /dev/null +++ b/allianceauth/eveonline/tests/esi_client_stub.py @@ -0,0 +1,168 @@ +from bravado.exception import HTTPNotFound + + +class BravadoResponseStub: + """Stub for IncomingResponse in bravado, e.g. for HTTPError exceptions""" + + def __init__( + self, status_code, reason="", text="", headers=None, raw_bytes=None + ) -> None: + self.reason = reason + self.status_code = status_code + self.text = text + self.headers = headers if headers else dict() + self.raw_bytes = raw_bytes + + def __str__(self): + return f"{self.status_code} {self.reason}" + + +class BravadoOperationStub: + """Stub to simulate the operation object return from bravado via django-esi""" + + class RequestConfig: + def __init__(self, also_return_response): + self.also_return_response = also_return_response + + class ResponseStub: + def __init__(self, headers): + self.headers = headers + + def __init__(self, data, headers: dict = None, also_return_response: bool = False): + self._data = data + self._headers = headers if headers else {"x-pages": 1} + self.request_config = BravadoOperationStub.RequestConfig(also_return_response) + + def result(self, **kwargs): + if self.request_config.also_return_response: + return [self._data, self.ResponseStub(self._headers)] + else: + return self._data + + def results(self, **kwargs): + return self.result(**kwargs) + + +class EsiClientStub: + """Stub for an ESI client.""" + class Alliance: + @staticmethod + def get_alliances_alliance_id(alliance_id): + data = { + 3001: { + "name": "Wayne Enterprises", + "ticker": "WYE", + "executor_corporation_id": 2001 + } + } + try: + return BravadoOperationStub(data[int(alliance_id)]) + except KeyError: + response = BravadoResponseStub( + 404, f"Alliance with ID {alliance_id} not found" + ) + raise HTTPNotFound(response) + + @staticmethod + def get_alliances_alliance_id_corporations(alliance_id): + data = [2001, 2002, 2003] + return BravadoOperationStub(data) + + class Character: + @staticmethod + def get_characters_character_id(character_id): + data = { + 1001: { + "corporation_id": 2001, + "name": "Bruce Wayne", + }, + 1002: { + "corporation_id": 2001, + "name": "Peter Parker", + }, + 1011: { + "corporation_id": 2011, + "name": "Lex Luthor", + } + } + try: + return BravadoOperationStub(data[int(character_id)]) + except KeyError: + response = BravadoResponseStub( + 404, f"Character with ID {character_id} not found" + ) + raise HTTPNotFound(response) + + @staticmethod + def post_characters_affiliation(characters: list): + data = [ + {'character_id': 1001, 'corporation_id': 2001, 'alliance_id': 3001}, + {'character_id': 1002, 'corporation_id': 2001, 'alliance_id': 3001}, + {'character_id': 1011, 'corporation_id': 2011}, + {'character_id': 1666, 'corporation_id': 1000001}, + ] + return BravadoOperationStub( + [x for x in data if x['character_id'] in characters] + ) + + class Corporation: + @staticmethod + def get_corporations_corporation_id(corporation_id): + data = { + 2001: { + "ceo_id": 1091, + "member_count": 10, + "name": "Wayne Technologies", + "ticker": "WTE", + "alliance_id": 3001 + }, + 2002: { + "ceo_id": 1092, + "member_count": 10, + "name": "Wayne Food", + "ticker": "WFO", + "alliance_id": 3001 + }, + 2003: { + "ceo_id": 1093, + "member_count": 10, + "name": "Wayne Energy", + "ticker": "WEG", + "alliance_id": 3001 + }, + 2011: { + "ceo_id": 1, + "member_count": 3, + "name": "LexCorp", + "ticker": "LC", + }, + 1000001: { + "ceo_id": 3000001, + "creator_id": 1, + "description": "The internal corporation used for characters in graveyard.", + "member_count": 6329026, + "name": "Doomheim", + "ticker": "666", + } + } + try: + return BravadoOperationStub(data[int(corporation_id)]) + except KeyError: + response = BravadoResponseStub( + 404, f"Corporation with ID {corporation_id} not found" + ) + raise HTTPNotFound(response) + + class Universe: + @staticmethod + def post_universe_names(ids: list): + data = [ + {"category": "character", "id": 1001, "name": "Bruce Wayne"}, + {"category": "character", "id": 1002, "name": "Peter Parker"}, + {"category": "character", "id": 1011, "name": "Lex Luthor"}, + {"category": "character", "id": 1666, "name": "Hal Jordan"}, + {"category": "corporation", "id": 2001, "name": "Wayne Technologies"}, + {"category": "corporation","id": 2002, "name": "Wayne Food"}, + {"category": "corporation","id": 1000001, "name": "Doomheim"}, + ] + return BravadoOperationStub([x for x in data if x['id'] in ids]) diff --git a/allianceauth/eveonline/tests/test_models.py b/allianceauth/eveonline/tests/test_models.py index fc716c00..94c9614a 100644 --- a/allianceauth/eveonline/tests/test_models.py +++ b/allianceauth/eveonline/tests/test_models.py @@ -1,12 +1,15 @@ from unittest.mock import Mock, patch +from django.core.exceptions import ObjectDoesNotExist from django.test import TestCase +from esi.models import Token + +from allianceauth.tests.auth_utils import AuthUtils -from ..models import ( - EveCharacter, EveCorporationInfo, EveAllianceInfo, EveFactionInfo -) -from ..providers import Alliance, Corporation, Character from ..evelinks import eveimageserver +from ..models import EveAllianceInfo, EveCharacter, EveCorporationInfo, EveFactionInfo +from ..providers import Alliance, Character, Corporation +from .esi_client_stub import EsiClientStub class EveCharacterTestCase(TestCase): @@ -402,8 +405,8 @@ class EveAllianceTestCase(TestCase): my_alliance.save() my_alliance.populate_alliance() - for corporation in EveCorporationInfo.objects\ - .filter(corporation_id__in=[2001, 2002] + for corporation in ( + EveCorporationInfo.objects.filter(corporation_id__in=[2001, 2002]) ): self.assertEqual(corporation.alliance, my_alliance) @@ -587,3 +590,98 @@ class EveCorporationTestCase(TestCase): self.my_corp.logo_url_256, 'https://images.evetech.net/corporations/2001/logo?size=256' ) + + +@patch('allianceauth.eveonline.providers.esi_client_factory') +@patch("allianceauth.eveonline.models.notify") +class TestCharacterUpdate(TestCase): + def test_should_update_normal_character(self, mock_notify, mock_esi_client_factory): + # given + mock_esi_client_factory.return_value = EsiClientStub() + my_character = EveCharacter.objects.create( + character_id=1001, + character_name="not my name", + corporation_id=2002, + corporation_name="Wayne Food", + corporation_ticker="WYF", + alliance_id=None + ) + # when + my_character.update_character() + # then + my_character.refresh_from_db() + self.assertEqual(my_character.character_name, "Bruce Wayne") + self.assertEqual(my_character.corporation_id, 2001) + self.assertEqual(my_character.corporation_name, "Wayne Technologies") + self.assertEqual(my_character.corporation_ticker, "WTE") + self.assertEqual(my_character.alliance_id, 3001) + self.assertEqual(my_character.alliance_name, "Wayne Enterprises") + self.assertEqual(my_character.alliance_ticker, "WYE") + self.assertFalse(mock_notify.called) + + def test_should_update_dead_character_with_owner( + self, mock_notify, mock_esi_client_factory + ): + # given + mock_esi_client_factory.return_value = EsiClientStub() + character_1666 = EveCharacter.objects.create( + character_id=1666, + character_name="Hal Jordan", + corporation_id=2002, + corporation_name="Wayne Food", + corporation_ticker="WYF", + alliance_id=None + ) + user = AuthUtils.create_user("Bruce Wayne") + token_1666 = Token.objects.create( + user=user, + character_id=character_1666.character_id, + character_name=character_1666.character_name, + character_owner_hash="ABC123-1666", + ) + character_1001 = EveCharacter.objects.create( + character_id=1001, + character_name="Bruce Wayne", + corporation_id=2001, + corporation_name="Wayne Technologies", + corporation_ticker="WYT", + alliance_id=None + ) + token_1001 = Token.objects.create( + user=user, + character_id=character_1001.character_id, + character_name=character_1001.character_name, + character_owner_hash="ABC123-1001", + ) + # when + character_1666.update_character() + # then + character_1666.refresh_from_db() + self.assertTrue(character_1666.is_biomassed) + self.assertNotIn(token_1666, user.token_set.all()) + self.assertIn(token_1001, user.token_set.all()) + with self.assertRaises(ObjectDoesNotExist): + self.assertTrue(character_1666.character_ownership) + user.profile.refresh_from_db() + self.assertIsNone(user.profile.main_character) + self.assertTrue(mock_notify.called) + + def test_should_handle_dead_character_without_owner( + self, mock_notify, mock_esi_client_factory + ): + # given + mock_esi_client_factory.return_value = EsiClientStub() + character_1666 = EveCharacter.objects.create( + character_id=1666, + character_name="Hal Jordan", + corporation_id=1011, + corporation_name="LexCorp", + corporation_ticker='LC', + alliance_id=None + ) + # when + character_1666.update_character() + # then + character_1666.refresh_from_db() + self.assertTrue(character_1666.is_biomassed) + self.assertFalse(mock_notify.called) diff --git a/allianceauth/eveonline/tests/test_providers.py b/allianceauth/eveonline/tests/test_providers.py index 59e9dfe6..95f4f069 100644 --- a/allianceauth/eveonline/tests/test_providers.py +++ b/allianceauth/eveonline/tests/test_providers.py @@ -7,6 +7,7 @@ from jsonschema.exceptions import RefResolutionError from django.test import TestCase from . import set_logger +from .esi_client_stub import EsiClientStub from ..providers import ( ObjectNotFound, Entity, @@ -632,13 +633,7 @@ class TestEveSwaggerProvider(TestCase): @patch(MODULE_PATH + '.esi_client_factory') def test_get_character(self, mock_esi_client_factory): - mock_esi_client_factory.return_value \ - .Character.get_characters_character_id \ - = TestEveSwaggerProvider.esi_get_characters_character_id - mock_esi_client_factory.return_value \ - .Character.post_characters_affiliation \ - = TestEveSwaggerProvider.esi_post_characters_affiliation - + mock_esi_client_factory.return_value = EsiClientStub() my_provider = EveSwaggerProvider() # character with alliance @@ -649,8 +644,8 @@ class TestEveSwaggerProvider(TestCase): self.assertEqual(my_character.alliance_id, 3001) # character wo/ alliance - my_character = my_provider.get_character(1002) - self.assertEqual(my_character.id, 1002) + my_character = my_provider.get_character(1011) + self.assertEqual(my_character.id, 1011) self.assertEqual(my_character.alliance_id, None) # character not found diff --git a/allianceauth/eveonline/tests/test_tasks.py b/allianceauth/eveonline/tests/test_tasks.py index 4bf40671..78054e8f 100644 --- a/allianceauth/eveonline/tests/test_tasks.py +++ b/allianceauth/eveonline/tests/test_tasks.py @@ -1,245 +1,271 @@ -from unittest.mock import patch, Mock +from unittest.mock import patch -from django.test import TestCase +from django.test import TestCase, TransactionTestCase, override_settings -from ..models import EveCharacter, EveCorporationInfo, EveAllianceInfo +from ..models import EveAllianceInfo, EveCharacter, EveCorporationInfo from ..tasks import ( + run_model_update, update_alliance, - update_corp, update_character, - run_model_update + update_character_chunk, + update_corp, ) +from .esi_client_stub import EsiClientStub -class TestTasks(TestCase): - - @patch('allianceauth.eveonline.tasks.EveCorporationInfo') - def test_update_corp(self, mock_EveCorporationInfo): - update_corp(42) - self.assertEqual( - mock_EveCorporationInfo.objects.update_corporation.call_count, 1 - ) - self.assertEqual( - mock_EveCorporationInfo.objects.update_corporation.call_args[0][0], 42 +@patch('allianceauth.eveonline.providers.esi_client_factory') +class TestUpdateTasks(TestCase): + def test_should_update_alliance(self, mock_esi_client_factory): + # given + mock_esi_client_factory.return_value = EsiClientStub() + my_alliance = EveAllianceInfo.objects.create( + alliance_id=3001, + alliance_name="Wayne Enterprises", + alliance_ticker="WYE", + executor_corp_id=2003 ) + # when + update_alliance(my_alliance.alliance_id) + # then + my_alliance.refresh_from_db() + self.assertEqual(my_alliance.executor_corp_id, 2001) - @patch('allianceauth.eveonline.tasks.EveAllianceInfo') - def test_update_alliance(self, mock_EveAllianceInfo): - update_alliance(42) - self.assertEqual( - mock_EveAllianceInfo.objects.update_alliance.call_args[0][0], 42 - ) - self.assertEqual( - mock_EveAllianceInfo.objects - .update_alliance.return_value.populate_alliance.call_count, 1 + def test_should_update_character(self, mock_esi_client_factory): + # given + mock_esi_client_factory.return_value = EsiClientStub() + my_character = EveCharacter.objects.create( + character_id=1001, + character_name="Bruce Wayne", + corporation_id=2002, + corporation_name="Wayne Food", + corporation_ticker="WYF", + alliance_id=None ) + # when + update_character(my_character.character_id) + # then + my_character.refresh_from_db() + self.assertEqual(my_character.corporation_id, 2001) - @patch('allianceauth.eveonline.tasks.EveCharacter') - def test_update_character(self, mock_EveCharacter): - update_character(42) - self.assertEqual( - mock_EveCharacter.objects.update_character.call_count, 1 + def test_should_update_corp(self, mock_esi_client_factory): + # given + mock_esi_client_factory.return_value = EsiClientStub() + EveAllianceInfo.objects.create( + alliance_id=3001, + alliance_name="Wayne Enterprises", + alliance_ticker="WYE", + executor_corp_id=2003 ) - self.assertEqual( - mock_EveCharacter.objects.update_character.call_args[0][0], 42 + my_corporation = EveCorporationInfo.objects.create( + corporation_id=2003, + corporation_name="Wayne Food", + corporation_ticker="WFO", + member_count=1, + alliance=None, + ceo_id=1999 ) + # when + update_corp(my_corporation.corporation_id) + # then + my_corporation.refresh_from_db() + self.assertEqual(my_corporation.alliance.alliance_id, 3001) + + # @patch('allianceauth.eveonline.tasks.EveCharacter') + # def test_update_character(self, mock_EveCharacter): + # update_character(42) + # self.assertEqual( + # mock_EveCharacter.objects.update_character.call_count, 1 + # ) + # self.assertEqual( + # mock_EveCharacter.objects.update_character.call_args[0][0], 42 + # ) -@patch('allianceauth.eveonline.tasks.update_character') -@patch('allianceauth.eveonline.tasks.update_alliance') -@patch('allianceauth.eveonline.tasks.update_corp') -@patch('allianceauth.eveonline.providers.provider') +@override_settings(CELERY_ALWAYS_EAGER=True) +@patch('allianceauth.eveonline.providers.esi_client_factory') +@patch('allianceauth.eveonline.tasks.providers') @patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2) -class TestRunModelUpdate(TestCase): - - @classmethod - def setUpClass(cls): - super().setUpClass() - EveCorporationInfo.objects.all().delete() - EveAllianceInfo.objects.all().delete() - EveCharacter.objects.all().delete() - +class TestRunModelUpdate(TransactionTestCase): + def test_should_run_updates(self, mock_providers, mock_esi_client_factory): + # given + mock_providers.provider.client = EsiClientStub() + mock_esi_client_factory.return_value = EsiClientStub() EveCorporationInfo.objects.create( - corporation_id=2345, - corporation_name='corp.name', - corporation_ticker='c.c.t', + corporation_id=2001, + corporation_name="Wayne Technologies", + corporation_ticker="WTE", member_count=10, alliance=None, ) - EveAllianceInfo.objects.create( - alliance_id=3456, - alliance_name='alliance.name', - alliance_ticker='a.t', - executor_corp_id=5, + alliance_3001 = EveAllianceInfo.objects.create( + alliance_id=3001, + alliance_name="Wayne Enterprises", + alliance_ticker="WYE", + executor_corp_id=2003 ) - EveCharacter.objects.create( - character_id=1, - character_name='character.name1', - corporation_id=2345, - corporation_name='character.corp.name', - corporation_ticker='c.c.t', # max 5 chars + corporation_2003 = EveCorporationInfo.objects.create( + corporation_id=2003, + corporation_name="Wayne Energy", + corporation_ticker="WEG", + member_count=99, + alliance=None, + ) + character_1001 = EveCharacter.objects.create( + character_id=1001, + character_name="Bruce Wayne", + corporation_id=2002, + corporation_name="Wayne Food", + corporation_ticker="WYF", alliance_id=None ) - EveCharacter.objects.create( - character_id=2, - character_name='character.name2', - corporation_id=9876, - corporation_name='character.corp.name', - corporation_ticker='c.c.t', # max 5 chars - alliance_id=3456, - alliance_name='character.alliance.name', - ) - EveCharacter.objects.create( - character_id=3, - character_name='character.name3', - corporation_id=9876, - corporation_name='character.corp.name', - corporation_ticker='c.c.t', # max 5 chars - alliance_id=3456, - alliance_name='character.alliance.name', - ) - EveCharacter.objects.create( - character_id=4, - character_name='character.name4', - corporation_id=9876, - corporation_name='character.corp.name', - corporation_ticker='c.c.t', # max 5 chars - alliance_id=3456, - alliance_name='character.alliance.name', - ) - """ - EveCharacter.objects.create( - character_id=5, - character_name='character.name5', - corporation_id=9876, - corporation_name='character.corp.name', - corporation_ticker='c.c.t', # max 5 chars - alliance_id=3456, - alliance_name='character.alliance.name', - ) - """ - - def setUp(self): - self.affiliations = [ - {'character_id': 1, 'corporation_id': 5}, - {'character_id': 2, 'corporation_id': 9876, 'alliance_id': 3456}, - {'character_id': 3, 'corporation_id': 9876, 'alliance_id': 7456}, - {'character_id': 4, 'corporation_id': 9876, 'alliance_id': 3456} - ] - self.names = [ - {'id': 1, 'name': 'character.name1'}, - {'id': 2, 'name': 'character.name2'}, - {'id': 3, 'name': 'character.name3'}, - {'id': 4, 'name': 'character.name4_new'} - ] - - def test_normal_run( - self, - mock_provider, - mock_update_corp, - mock_update_alliance, - mock_update_character, - ): - def get_affiliations(characters: list): - response = [x for x in self.affiliations if x['character_id'] in characters] - mock_operator = Mock(**{'result.return_value': response}) - return mock_operator - - def get_names(ids: list): - response = [x for x in self.names if x['id'] in ids] - mock_operator = Mock(**{'result.return_value': response}) - return mock_operator - - mock_provider.client.Character.post_characters_affiliation.side_effect \ - = get_affiliations - - mock_provider.client.Universe.post_universe_names.side_effect = get_names - + # when run_model_update() - + # then + character_1001.refresh_from_db() self.assertEqual( - mock_provider.client.Character.post_characters_affiliation.call_count, 2 + character_1001.corporation_id, 2001 # char has new corp ) + corporation_2003.refresh_from_db() self.assertEqual( - mock_provider.client.Universe.post_universe_names.call_count, 2 + corporation_2003.alliance.alliance_id, 3001 # corp has new alliance + ) + alliance_3001.refresh_from_db() + self.assertEqual( + alliance_3001.executor_corp_id, 2001 # alliance has been updated ) - # character 1 has changed corp - # character 2 no change - # character 3 has changed alliance - # character 4 has changed name - self.assertEqual(mock_update_corp.apply_async.call_count, 1) - self.assertEqual( - int(mock_update_corp.apply_async.call_args[1]['args'][0]), 2345 - ) - self.assertEqual(mock_update_alliance.apply_async.call_count, 1) - self.assertEqual( - int(mock_update_alliance.apply_async.call_args[1]['args'][0]), 3456 - ) - characters_updated = { - x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list + +@override_settings(CELERY_ALWAYS_EAGER=True) +@patch('allianceauth.eveonline.tasks.update_character', wraps=update_character) +@patch('allianceauth.eveonline.providers.esi_client_factory') +@patch('allianceauth.eveonline.tasks.providers') +@patch('allianceauth.eveonline.tasks.CHUNK_SIZE', 2) +class TestUpdateCharacterChunk(TestCase): + @staticmethod + def _updated_character_ids(spy_update_character) -> set: + """Character IDs passed to update_character task for update.""" + return { + x[1]["args"][0] for x in spy_update_character.apply_async.call_args_list } - excepted = {1, 3, 4} - self.assertSetEqual(characters_updated, excepted) - def test_ignore_character_not_in_affiliations( - self, - mock_provider, - mock_update_corp, - mock_update_alliance, - mock_update_character, + def test_should_update_corp_change( + self, mock_providers, mock_esi_client_factory, spy_update_character ): - def get_affiliations(characters: list): - response = [x for x in self.affiliations if x['character_id'] in characters] - mock_operator = Mock(**{'result.return_value': response}) - return mock_operator + # given + mock_providers.provider.client = EsiClientStub() + mock_esi_client_factory.return_value = EsiClientStub() + character_1001 = EveCharacter.objects.create( + character_id=1001, + character_name="Bruce Wayne", + corporation_id=2003, + corporation_name="Wayne Energy", + corporation_ticker="WEG", + alliance_id=3001, + alliance_name="Wayne Enterprises", + alliance_ticker="WYE", + ) + character_1002 = EveCharacter.objects.create( + character_id=1002, + character_name="Peter Parker", + corporation_id=2001, + corporation_name="Wayne Technologies", + corporation_ticker="WTE", + alliance_id=3001, + alliance_name="Wayne Enterprises", + alliance_ticker="WYE", + ) + # when + update_character_chunk([ + character_1001.character_id, character_1002.character_id + ]) + # then + character_1001.refresh_from_db() + self.assertEqual(character_1001.corporation_id, 2001) + self.assertSetEqual(self._updated_character_ids(spy_update_character), {1001}) - def get_names(ids: list): - response = [x for x in self.names if x['id'] in ids] - mock_operator = Mock(**{'result.return_value': response}) - return mock_operator - - del self.affiliations[0] - - mock_provider.client.Character.post_characters_affiliation.side_effect \ - = get_affiliations - - mock_provider.client.Universe.post_universe_names.side_effect = get_names - - run_model_update() - characters_updated = { - x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list - } - excepted = {3, 4} - self.assertSetEqual(characters_updated, excepted) - - def test_ignore_character_not_in_names( - self, - mock_provider, - mock_update_corp, - mock_update_alliance, - mock_update_character, + def test_should_update_name_change( + self, mock_providers, mock_esi_client_factory, spy_update_character ): - def get_affiliations(characters: list): - response = [x for x in self.affiliations if x['character_id'] in characters] - mock_operator = Mock(**{'result.return_value': response}) - return mock_operator + # given + mock_providers.provider.client = EsiClientStub() + mock_esi_client_factory.return_value = EsiClientStub() + character_1001 = EveCharacter.objects.create( + character_id=1001, + character_name="Batman", + corporation_id=2001, + corporation_name="Wayne Technologies", + corporation_ticker="WTE", + alliance_id=3001, + alliance_name="Wayne Technologies", + alliance_ticker="WYT", + ) + # when + update_character_chunk([character_1001.character_id]) + # then + character_1001.refresh_from_db() + self.assertEqual(character_1001.character_name, "Bruce Wayne") + self.assertSetEqual(self._updated_character_ids(spy_update_character), {1001}) - def get_names(ids: list): - response = [x for x in self.names if x['id'] in ids] - mock_operator = Mock(**{'result.return_value': response}) - return mock_operator + def test_should_update_alliance_change( + self, mock_providers, mock_esi_client_factory, spy_update_character + ): + # given + mock_providers.provider.client = EsiClientStub() + mock_esi_client_factory.return_value = EsiClientStub() + character_1001 = EveCharacter.objects.create( + character_id=1001, + character_name="Bruce Wayne", + corporation_id=2001, + corporation_name="Wayne Technologies", + corporation_ticker="WTE", + alliance_id=None, + ) + # when + update_character_chunk([character_1001.character_id]) + # then + character_1001.refresh_from_db() + self.assertEqual(character_1001.alliance_id, 3001) + self.assertSetEqual(self._updated_character_ids(spy_update_character), {1001}) - del self.names[3] + def test_should_not_update_when_not_changed( + self, mock_providers, mock_esi_client_factory, spy_update_character + ): + # given + mock_providers.provider.client = EsiClientStub() + mock_esi_client_factory.return_value = EsiClientStub() + character_1001 = EveCharacter.objects.create( + character_id=1001, + character_name="Bruce Wayne", + corporation_id=2001, + corporation_name="Wayne Technologies", + corporation_ticker="WTE", + alliance_id=3001, + alliance_name="Wayne Technologies", + alliance_ticker="WYT", + ) + # when + update_character_chunk([character_1001.character_id]) + # then + self.assertSetEqual(self._updated_character_ids(spy_update_character), set()) - mock_provider.client.Character.post_characters_affiliation.side_effect \ - = get_affiliations - - mock_provider.client.Universe.post_universe_names.side_effect = get_names - - run_model_update() - characters_updated = { - x[1]['args'][0] for x in mock_update_character.apply_async.call_args_list - } - excepted = {1, 3} - self.assertSetEqual(characters_updated, excepted) + def test_should_fall_back_to_single_updates_when_bulk_update_failed( + self, mock_providers, mock_esi_client_factory, spy_update_character + ): + # given + mock_providers.provider.client.Character.post_characters_affiliation\ + .side_effect = OSError + mock_esi_client_factory.return_value = EsiClientStub() + character_1001 = EveCharacter.objects.create( + character_id=1001, + character_name="Bruce Wayne", + corporation_id=2001, + corporation_name="Wayne Technologies", + corporation_ticker="WTE", + alliance_id=3001, + alliance_name="Wayne Technologies", + alliance_ticker="WYT", + ) + # when + update_character_chunk([character_1001.character_id]) + # then + self.assertSetEqual(self._updated_character_ids(spy_update_character), {1001})