aa-fleetfinder 2.7.2__py3-none-any.whl → 3.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aa-fleetfinder might be problematic. Click here for more details.

Files changed (32) hide show
  1. {aa_fleetfinder-2.7.2.dist-info → aa_fleetfinder-3.0.0b1.dist-info}/METADATA +5 -14
  2. {aa_fleetfinder-2.7.2.dist-info → aa_fleetfinder-3.0.0b1.dist-info}/RECORD +32 -32
  3. fleetfinder/__init__.py +5 -3
  4. fleetfinder/apps.py +5 -4
  5. fleetfinder/locale/cs_CZ/LC_MESSAGES/django.po +21 -22
  6. fleetfinder/locale/de/LC_MESSAGES/django.mo +0 -0
  7. fleetfinder/locale/de/LC_MESSAGES/django.po +27 -24
  8. fleetfinder/locale/django.pot +22 -23
  9. fleetfinder/locale/es/LC_MESSAGES/django.po +25 -22
  10. fleetfinder/locale/fr_FR/LC_MESSAGES/django.po +26 -22
  11. fleetfinder/locale/it_IT/LC_MESSAGES/django.po +21 -22
  12. fleetfinder/locale/ja/LC_MESSAGES/django.po +25 -22
  13. fleetfinder/locale/ko_KR/LC_MESSAGES/django.po +25 -22
  14. fleetfinder/locale/nl_NL/LC_MESSAGES/django.po +21 -22
  15. fleetfinder/locale/pl_PL/LC_MESSAGES/django.po +21 -22
  16. fleetfinder/locale/ru/LC_MESSAGES/django.po +25 -22
  17. fleetfinder/locale/sk/LC_MESSAGES/django.po +21 -22
  18. fleetfinder/locale/uk/LC_MESSAGES/django.po +26 -22
  19. fleetfinder/locale/zh_Hans/LC_MESSAGES/django.po +25 -22
  20. fleetfinder/providers.py +22 -4
  21. fleetfinder/tasks.py +279 -110
  22. fleetfinder/tests/__init__.py +39 -1
  23. fleetfinder/tests/test_access.py +2 -2
  24. fleetfinder/tests/test_auth_hooks.py +2 -2
  25. fleetfinder/tests/test_settings.py +3 -2
  26. fleetfinder/tests/test_tasks.py +1010 -34
  27. fleetfinder/tests/test_templatetags.py +2 -4
  28. fleetfinder/tests/test_user_agent.py +62 -14
  29. fleetfinder/tests/test_views.py +700 -52
  30. fleetfinder/views.py +102 -55
  31. {aa_fleetfinder-2.7.2.dist-info → aa_fleetfinder-3.0.0b1.dist-info}/WHEEL +0 -0
  32. {aa_fleetfinder-2.7.2.dist-info → aa_fleetfinder-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -3,19 +3,285 @@ Tests for the fleetfinder.tasks module.
3
3
  """
4
4
 
5
5
  # Standard Library
6
- from unittest import TestCase
7
- from unittest.mock import Mock, patch
6
+ from datetime import timedelta
7
+ from unittest.mock import MagicMock, Mock, patch
8
+
9
+ # Third Party
10
+ from aiopenapi3 import ContentTypeError
11
+
12
+ # Django
13
+ from django.utils import timezone
14
+
15
+ # Alliance Auth
16
+ from esi.exceptions import HTTPClientError
8
17
 
9
18
  # AA Fleet Finder
19
+ from fleetfinder.models import Fleet
10
20
  from fleetfinder.tasks import (
21
+ ESI_ERROR_GRACE_TIME,
22
+ ESI_MAX_ERROR_COUNT,
23
+ FleetViewAggregate,
24
+ _check_for_esi_fleet,
25
+ _close_esi_fleet,
26
+ _esi_fleet_error_handling,
27
+ _fetch_chunk,
11
28
  _get_fleet_aggregate,
29
+ _make_name_lookup,
30
+ _process_fleet,
31
+ _send_invitation,
12
32
  check_fleet_adverts,
33
+ get_fleet_composition,
34
+ send_fleet_invitation,
13
35
  )
36
+ from fleetfinder.tests import BaseTestCase
37
+
38
+
39
+ class TestFleetViewAggregateClass(BaseTestCase):
40
+ """
41
+ Tests for the FleetViewAggregate class.
42
+ """
43
+
44
+ def test_initializes_with_valid_fleet_and_aggregate(self):
45
+ """
46
+ Test that FleetViewAggregate initializes correctly with valid fleet and aggregate data.
47
+
48
+ :return:
49
+ :rtype:
50
+ """
51
+
52
+ fleet_data = [{"id": 1, "name": "Fleet Member 1"}]
53
+ aggregate_data = {"ship_type": 5}
54
+ aggregate = FleetViewAggregate(fleet=fleet_data, aggregate=aggregate_data)
55
+
56
+ self.assertEqual(aggregate.fleet, fleet_data)
57
+ self.assertEqual(aggregate.aggregate, aggregate_data)
58
+
59
+ def test_initializes_with_empty_fleet_and_aggregate(self):
60
+ """
61
+ Test that FleetViewAggregate initializes correctly with empty fleet and aggregate data.
62
+
63
+ :return:
64
+ :rtype:
65
+ """
66
+
67
+ fleet_data = []
68
+ aggregate_data = {}
69
+ aggregate = FleetViewAggregate(fleet=fleet_data, aggregate=aggregate_data)
70
+
71
+ self.assertEqual(aggregate.fleet, fleet_data)
72
+ self.assertEqual(aggregate.aggregate, aggregate_data)
73
+
74
+ def test_handles_large_fleet_and_aggregate(self):
75
+ """
76
+ Test that FleetViewAggregate handles large fleet and aggregate data.
77
+
78
+ :return:
79
+ :rtype:
80
+ """
81
+
82
+ fleet_data = [{"id": i, "name": f"Fleet Member {i}"} for i in range(1000)]
83
+ aggregate_data = {"ship_type": 1000}
84
+ aggregate = FleetViewAggregate(fleet=fleet_data, aggregate=aggregate_data)
85
+
86
+ self.assertEqual(len(aggregate.fleet), 1000)
87
+ self.assertEqual(aggregate.aggregate, aggregate_data)
88
+
89
+
90
+ class TestHelperSendInvitation(BaseTestCase):
91
+ """
92
+ Tests for the _send_invitation helper function.
93
+ """
94
+
95
+ def test_sends_invitation_successfully(self):
96
+ """
97
+ Test that _send_invitation sends an invitation successfully.
98
+
99
+ :return:
100
+ :rtype:
101
+ """
102
+
103
+ mock_esi_client = MagicMock()
104
+ mock_esi_client.Fleets.PostFleetsFleetIdMembers.return_value.result.return_value = (
105
+ None
106
+ )
107
+
108
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_esi_client)):
109
+ _send_invitation(12345, "mock_token", 67890)
110
+
111
+ mock_esi_client.Fleets.PostFleetsFleetIdMembers.assert_called_once_with(
112
+ fleet_id=67890,
113
+ token="mock_token",
114
+ body={"character_id": 12345, "role": "squad_member"},
115
+ )
116
+
117
+ def test_handles_esi_client_error_gracefully(self):
118
+ """
119
+ Test that _send_invitation handles ESIClientError gracefully.
120
+
121
+ :return:
122
+ :rtype:
123
+ """
124
+
125
+ mock_esi_client = MagicMock()
126
+ mock_esi_client.Fleets.PostFleetsFleetIdMembers.return_value.result.side_effect = HTTPClientError(
127
+ 500, {}, b""
128
+ )
14
129
 
130
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_esi_client)):
131
+ with self.assertRaises(HTTPClientError):
132
+ _send_invitation(12345, "mock_token", 67890)
15
133
 
16
- class TestGetFleetAggregate(TestCase):
134
+ mock_esi_client.Fleets.PostFleetsFleetIdMembers.assert_called_once_with(
135
+ fleet_id=67890,
136
+ token="mock_token",
137
+ body={"character_id": 12345, "role": "squad_member"},
138
+ )
139
+
140
+
141
+ class TestHelperCloseEsiFleet(BaseTestCase):
142
+ """
143
+ Tests for the _close_esi_fleet helper function.
144
+ """
145
+
146
+ def test_closes_fleet_and_logs_reason(self):
147
+ """
148
+ Test that _close_esi_fleet closes the fleet and logs the reason.
149
+
150
+ :return:
151
+ :rtype:
152
+ """
153
+
154
+ mock_fleet = MagicMock()
155
+
156
+ with patch("fleetfinder.tasks.logger.info") as mock_info:
157
+ _close_esi_fleet(fleet=mock_fleet, reason="Test reason")
158
+
159
+ mock_fleet.delete.assert_called_once()
160
+ mock_info.assert_called_once()
161
+ args, kwargs = mock_info.call_args
162
+
163
+ self.assertTrue(
164
+ any("Closing: Test reason" in str(x) for x in args + tuple(kwargs.values()))
165
+ )
166
+
167
+ def test_does_not_fail_with_empty_reason(self):
168
+ """
169
+ Test that _close_esi_fleet does not fail when given an empty reason.
170
+
171
+ :return:
172
+ :rtype:
173
+ """
174
+
175
+ mock_fleet = MagicMock()
176
+
177
+ with patch("fleetfinder.tasks.logger.info") as mock_info:
178
+ _close_esi_fleet(fleet=mock_fleet, reason="")
179
+
180
+ mock_fleet.delete.assert_called_once()
181
+ mock_info.assert_called_once()
182
+ args, kwargs = mock_info.call_args
183
+
184
+ self.assertTrue(
185
+ any("Closing: " in str(x) for x in args + tuple(kwargs.values()))
186
+ )
187
+
188
+
189
+ class TestHelperEsiFleetErrorHandling(BaseTestCase):
17
190
  """
18
- Tests for the _get_fleet_aggregate function.
191
+ Tests for the _esi_fleet_error_handling helper function.
192
+ """
193
+
194
+ def test_closes_fleet_when_error_count_exceeds_limit(self):
195
+ """
196
+ Test that _esi_fleet_error_handling closes the fleet when error count exceeds limit.
197
+
198
+ :return:
199
+ :rtype:
200
+ """
201
+
202
+ mock_fleet = MagicMock()
203
+ mock_fleet.last_esi_error = Fleet.EsiError.NO_FLEET
204
+ mock_fleet.last_esi_error_time = timezone.now() - timedelta(
205
+ seconds=ESI_ERROR_GRACE_TIME - 10
206
+ )
207
+ mock_fleet.esi_error_count = ESI_MAX_ERROR_COUNT
208
+
209
+ with patch("fleetfinder.tasks._close_esi_fleet") as mock_close_fleet:
210
+ _esi_fleet_error_handling(
211
+ fleet=mock_fleet, error_key=Fleet.EsiError.NO_FLEET
212
+ )
213
+
214
+ mock_close_fleet.assert_called_once_with(
215
+ fleet=mock_fleet, reason=Fleet.EsiError.NO_FLEET.label
216
+ )
217
+
218
+ def test_increments_error_count_for_same_error_within_grace_period(self):
219
+ """
220
+ Test that _esi_fleet_error_handling increments error count for the same error within grace period.
221
+
222
+ :return:
223
+ :rtype:
224
+ """
225
+
226
+ mock_fleet = MagicMock()
227
+ mock_fleet.last_esi_error = Fleet.EsiError.NO_FLEET
228
+ mock_fleet.last_esi_error_time = timezone.now() - timedelta(
229
+ seconds=ESI_ERROR_GRACE_TIME - 10
230
+ )
231
+ mock_fleet.esi_error_count = 1
232
+
233
+ _esi_fleet_error_handling(fleet=mock_fleet, error_key=Fleet.EsiError.NO_FLEET)
234
+
235
+ self.assertEqual(mock_fleet.esi_error_count, 2)
236
+ mock_fleet.save.assert_called_once()
237
+
238
+ def test_resets_error_count_for_new_error(self):
239
+ """
240
+ Test that _esi_fleet_error_handling resets error count for a new error.
241
+
242
+ :return:
243
+ :rtype:
244
+ """
245
+
246
+ mock_fleet = MagicMock()
247
+ mock_fleet.last_esi_error = Fleet.EsiError.NO_FLEET
248
+ mock_fleet.last_esi_error_time = timezone.now() - timedelta(
249
+ seconds=ESI_ERROR_GRACE_TIME - 10
250
+ )
251
+ mock_fleet.esi_error_count = 3
252
+
253
+ _esi_fleet_error_handling(
254
+ fleet=mock_fleet, error_key=Fleet.EsiError.NOT_IN_FLEET
255
+ )
256
+
257
+ self.assertEqual(mock_fleet.esi_error_count, 1)
258
+ self.assertEqual(mock_fleet.last_esi_error, Fleet.EsiError.NOT_IN_FLEET)
259
+ mock_fleet.save.assert_called_once()
260
+
261
+ def test_resets_error_count_for_same_error_outside_grace_period(self):
262
+ """
263
+ Test that _esi_fleet_error_handling resets error count for the same error outside grace period.
264
+
265
+ :return:
266
+ :rtype:
267
+ """
268
+
269
+ mock_fleet = MagicMock()
270
+ mock_fleet.last_esi_error = Fleet.EsiError.NO_FLEET
271
+ mock_fleet.last_esi_error_time = timezone.now() - timedelta(
272
+ seconds=ESI_ERROR_GRACE_TIME + 10
273
+ )
274
+ mock_fleet.esi_error_count = 3
275
+
276
+ _esi_fleet_error_handling(fleet=mock_fleet, error_key=Fleet.EsiError.NO_FLEET)
277
+
278
+ self.assertEqual(mock_fleet.esi_error_count, 1)
279
+ mock_fleet.save.assert_called_once()
280
+
281
+
282
+ class TestHelperGetFleetAggregate(BaseTestCase):
283
+ """
284
+ Tests for the _get_fleet_aggregate helper function.
19
285
  """
20
286
 
21
287
  def test_returns_correct_counts_for_valid_fleet_infos(self):
@@ -34,7 +300,7 @@ class TestGetFleetAggregate(TestCase):
34
300
 
35
301
  result = _get_fleet_aggregate(fleet_infos)
36
302
 
37
- assert result == {"Cruiser": 2, "Battleship": 1}
303
+ self.assertEqual(result, {"Cruiser": 2, "Battleship": 1})
38
304
 
39
305
  def test_returns_empty_dict_for_empty_fleet_infos(self):
40
306
  """
@@ -48,7 +314,7 @@ class TestGetFleetAggregate(TestCase):
48
314
 
49
315
  result = _get_fleet_aggregate(fleet_infos)
50
316
 
51
- assert result == {}
317
+ self.assertEqual(result, {})
52
318
 
53
319
  def test_returns_only_valid_ship_type_names(self):
54
320
  """
@@ -68,31 +334,381 @@ class TestGetFleetAggregate(TestCase):
68
334
 
69
335
  result = _get_fleet_aggregate(fleet_infos)
70
336
 
71
- assert result == {"Cruiser": 1, "Battleship": 1}
337
+ self.assertEqual(result, {"Cruiser": 1, "Battleship": 1})
338
+
339
+
340
+ class TestHelperCheckForEsiFleet(BaseTestCase):
341
+ """
342
+ Tests for the _check_for_esi_fleet helper function.
343
+ """
344
+
345
+ def test_returns_fleet_data_when_fleet_exists(self):
346
+ """
347
+ Test that _check_for_esi_fleet returns fleet data when fleet exists.
348
+
349
+ :return:
350
+ :rtype:
351
+ """
352
+
353
+ mock_fleet = MagicMock()
354
+ mock_token = MagicMock()
355
+ mock_esi_fleet = {"fleet_id": 12345}
356
+
357
+ # create a mock response object whose .result() returns the dict
358
+ mock_response = MagicMock()
359
+ mock_response.result.return_value = mock_esi_fleet
360
+
361
+ # ensure the client call returns the mock response
362
+ mock_get = MagicMock(return_value=mock_response)
363
+ mock_client = MagicMock()
364
+ mock_client.Fleets.GetCharactersCharacterIdFleet = mock_get
365
+
366
+ with patch("fleetfinder.tasks.Token.get_token", return_value=mock_token):
367
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_client)):
368
+ result = _check_for_esi_fleet(fleet=mock_fleet)
369
+
370
+ self.assertIsInstance(result, dict)
371
+ self.assertIn("fleet", result)
372
+ self.assertIn("token", result)
373
+ self.assertEqual(result["fleet"], mock_esi_fleet)
374
+ self.assertEqual(result["token"], mock_token)
375
+
376
+ def test_returns_false_when_content_type_error_occurs(self):
377
+ """
378
+ Test that _check_for_esi_fleet returns False when a ContentTypeError occurs.
379
+
380
+ :return:
381
+ :rtype:
382
+ """
383
+
384
+ mock_fleet = MagicMock()
385
+ mock_token = MagicMock()
386
+
387
+ # .result() raises a properly constructed ContentTypeError instance
388
+ mock_response = MagicMock()
389
+ mock_response.result.side_effect = ContentTypeError(
390
+ operation="GetCharactersCharacterIdFleet",
391
+ content_type="application/json",
392
+ message="Unexpected content type",
393
+ response=Mock(),
394
+ )
395
+
396
+ mock_client = MagicMock()
397
+ mock_client.Fleets.GetCharactersCharacterIdFleet.return_value = mock_response
398
+
399
+ with patch(
400
+ "fleetfinder.tasks.Token.get_token", return_value=mock_token
401
+ ) as mock_get_token:
402
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_client)):
403
+ result = _check_for_esi_fleet(fleet=mock_fleet)
404
+
405
+ self.assertIs(result, False)
406
+ mock_client.Fleets.GetCharactersCharacterIdFleet.assert_called_once()
407
+ mock_response.result.assert_called_once()
408
+ mock_get_token.assert_called_once()
409
+
410
+ def test_handles_http_client_error_for_fleet_not_found(self):
411
+ """
412
+ Test that _check_for_esi_fleet handles HTTPClientError for fleet not found.
413
+
414
+ :return:
415
+ :rtype:
416
+ """
417
+
418
+ mock_fleet = MagicMock()
419
+ mock_token = MagicMock()
420
+
421
+ # concrete response whose .result() raises an instance of HTTPClientError (404)
422
+ mock_response = MagicMock()
423
+ mock_response.result.side_effect = HTTPClientError(404, {}, b"")
424
+
425
+ mock_client = MagicMock()
426
+ mock_client.Fleets.GetCharactersCharacterIdFleet.return_value = mock_response
427
+
428
+ with patch("fleetfinder.tasks.Token.get_token", return_value=mock_token):
429
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_client)):
430
+ with patch(
431
+ "fleetfinder.tasks._esi_fleet_error_handling"
432
+ ) as mock_error_handling:
433
+ _check_for_esi_fleet(fleet=mock_fleet)
434
+
435
+ mock_client.Fleets.GetCharactersCharacterIdFleet.assert_called_once()
436
+ mock_response.result.assert_called_once()
437
+ mock_error_handling.assert_called_once_with(
438
+ error_key=Fleet.EsiError.NOT_IN_FLEET, fleet=mock_fleet
439
+ )
440
+
441
+ def test_handles_http_client_error_for_other_errors(self):
442
+ """
443
+ Test that _check_for_esi_fleet handles HTTPClientError for other errors.
444
+
445
+ :return:
446
+ :rtype:
447
+ """
448
+
449
+ mock_fleet = MagicMock()
450
+ mock_token = MagicMock()
451
+
452
+ # concrete response whose .result() raises an instance of HTTPClientError (403)
453
+ mock_response = MagicMock()
454
+ mock_response.result.side_effect = HTTPClientError(403, {}, b"")
455
+
456
+ mock_client = MagicMock()
457
+ mock_client.Fleets.GetCharactersCharacterIdFleet.return_value = mock_response
458
+
459
+ with patch("fleetfinder.tasks.Token.get_token", return_value=mock_token):
460
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_client)):
461
+ with patch(
462
+ "fleetfinder.tasks._esi_fleet_error_handling"
463
+ ) as mock_error_handling:
464
+ _check_for_esi_fleet(fleet=mock_fleet)
465
+
466
+ mock_client.Fleets.GetCharactersCharacterIdFleet.assert_called_once()
467
+ mock_response.result.assert_called_once()
468
+ mock_error_handling.assert_called_once_with(
469
+ error_key=Fleet.EsiError.NO_FLEET, fleet=mock_fleet
470
+ )
471
+
472
+ def handles_generic_exception(self):
473
+ mock_fleet = MagicMock()
474
+
475
+ mock_esi_client = MagicMock()
476
+ mock_esi_client.Fleets.GetCharactersCharacterIdFleet.return_value.result.side_effect = (
477
+ Exception
478
+ )
479
+
480
+ with patch("fleetfinder.tasks.Token.get_token"):
481
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_esi_client)):
482
+ with patch(
483
+ "fleetfinder.tasks._esi_fleet_error_handling"
484
+ ) as mock_error_handling:
485
+ _check_for_esi_fleet(fleet=mock_fleet)
486
+
487
+ mock_error_handling.assert_called_once_with(
488
+ error_key=Fleet.EsiError.NO_FLEET, fleet=mock_fleet
489
+ )
490
+
491
+
492
+ class TestHelperProcessFleet(BaseTestCase):
493
+ """
494
+ Tests for the _process_fleet helper function.
495
+ """
496
+
497
+ def test_processes_fleet_successfully(self):
498
+ """
499
+ Test that _process_fleet processes a fleet successfully.
500
+
501
+ :return:
502
+ :rtype:
503
+ """
504
+
505
+ mock_fleet = MagicMock()
506
+ mock_fleet.name = "Test Fleet"
507
+ mock_fleet.fleet_commander.character_id = 12345
508
+ mock_fleet.fleet_id = 67890
509
+
510
+ mock_esi_fleet = {"fleet": MagicMock(fleet_id=67890), "token": MagicMock()}
511
+
512
+ mock_esi_client = MagicMock()
513
+ mock_esi_client.Fleets.GetFleetsFleetIdMembers.return_value.result.return_value = (
514
+ []
515
+ )
516
+
517
+ with patch(
518
+ "fleetfinder.tasks._check_for_esi_fleet", return_value=mock_esi_fleet
519
+ ) as mock_check:
520
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_esi_client)):
521
+ _process_fleet(fleet=mock_fleet)
522
+
523
+ mock_check.assert_called_once_with(fleet=mock_fleet)
524
+ mock_esi_client.Fleets.GetFleetsFleetIdMembers.assert_called_once_with(
525
+ fleet_id=67890, token=mock_esi_fleet["token"]
526
+ )
527
+
528
+ def test_skips_processing_when_fleet_does_not_exist(self):
529
+ """
530
+ Test that _process_fleet skips processing when fleet does not exist.
531
+
532
+ :return:
533
+ :rtype:
534
+ """
535
+
536
+ mock_fleet = MagicMock()
537
+
538
+ with patch(
539
+ "fleetfinder.tasks._check_for_esi_fleet", return_value=False
540
+ ) as mock_check:
541
+ _process_fleet(fleet=mock_fleet)
542
+
543
+ mock_check.assert_called_once_with(fleet=mock_fleet)
544
+
545
+ def test_handles_fleet_commander_change(self):
546
+ """
547
+ Test that _process_fleet handles fleet commander change.
548
+
549
+ :return:
550
+ :rtype:
551
+ """
552
+
553
+ mock_fleet = MagicMock()
554
+ mock_fleet.name = "Test Fleet"
555
+ mock_fleet.fleet_commander.character_id = 12345
556
+ mock_fleet.fleet_id = 67890
557
+
558
+ mock_esi_fleet = {"fleet": MagicMock(fleet_id=11111), "token": MagicMock()}
559
+
560
+ with patch(
561
+ "fleetfinder.tasks._check_for_esi_fleet", return_value=mock_esi_fleet
562
+ ):
563
+ with patch(
564
+ "fleetfinder.tasks._esi_fleet_error_handling"
565
+ ) as mock_error_handling:
566
+ _process_fleet(fleet=mock_fleet)
567
+
568
+ mock_error_handling.assert_called_once_with(
569
+ fleet=mock_fleet, error_key=Fleet.EsiError.FC_CHANGED_FLEET
570
+ )
72
571
 
572
+ def test_handles_not_fleet_boss_error(self):
573
+ """
574
+ Test that _process_fleet handles not fleet boss error.
575
+
576
+ :return:
577
+ :rtype:
578
+ """
579
+
580
+ mock_fleet = MagicMock()
581
+ mock_fleet.name = "Test Fleet"
582
+ mock_fleet.fleet_commander.character_id = 12345
583
+ mock_fleet.fleet_id = 67890
584
+
585
+ mock_esi_fleet = {"fleet": MagicMock(fleet_id=67890), "token": MagicMock()}
73
586
 
74
- class TestCheckFleetAdvert(TestCase):
587
+ mock_esi_client = MagicMock()
588
+ mock_esi_client.Fleets.GetFleetsFleetIdMembers.return_value.result.side_effect = (
589
+ Exception()
590
+ )
591
+
592
+ with patch(
593
+ "fleetfinder.tasks._check_for_esi_fleet", return_value=mock_esi_fleet
594
+ ):
595
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_esi_client)):
596
+ with patch(
597
+ "fleetfinder.tasks._esi_fleet_error_handling"
598
+ ) as mock_error_handling:
599
+ _process_fleet(fleet=mock_fleet)
600
+
601
+ mock_error_handling.assert_called_once_with(
602
+ fleet=mock_fleet, error_key=Fleet.EsiError.NOT_FLEETBOSS
603
+ )
604
+
605
+
606
+ class TestSendFleetInvitation(BaseTestCase):
607
+ """
608
+ Tests for the send_fleet_invitation function.
609
+ """
610
+
611
+ def test_sends_invitations_to_all_characters_successfully(self):
612
+ """
613
+ Test that send_fleet_invitation sends invitations to all characters successfully.
614
+
615
+ :return:
616
+ :rtype:
617
+ """
618
+
619
+ mock_fleet = MagicMock()
620
+ mock_fleet.fleet_commander.character_id = 12345
621
+
622
+ mock_token = MagicMock()
623
+ mock_character_ids = [111, 222, 333]
624
+
625
+ with patch("fleetfinder.tasks.Fleet.objects.get", return_value=mock_fleet):
626
+ with patch("fleetfinder.tasks.Token.get_token", return_value=mock_token):
627
+ with patch(
628
+ "fleetfinder.tasks._send_invitation"
629
+ ) as mock_send_invitation:
630
+ send_fleet_invitation(
631
+ fleet_id=67890, character_ids=mock_character_ids
632
+ )
633
+
634
+ mock_send_invitation.assert_any_call(
635
+ character_id=111, fleet_commander_token=mock_token, fleet_id=67890
636
+ )
637
+ mock_send_invitation.assert_any_call(
638
+ character_id=222, fleet_commander_token=mock_token, fleet_id=67890
639
+ )
640
+ mock_send_invitation.assert_any_call(
641
+ character_id=333, fleet_commander_token=mock_token, fleet_id=67890
642
+ )
643
+ self.assertEqual(mock_send_invitation.call_count, 3)
644
+
645
+ def test_handles_empty_character_list_gracefully(self):
646
+ """
647
+ Test that send_fleet_invitation handles an empty character list gracefully.
648
+
649
+ :return:
650
+ :rtype:
651
+ """
652
+
653
+ mock_fleet = MagicMock()
654
+ mock_fleet.fleet_commander.character_id = 12345
655
+
656
+ with patch("fleetfinder.tasks.Fleet.objects.get", return_value=mock_fleet):
657
+ with patch("fleetfinder.tasks.Token.get_token"):
658
+ with patch(
659
+ "fleetfinder.tasks._send_invitation"
660
+ ) as mock_send_invitation:
661
+ send_fleet_invitation(fleet_id=67890, character_ids=[])
662
+
663
+ mock_send_invitation.assert_not_called()
664
+
665
+ def test_raises_exception_when_fleet_not_found(self):
666
+ """
667
+ Test that send_fleet_invitation raises an exception when the fleet is not found.
668
+
669
+ :return:
670
+ :rtype:
671
+ """
672
+
673
+ with patch(
674
+ "fleetfinder.tasks.Fleet.objects.get", side_effect=Fleet.DoesNotExist
675
+ ):
676
+ with self.assertRaises(Fleet.DoesNotExist):
677
+ send_fleet_invitation(fleet_id=67890, character_ids=[111, 222, 333])
678
+
679
+ def test_raises_exception_when_token_retrieval_fails(self):
680
+ """
681
+ Test that send_fleet_invitation raises an exception when token retrieval fails.
682
+
683
+ :return:
684
+ :rtype:
685
+ """
686
+
687
+ mock_fleet = MagicMock()
688
+ mock_fleet.fleet_commander.character_id = 12345
689
+
690
+ with patch("fleetfinder.tasks.Fleet.objects.get", return_value=mock_fleet):
691
+ with patch("fleetfinder.tasks.Token.get_token", side_effect=Exception):
692
+ with self.assertRaises(Exception):
693
+ send_fleet_invitation(fleet_id=67890, character_ids=[111, 222, 333])
694
+
695
+
696
+ class TestCheckFleetAdverts(BaseTestCase):
75
697
  """
76
698
  Tests for the check_fleet_adverts function.
77
699
  """
78
700
 
79
701
  @patch("fleetfinder.models.Fleet.objects.all")
80
- @patch("fleetfinder.tasks.fetch_esi_status")
81
- def test_processes_registered_fleets_when_available(
82
- self, mock_fetch_esi_status, mock_fleet_objects
83
- ):
702
+ def test_processes_registered_fleets_when_available(self, mock_fleet_objects):
84
703
  """
85
704
  Test that check_fleet_adverts processes registered fleets when ESI is available.
86
705
 
87
- :param mock_fetch_esi_status:
88
- :type mock_fetch_esi_status:
89
706
  :param mock_fleet_objects:
90
707
  :type mock_fleet_objects:
91
708
  :return:
92
709
  :rtype:
93
710
  """
94
711
 
95
- mock_fetch_esi_status.return_value.is_ok = True
96
712
  mock_fleet_objects.return_value.exists.return_value = True
97
713
  mock_fleet_objects.return_value.count.return_value = 2
98
714
  mock_fleet_objects.return_value.__iter__.return_value = iter([Mock(), Mock()])
@@ -100,41 +716,401 @@ class TestCheckFleetAdvert(TestCase):
100
716
  check_fleet_adverts()
101
717
 
102
718
  mock_fleet_objects.return_value.__iter__.assert_called_once()
103
- mock_fetch_esi_status.assert_called_once()
104
719
 
105
720
  @patch("fleetfinder.models.Fleet.objects.all")
106
- @patch("fleetfinder.tasks.fetch_esi_status")
107
- def test_logs_no_registered_fleets_when_none_exist(
108
- self, mock_fetch_esi_status, mock_fleet_objects
109
- ):
721
+ def test_logs_no_registered_fleets_when_none_exist(self, mock_fleet_objects):
110
722
  """
111
723
  Test that check_fleet_adverts logs a message when no registered fleets exist.
112
724
 
113
- :param mock_fetch_esi_status:
114
- :type mock_fetch_esi_status:
115
725
  :param mock_fleet_objects:
116
726
  :type mock_fleet_objects:
117
727
  :return:
118
728
  :rtype:
119
729
  """
120
730
 
121
- mock_fetch_esi_status.return_value.is_ok = True
122
731
  mock_fleet_objects.return_value.exists.return_value = False
123
732
 
124
733
  check_fleet_adverts()
125
734
 
126
735
  mock_fleet_objects.return_value.exists.assert_called_once()
127
- mock_fetch_esi_status.assert_not_called()
128
736
 
129
- @patch("fleetfinder.models.Fleet.objects.all")
130
- @patch("fleetfinder.tasks.fetch_esi_status")
131
- def test_aborts_processing_when_esi_is_unavailable(
132
- self, mock_fetch_esi_status, mock_fleet_objects
133
- ):
134
- mock_fetch_esi_status.return_value.is_ok = False
135
- mock_fleet_objects.return_value.exists.return_value = True
136
737
 
137
- check_fleet_adverts()
738
+ class TestHelperMakeNameLookup(BaseTestCase):
739
+ """
740
+ Tests for the _make_name_lookup helper function.
741
+ """
742
+
743
+ def test_creates_lookup_from_dicts(self):
744
+ """
745
+ Test that _make_name_lookup creates a lookup from a list of dictionaries.
746
+
747
+ :return:
748
+ :rtype:
749
+ """
750
+
751
+ input_data = [{"id": 1, "name": "Name1"}, {"id": 2, "name": "Name2"}]
752
+ expected_output = {1: "Name1", 2: "Name2"}
753
+
754
+ result = _make_name_lookup(input_data)
755
+
756
+ self.assertEqual(result, expected_output)
757
+
758
+ def test_creates_lookup_from_objects(self):
759
+ """
760
+ Test that _make_name_lookup creates a lookup from a list of objects.
761
+
762
+ :return:
763
+ :rtype:
764
+ """
765
+
766
+ mock_item1 = MagicMock()
767
+ mock_item1.id = 1
768
+ mock_item1.name = "Name1"
769
+ mock_item2 = MagicMock()
770
+ mock_item2.id = 2
771
+ mock_item2.name = "Name2"
772
+ input_data = [mock_item1, mock_item2]
773
+ expected_output = {1: "Name1", 2: "Name2"}
774
+
775
+ result = _make_name_lookup(input_data)
776
+
777
+ self.assertEqual(result, expected_output)
778
+
779
+ def test_handles_mixed_input_types(self):
780
+ """
781
+ Test that _make_name_lookup handles mixed input types (dicts and objects).
782
+
783
+ :return:
784
+ :rtype:
785
+ """
786
+
787
+ mock_item = MagicMock()
788
+ mock_item.id = 3
789
+ mock_item.name = "Name3"
790
+ input_data = [{"id": 1, "name": "Name1"}, mock_item, {"id": 2, "name": "Name2"}]
791
+ expected_output = {1: "Name1", 2: "Name2", 3: "Name3"}
792
+
793
+ result = _make_name_lookup(input_data)
794
+
795
+ self.assertEqual(result, expected_output)
796
+
797
+ def test_ignores_items_with_missing_id_or_name(self):
798
+ """
799
+ Test that _make_name_lookup ignores items with missing id or name.
800
+
801
+ :return:
802
+ :rtype:
803
+ """
804
+
805
+ input_data = [
806
+ {"id": 1, "name": "Name1"},
807
+ {"id": None, "name": "Name2"},
808
+ {"id": 2, "name": None},
809
+ {"name": "Name3"},
810
+ {"id": 3},
811
+ ]
812
+ expected_output = {1: "Name1"}
813
+
814
+ result = _make_name_lookup(input_data)
815
+
816
+ self.assertEqual(result, expected_output)
817
+
818
+ def test_returns_empty_dict_for_empty_input(self):
819
+ """
820
+ Test that _make_name_lookup returns an empty dictionary for empty input.
821
+
822
+ :return:
823
+ :rtype:
824
+ """
825
+
826
+ input_data = []
827
+ expected_output = {}
828
+
829
+ result = _make_name_lookup(input_data)
830
+
831
+ self.assertEqual(result, expected_output)
832
+
833
+ def test_skips_none_items_in_input(self):
834
+ """
835
+ Test that _make_name_lookup skips None items in the input list.
836
+
837
+ :return:
838
+ :rtype:
839
+ """
840
+
841
+ input_data = [{"id": 1, "name": "Name1"}, None, {"id": 2, "name": "Name2"}]
842
+ expected_output = {1: "Name1", 2: "Name2"}
843
+
844
+ result = _make_name_lookup(input_data)
845
+
846
+ self.assertEqual(result, expected_output)
847
+
848
+
849
+ class TestHelperFetchChunk(BaseTestCase):
850
+ """
851
+ Tests for the _fetch_chunk helper function.
852
+ """
853
+
854
+ def test_fetches_names_for_valid_ids(self):
855
+ """
856
+ Test that _fetch_chunk fetches names for valid IDs.
857
+
858
+ :return:
859
+ :rtype:
860
+ """
861
+
862
+ ids = [1, 2, 3]
863
+ mock_result = [
864
+ {"id": 1, "name": "Name1"},
865
+ {"id": 2, "name": "Name2"},
866
+ {"id": 3, "name": "Name3"},
867
+ ]
868
+
869
+ mock_client = MagicMock()
870
+ mock_client.Universe.PostUniverseNames.return_value.result.return_value = (
871
+ mock_result
872
+ )
873
+
874
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_client)):
875
+ result = _fetch_chunk(ids)
876
+
877
+ self.assertEqual(result, mock_result)
878
+ mock_client.Universe.PostUniverseNames.assert_called_once_with(body=ids)
879
+
880
+ def test_handles_single_id_failure_gracefully(self):
881
+ """
882
+ Test that _fetch_chunk handles single ID failure gracefully.
883
+
884
+ :return:
885
+ :rtype:
886
+ """
887
+
888
+ ids = [1]
889
+
890
+ mock_client = MagicMock()
891
+ mock_client.Universe.PostUniverseNames.return_value.result.side_effect = (
892
+ Exception()
893
+ )
894
+
895
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_client)):
896
+ result = _fetch_chunk(ids)
897
+
898
+ self.assertEqual(result, [])
899
+ mock_client.Universe.PostUniverseNames.assert_called_once_with(body=ids)
900
+
901
+ def test_retries_with_split_on_failure(self):
902
+ """
903
+ Test that _fetch_chunk retries with split on failure.
904
+
905
+ :return:
906
+ :rtype:
907
+ """
908
+
909
+ ids = [1, 2, 3, 4]
910
+ mock_result_1 = [{"id": 1, "name": "Name1"}, {"id": 2, "name": "Name2"}]
911
+ mock_result_2 = [{"id": 3, "name": "Name3"}, {"id": 4, "name": "Name4"}]
912
+
913
+ mock_client = MagicMock()
914
+ mock_client.Universe.PostUniverseNames.return_value.result.side_effect = [
915
+ Exception(),
916
+ mock_result_1,
917
+ mock_result_2,
918
+ ]
919
+
920
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_client)):
921
+ result = _fetch_chunk(ids)
922
+
923
+ self.assertEqual(result, mock_result_1 + mock_result_2)
924
+ self.assertEqual(mock_client.Universe.PostUniverseNames.call_count, 3)
925
+ mock_client.Universe.PostUniverseNames.assert_any_call(body=[1, 2])
926
+ mock_client.Universe.PostUniverseNames.assert_any_call(body=[3, 4])
927
+
928
+ def test_handles_empty_id_list(self):
929
+ """
930
+ Test that _fetch_chunk handles an empty ID list.
931
+
932
+ :return:
933
+ :rtype:
934
+ """
935
+
936
+ ids = []
937
+
938
+ mock_client = MagicMock()
939
+ mock_client.Universe.PostUniverseNames.return_value.result.return_value = []
940
+
941
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_client)):
942
+ result = _fetch_chunk(ids)
943
+
944
+ self.assertEqual(result, [])
945
+
946
+
947
+ class TestGetFleetComposition(BaseTestCase):
948
+ """
949
+ Tests for the get_fleet_composition function.
950
+ """
951
+
952
+ def test_retrieves_fleet_composition_successfully(self):
953
+ """
954
+ Test that get_fleet_composition retrieves fleet composition successfully.
955
+
956
+ :return:
957
+ :rtype:
958
+ """
959
+
960
+ mock_fleet = MagicMock()
961
+ mock_fleet.fleet_commander.character_id = 1
962
+ mock_fleet.fleet_commander.character_name = "Commander"
963
+ mock_fleet.name = "Test Fleet"
964
+ mock_fleet.fleet_id = 67890
965
+
966
+ mock_token = MagicMock()
967
+
968
+ def _make_member(character_id, solar_system_id, ship_type_id, takes_fleet_warp):
969
+ """
970
+ Helper to create a mock fleet member.
971
+
972
+ :param character_id:
973
+ :type character_id:
974
+ :param solar_system_id:
975
+ :type solar_system_id:
976
+ :param ship_type_id:
977
+ :type ship_type_id:
978
+ :param takes_fleet_warp:
979
+ :type takes_fleet_warp:
980
+ :return:
981
+ :rtype:
982
+ """
983
+
984
+ m = MagicMock()
985
+ m.character_id = character_id
986
+ m.solar_system_id = solar_system_id
987
+ m.ship_type_id = ship_type_id
988
+ m.takes_fleet_warp = takes_fleet_warp
989
+ m.dict.return_value = {
990
+ "character_id": character_id,
991
+ "solar_system_id": solar_system_id,
992
+ "ship_type_id": ship_type_id,
993
+ }
994
+ return m
995
+
996
+ mock_fleet_infos = [
997
+ _make_member(1, 101, 201, True),
998
+ _make_member(2, 102, 202, False),
999
+ ]
1000
+
1001
+ mock_name_lookup = {
1002
+ 1: "Character1",
1003
+ 2: "Character2",
1004
+ 101: "SolarSystem1",
1005
+ 102: "SolarSystem2",
1006
+ 201: "ShipType1",
1007
+ 202: "ShipType2",
1008
+ }
1009
+
1010
+ mock_esi_client = MagicMock()
1011
+ mock_esi_client.Fleets.GetFleetsFleetIdMembers.return_value.result.return_value = (
1012
+ mock_fleet_infos
1013
+ )
1014
+
1015
+ with patch("fleetfinder.tasks.Fleet.objects.get", return_value=mock_fleet):
1016
+ with patch("fleetfinder.tasks.Token.get_token", return_value=mock_token):
1017
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_esi_client)):
1018
+ with patch("fleetfinder.tasks._fetch_chunk") as mock_fetch_chunk:
1019
+ mock_fetch_chunk.side_effect = lambda ids: [
1020
+ {"id": id_, "name": mock_name_lookup[id_]} for id_ in ids
1021
+ ]
1022
+
1023
+ with patch(
1024
+ "fleetfinder.tasks._get_fleet_aggregate"
1025
+ ) as mock_aggregate:
1026
+ mock_aggregate.return_value = {
1027
+ "ShipType1": 1,
1028
+ "ShipType2": 1,
1029
+ }
1030
+
1031
+ result = get_fleet_composition(fleet_id=67890)
1032
+
1033
+ self.assertEqual(
1034
+ result.fleet,
1035
+ [
1036
+ {
1037
+ "character_id": 1,
1038
+ "solar_system_id": 101,
1039
+ "ship_type_id": 201,
1040
+ "takes_fleet_warp": True,
1041
+ "character_name": "Character1",
1042
+ "solar_system_name": "SolarSystem1",
1043
+ "ship_type_name": "ShipType1",
1044
+ "is_fleet_boss": True,
1045
+ },
1046
+ {
1047
+ "character_id": 2,
1048
+ "solar_system_id": 102,
1049
+ "ship_type_id": 202,
1050
+ "takes_fleet_warp": False,
1051
+ "character_name": "Character2",
1052
+ "solar_system_name": "SolarSystem2",
1053
+ "ship_type_name": "ShipType2",
1054
+ "is_fleet_boss": False,
1055
+ },
1056
+ ],
1057
+ )
1058
+ self.assertEqual(result.aggregate, {"ShipType1": 1, "ShipType2": 1})
1059
+
1060
+ def test_raises_exception_when_fleet_does_not_exist(self):
1061
+ """
1062
+ Test that get_fleet_composition raises an exception when the fleet does not exist.
1063
+
1064
+ :return:
1065
+ :rtype:
1066
+ """
1067
+
1068
+ with patch(
1069
+ "fleetfinder.tasks.Fleet.objects.get", side_effect=Fleet.DoesNotExist
1070
+ ):
1071
+ with self.assertRaises(Fleet.DoesNotExist):
1072
+ get_fleet_composition(fleet_id=67890)
1073
+
1074
+ def test_raises_runtime_error_on_esi_failure(self):
1075
+ """
1076
+ Test that get_fleet_composition raises a RuntimeError on ESI failure.
1077
+
1078
+ :return:
1079
+ :rtype:
1080
+ """
1081
+
1082
+ mock_fleet = MagicMock()
1083
+ mock_fleet.fleet_commander.character_id = 12345
1084
+ mock_fleet.fleet_id = 67890
1085
+
1086
+ with patch("fleetfinder.tasks.Fleet.objects.get", return_value=mock_fleet):
1087
+ with patch("fleetfinder.tasks.Token.get_token", side_effect=Exception):
1088
+ with self.assertRaises(RuntimeError):
1089
+ get_fleet_composition(fleet_id=67890)
1090
+
1091
+ def test_handles_handles_empty_fleet_info_gracefully(self):
1092
+ """
1093
+ Test that get_fleet_composition handles empty fleet info gracefully.
1094
+
1095
+ :return:
1096
+ :rtype:
1097
+ """
1098
+
1099
+ mock_fleet = MagicMock()
1100
+ mock_fleet.fleet_commander.character_id = 12345
1101
+ mock_fleet.fleet_id = 67890
1102
+
1103
+ mock_token = MagicMock()
1104
+
1105
+ mock_esi_client = MagicMock()
1106
+ mock_esi_client.Fleets.GetFleetsFleetIdMembers.return_value.result.return_value = (
1107
+ []
1108
+ )
1109
+
1110
+ with patch("fleetfinder.tasks.Fleet.objects.get", return_value=mock_fleet):
1111
+ with patch("fleetfinder.tasks.Token.get_token", return_value=mock_token):
1112
+ with patch("fleetfinder.tasks.esi", Mock(client=mock_esi_client)):
1113
+ result = get_fleet_composition(fleet_id=67890)
138
1114
 
139
- mock_fetch_esi_status.assert_called_once()
140
- mock_fleet_objects.return_value.__iter__.assert_not_called()
1115
+ self.assertEqual(result.fleet, [])
1116
+ self.assertEqual(result.aggregate, {})