firefighter-incident 0.0.12__py3-none-any.whl → 0.0.14__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.
Files changed (100) hide show
  1. firefighter/_version.py +16 -3
  2. firefighter/api/serializers.py +8 -8
  3. firefighter/api/urls.py +8 -1
  4. firefighter/api/views/_base.py +1 -1
  5. firefighter/api/views/components.py +5 -5
  6. firefighter/api/views/incidents.py +9 -9
  7. firefighter/firefighter/settings/components/raid.py +3 -0
  8. firefighter/incidents/admin.py +24 -24
  9. firefighter/incidents/factories.py +14 -5
  10. firefighter/incidents/forms/close_incident.py +4 -4
  11. firefighter/incidents/forms/create_incident.py +4 -4
  12. firefighter/incidents/forms/update_status.py +4 -4
  13. firefighter/incidents/menus.py +2 -2
  14. firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +7 -5
  15. firefighter/incidents/migrations/0009_update_sla.py +7 -5
  16. firefighter/incidents/migrations/0019_set_security_components_private.py +67 -0
  17. firefighter/incidents/migrations/0020_create_incident_category_model.py +64 -0
  18. firefighter/incidents/migrations/0021_copy_component_data_to_incident_category.py +57 -0
  19. firefighter/incidents/migrations/0022_add_incident_category_fields.py +34 -0
  20. firefighter/incidents/migrations/0023_populate_incident_category_references.py +57 -0
  21. firefighter/incidents/migrations/0024_remove_component_fields_and_model.py +26 -0
  22. firefighter/incidents/migrations/0025_make_incident_category_required.py +24 -0
  23. firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py +39 -0
  24. firefighter/incidents/models/__init__.py +1 -1
  25. firefighter/incidents/models/group.py +1 -1
  26. firefighter/incidents/models/incident.py +15 -15
  27. firefighter/incidents/models/{component.py → incident_category.py} +30 -29
  28. firefighter/incidents/models/incident_update.py +3 -3
  29. firefighter/incidents/tables.py +9 -9
  30. firefighter/incidents/templates/layouts/partials/incident_card.html +1 -1
  31. firefighter/incidents/templates/layouts/partials/incident_timeline.html +2 -2
  32. firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html} +13 -13
  33. firefighter/incidents/templates/pages/{component_list.html → incident_category_list.html} +2 -2
  34. firefighter/incidents/templates/pages/incident_detail.html +3 -3
  35. firefighter/incidents/urls.py +6 -6
  36. firefighter/incidents/views/components/details.py +9 -9
  37. firefighter/incidents/views/components/list.py +9 -9
  38. firefighter/incidents/views/reports.py +2 -2
  39. firefighter/incidents/views/users/details.py +2 -2
  40. firefighter/incidents/views/views.py +7 -7
  41. firefighter/jira_app/client.py +1 -1
  42. firefighter/logging/custom_json_formatter.py +2 -1
  43. firefighter/pagerduty/tasks/trigger_oncall.py +1 -1
  44. firefighter/raid/admin.py +0 -11
  45. firefighter/raid/client.py +3 -3
  46. firefighter/raid/forms.py +53 -19
  47. firefighter/raid/migrations/0003_delete_raidarea.py +16 -0
  48. firefighter/raid/models.py +2 -21
  49. firefighter/raid/serializers.py +5 -4
  50. firefighter/raid/service.py +29 -27
  51. firefighter/raid/signals/incident_created.py +4 -2
  52. firefighter/raid/utils.py +1 -1
  53. firefighter/raid/views/__init__.py +1 -1
  54. firefighter/raid/views/open_normal.py +2 -2
  55. firefighter/slack/admin.py +8 -8
  56. firefighter/slack/management/commands/switch_test_users.py +272 -0
  57. firefighter/slack/messages/slack_messages.py +5 -5
  58. firefighter/slack/migrations/0005_add_incident_categories_fields.py +33 -0
  59. firefighter/slack/migrations/0006_copy_components_to_incident_categories.py +57 -0
  60. firefighter/slack/migrations/0007_remove_components_fields.py +22 -0
  61. firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py +33 -0
  62. firefighter/slack/models/conversation.py +3 -3
  63. firefighter/slack/models/incident_channel.py +1 -1
  64. firefighter/slack/models/user.py +1 -1
  65. firefighter/slack/models/user_group.py +3 -3
  66. firefighter/slack/rules.py +1 -1
  67. firefighter/slack/signals/get_users.py +2 -2
  68. firefighter/slack/signals/incident_updated.py +1 -1
  69. firefighter/slack/utils.py +2 -2
  70. firefighter/slack/views/events/home.py +2 -2
  71. firefighter/slack/views/modals/base_modal/form_utils.py +15 -0
  72. firefighter/slack/views/modals/close.py +3 -3
  73. firefighter/slack/views/modals/open.py +25 -1
  74. firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
  75. firefighter/slack/views/modals/opening/details/critical.py +1 -1
  76. firefighter/slack/views/modals/opening/select_impact.py +5 -2
  77. firefighter/slack/views/modals/update_status.py +4 -4
  78. firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
  79. {firefighter_incident-0.0.12.dist-info → firefighter_incident-0.0.14.dist-info}/METADATA +2 -2
  80. {firefighter_incident-0.0.12.dist-info → firefighter_incident-0.0.14.dist-info}/RECORD +99 -77
  81. firefighter_tests/conftest.py +4 -5
  82. firefighter_tests/test_api/test_api_landbot.py +1 -1
  83. firefighter_tests/test_firefighter/test_sso.py +146 -0
  84. firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
  85. firefighter_tests/test_incidents/test_incident_urls.py +3 -3
  86. firefighter_tests/test_incidents/test_models/test_incident_category.py +165 -0
  87. firefighter_tests/test_incidents/test_models/test_incident_model.py +2 -2
  88. firefighter_tests/test_raid/test_priority_mapping.py +267 -0
  89. firefighter_tests/test_raid/test_raid_client.py +580 -0
  90. firefighter_tests/test_raid/test_raid_forms.py +795 -0
  91. firefighter_tests/test_raid/test_raid_models.py +185 -0
  92. firefighter_tests/test_raid/test_raid_serializers.py +507 -0
  93. firefighter_tests/test_raid/test_raid_service.py +442 -0
  94. firefighter_tests/test_raid/test_raid_views.py +196 -0
  95. firefighter_tests/test_slack/views/modals/test_close.py +6 -6
  96. firefighter_tests/test_slack/views/modals/test_update_status.py +4 -4
  97. firefighter_fixtures/raid/area.json +0 -1
  98. {firefighter_incident-0.0.12.dist-info → firefighter_incident-0.0.14.dist-info}/WHEEL +0 -0
  99. {firefighter_incident-0.0.12.dist-info → firefighter_incident-0.0.14.dist-info}/entry_points.txt +0 -0
  100. {firefighter_incident-0.0.12.dist-info → firefighter_incident-0.0.14.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,795 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import ANY, Mock, patch
4
+
5
+ import pytest
6
+ from django.test import TestCase
7
+ from slack_sdk.errors import SlackApiError
8
+
9
+ from firefighter.incidents.factories import (
10
+ IncidentCategoryFactory,
11
+ IncidentFactory,
12
+ PriorityFactory,
13
+ UserFactory,
14
+ )
15
+ from firefighter.incidents.models.priority import Priority
16
+ from firefighter.jira_app.client import JiraAPIError, JiraUserNotFoundError
17
+ from firefighter.jira_app.models import JiraUser
18
+ from firefighter.raid.forms import (
19
+ CreateNormalCustomerIncidentForm,
20
+ CreateRaidDocumentationRequestIncidentForm,
21
+ CreateRaidFeatureRequestIncidentForm,
22
+ CreateRaidInternalIncidentForm,
23
+ PlatformChoices,
24
+ RaidCreateIncidentSellerForm,
25
+ alert_slack_comment_ticket,
26
+ alert_slack_new_jira_ticket,
27
+ alert_slack_update_ticket,
28
+ get_business_impact,
29
+ get_internal_alert_conversations,
30
+ get_partner_alert_conversations,
31
+ initial_priority,
32
+ process_jira_issue,
33
+ send_message_to_watchers,
34
+ set_jira_ticket_watchers_raid,
35
+ )
36
+ from firefighter.raid.models import FeatureTeam, JiraTicket
37
+ from firefighter.slack.models.conversation import Conversation
38
+ from firefighter.slack.models.user import SlackUser
39
+
40
+
41
+ class TestPlatformChoices(TestCase):
42
+ """Test PlatformChoices enum."""
43
+
44
+ def test_platform_choices_values(self):
45
+ """Test that platform choices have correct values."""
46
+ assert PlatformChoices.FR == "platform-FR"
47
+ assert PlatformChoices.DE == "platform-DE"
48
+ assert PlatformChoices.IT == "platform-IT"
49
+ assert PlatformChoices.ES == "platform-ES"
50
+ assert PlatformChoices.UK == "platform-UK"
51
+ assert PlatformChoices.ALL == "platform-All"
52
+ assert PlatformChoices.INTERNAL == "platform-Internal"
53
+
54
+
55
+ @pytest.mark.django_db
56
+ class TestInitialPriority:
57
+ """Test initial_priority function."""
58
+
59
+ def test_initial_priority_returns_default(self):
60
+ """Test that initial_priority returns default priority."""
61
+ # Given
62
+ Priority.objects.all().delete() # Clean slate
63
+ default_priority = PriorityFactory(default=True, value=100)
64
+ PriorityFactory(default=False, value=101) # Create non-default priority
65
+
66
+ # When
67
+ result = initial_priority()
68
+
69
+ # Then
70
+ assert result == default_priority
71
+
72
+
73
+ @pytest.mark.django_db
74
+ class TestCreateNormalCustomerIncidentForm:
75
+ """Test CreateNormalCustomerIncidentForm functionality."""
76
+
77
+ def setup_method(self):
78
+ """Set up test data."""
79
+ Priority.objects.all().delete() # Clear existing priorities
80
+ self.user = UserFactory()
81
+ self.jira_user = JiraUser.objects.create(id="jira-123", user=self.user)
82
+ self.priority = PriorityFactory(value=1)
83
+ self.incident_category = IncidentCategoryFactory()
84
+ self.feature_team = FeatureTeam.objects.create(
85
+ name="Test Team", jira_project_key="TEST"
86
+ )
87
+
88
+ @patch("firefighter.raid.forms.process_jira_issue")
89
+ @patch("firefighter.raid.forms.create_issue_customer")
90
+ @patch("firefighter.raid.forms.get_jira_user_from_user")
91
+ @patch("firefighter.raid.forms.get_business_impact")
92
+ def test_trigger_incident_workflow(
93
+ self, mock_get_business_impact, mock_get_jira_user, mock_create_issue, mock_process_jira_issue
94
+ ):
95
+ """Test trigger_incident_workflow method."""
96
+ # Given
97
+ mock_get_jira_user.return_value = self.jira_user
98
+ mock_create_issue.return_value = {"id": "10001", "key": "TEST-123"}
99
+ mock_get_business_impact.return_value = "High"
100
+ mock_process_jira_issue.return_value = None
101
+
102
+ form_data = {
103
+ "incident_category": self.incident_category,
104
+ "platform": "platform-FR",
105
+ "title": "Test incident title",
106
+ "description": "Test incident description",
107
+ "priority": self.priority,
108
+ "suggested_team_routing": self.feature_team,
109
+ "zendesk_ticket_id": "12345",
110
+ }
111
+
112
+ form = CreateNormalCustomerIncidentForm()
113
+ form.cleaned_data = form_data
114
+
115
+ # When
116
+ form.trigger_incident_workflow(self.user, {})
117
+
118
+ # Then
119
+ mock_get_jira_user.assert_called_once_with(self.user)
120
+ mock_create_issue.assert_called_once()
121
+ mock_process_jira_issue.assert_called_once()
122
+
123
+
124
+ @pytest.mark.django_db
125
+ class TestCreateRaidDocumentationRequestIncidentForm:
126
+ """Test CreateRaidDocumentationRequestIncidentForm functionality."""
127
+
128
+ def setup_method(self):
129
+ """Set up test data."""
130
+ Priority.objects.all().delete() # Clear existing priorities
131
+ self.user = UserFactory()
132
+ self.jira_user = JiraUser.objects.create(id="jira-doc-123", user=self.user)
133
+ self.priority = PriorityFactory(value=20)
134
+ self.incident_category = IncidentCategoryFactory()
135
+ self.feature_team = FeatureTeam.objects.create(
136
+ name="Documentation Team", jira_project_key="DOC"
137
+ )
138
+
139
+ @patch("firefighter.raid.forms.process_jira_issue")
140
+ @patch("firefighter.raid.forms.create_issue_documentation_request")
141
+ @patch("firefighter.raid.forms.get_jira_user_from_user")
142
+ def test_trigger_incident_workflow(
143
+ self, mock_get_jira_user, mock_create_issue, mock_process_jira_issue
144
+ ):
145
+ """Test trigger_incident_workflow method."""
146
+ # Given
147
+ mock_get_jira_user.return_value = self.jira_user
148
+ mock_create_issue.return_value = {"id": "10002", "key": "TEST-124"}
149
+ mock_process_jira_issue.return_value = None
150
+
151
+ form_data = {
152
+ "incident_category": self.incident_category,
153
+ "platform": "platform-DE",
154
+ "title": "Documentation request title",
155
+ "description": "Documentation request description",
156
+ "priority": self.priority,
157
+ "suggested_team_routing": self.feature_team,
158
+ }
159
+
160
+ form = CreateRaidDocumentationRequestIncidentForm()
161
+ form.cleaned_data = form_data
162
+
163
+ # When
164
+ form.trigger_incident_workflow(self.user, {})
165
+
166
+ # Then
167
+ mock_get_jira_user.assert_called_once_with(self.user)
168
+ mock_create_issue.assert_called_once()
169
+ mock_process_jira_issue.assert_called_once()
170
+
171
+
172
+ @pytest.mark.django_db
173
+ class TestCreateRaidFeatureRequestIncidentForm:
174
+ """Test CreateRaidFeatureRequestIncidentForm functionality."""
175
+
176
+ def setup_method(self):
177
+ """Set up test data."""
178
+ self.user = UserFactory()
179
+ self.jira_user = JiraUser.objects.create(id="jira-feat-123", user=self.user)
180
+ self.priority = PriorityFactory(value=30)
181
+ self.incident_category = IncidentCategoryFactory()
182
+ self.feature_team = FeatureTeam.objects.create(
183
+ name="Feature Team", jira_project_key="FEAT"
184
+ )
185
+
186
+ @patch("firefighter.raid.forms.process_jira_issue")
187
+ @patch("firefighter.raid.forms.create_issue_feature_request")
188
+ @patch("firefighter.raid.forms.get_jira_user_from_user")
189
+ def test_trigger_incident_workflow(
190
+ self, mock_get_jira_user, mock_create_issue, mock_process_jira_issue
191
+ ):
192
+ """Test trigger_incident_workflow method."""
193
+ # Given
194
+ mock_get_jira_user.return_value = self.jira_user
195
+ mock_create_issue.return_value = {"id": "10003", "key": "TEST-125"}
196
+ mock_process_jira_issue.return_value = None
197
+
198
+ form_data = {
199
+ "incident_category": self.incident_category,
200
+ "platform": "platform-IT",
201
+ "title": "Feature request title",
202
+ "description": "Feature request description",
203
+ "priority": self.priority,
204
+ "suggested_team_routing": self.feature_team,
205
+ }
206
+
207
+ form = CreateRaidFeatureRequestIncidentForm()
208
+ form.cleaned_data = form_data
209
+
210
+ # When
211
+ form.trigger_incident_workflow(self.user, {})
212
+
213
+ # Then
214
+ mock_get_jira_user.assert_called_once_with(self.user)
215
+ mock_create_issue.assert_called_once()
216
+ mock_process_jira_issue.assert_called_once()
217
+
218
+
219
+ @pytest.mark.django_db
220
+ class TestCreateRaidInternalIncidentForm:
221
+ """Test CreateRaidInternalIncidentForm functionality."""
222
+
223
+ def setup_method(self):
224
+ """Set up test data."""
225
+ self.user = UserFactory()
226
+ self.jira_user = JiraUser.objects.create(id="jira-int-123", user=self.user)
227
+ self.priority = PriorityFactory(value=40)
228
+ self.incident_category = IncidentCategoryFactory()
229
+ self.feature_team = FeatureTeam.objects.create(
230
+ name="Internal Team", jira_project_key="INT"
231
+ )
232
+
233
+ @patch("firefighter.raid.forms.process_jira_issue")
234
+ @patch("firefighter.raid.forms.create_issue_internal")
235
+ @patch("firefighter.raid.forms.get_jira_user_from_user")
236
+ @patch("firefighter.raid.forms.get_business_impact")
237
+ def test_trigger_incident_workflow(
238
+ self, mock_get_business_impact, mock_get_jira_user, mock_create_issue, mock_process_jira_issue
239
+ ):
240
+ """Test trigger_incident_workflow method."""
241
+ # Given
242
+ mock_get_jira_user.return_value = self.jira_user
243
+ mock_create_issue.return_value = {"id": "10004", "key": "TEST-126"}
244
+ mock_get_business_impact.return_value = "Medium"
245
+ mock_process_jira_issue.return_value = None
246
+
247
+ form_data = {
248
+ "incident_category": self.incident_category,
249
+ "platform": "platform-ES",
250
+ "title": "Internal incident title",
251
+ "description": "Internal incident description",
252
+ "priority": self.priority,
253
+ "suggested_team_routing": self.feature_team,
254
+ }
255
+
256
+ form = CreateRaidInternalIncidentForm()
257
+ form.cleaned_data = form_data
258
+
259
+ # When
260
+ form.trigger_incident_workflow(self.user, {})
261
+
262
+ # Then
263
+ mock_get_jira_user.assert_called_once_with(self.user)
264
+ mock_create_issue.assert_called_once()
265
+ mock_process_jira_issue.assert_called_once()
266
+
267
+
268
+ @pytest.mark.django_db
269
+ class TestRaidCreateIncidentSellerForm:
270
+ """Test RaidCreateIncidentSellerForm functionality."""
271
+
272
+ def setup_method(self):
273
+ """Set up test data."""
274
+ self.user = UserFactory()
275
+ self.jira_user = JiraUser.objects.create(id="jira-sell-123", user=self.user)
276
+ self.priority = PriorityFactory(value=50)
277
+ self.incident_category = IncidentCategoryFactory()
278
+ self.feature_team = FeatureTeam.objects.create(
279
+ name="Seller Team", jira_project_key="SELL"
280
+ )
281
+
282
+ @patch("firefighter.raid.forms.process_jira_issue")
283
+ @patch("firefighter.raid.forms.create_issue_seller")
284
+ @patch("firefighter.raid.forms.get_jira_user_from_user")
285
+ @patch("firefighter.raid.forms.get_business_impact")
286
+ def test_trigger_incident_workflow(
287
+ self, mock_get_business_impact, mock_get_jira_user, mock_create_issue, mock_process_jira_issue
288
+ ):
289
+ """Test trigger_incident_workflow method."""
290
+ # Given
291
+ mock_get_jira_user.return_value = self.jira_user
292
+ mock_create_issue.return_value = {"id": "10005", "key": "TEST-127"}
293
+ mock_get_business_impact.return_value = "Low"
294
+ mock_process_jira_issue.return_value = None
295
+
296
+ form_data = {
297
+ "incident_category": self.incident_category,
298
+ "platform": "platform-UK",
299
+ "title": "Seller incident title",
300
+ "description": "Seller incident description",
301
+ "priority": self.priority,
302
+ "suggested_team_routing": self.feature_team,
303
+ "seller_contract_id": "SELLER123",
304
+ "is_key_account": True,
305
+ "is_seller_in_golden_list": False,
306
+ "zoho_desk_ticket_id": "ZOHO456",
307
+ }
308
+
309
+ form = RaidCreateIncidentSellerForm()
310
+ form.cleaned_data = form_data
311
+
312
+ # When
313
+ form.trigger_incident_workflow(self.user, {})
314
+
315
+ # Then
316
+ mock_get_jira_user.assert_called_once_with(self.user)
317
+ mock_create_issue.assert_called_once()
318
+ mock_process_jira_issue.assert_called_once()
319
+
320
+
321
+ @pytest.mark.django_db
322
+ class TestProcessJiraIssue:
323
+ """Test process_jira_issue function."""
324
+
325
+ def setup_method(self):
326
+ """Set up test data."""
327
+ self.user = UserFactory()
328
+ self.jira_user = JiraUser.objects.create(id="jira-123", user=self.user)
329
+
330
+ @patch("firefighter.raid.forms.alert_slack_new_jira_ticket")
331
+ @patch("firefighter.raid.forms.set_jira_ticket_watchers_raid")
332
+ @patch("firefighter.raid.forms.SelectImpactForm")
333
+ def test_process_jira_issue(self, mock_impact_form, mock_set_watchers, mock_alert_slack):
334
+ """Test process_jira_issue function."""
335
+ # Given
336
+ issue_data = {
337
+ "id": "10001",
338
+ "key": "TEST-123",
339
+ "summary": "Test issue",
340
+ "reporter": self.jira_user,
341
+ }
342
+ impacts_data = {"business_impact": "High"}
343
+
344
+ mock_form_instance = Mock()
345
+ mock_impact_form.return_value = mock_form_instance
346
+ mock_set_watchers.return_value = None
347
+ mock_alert_slack.return_value = None
348
+
349
+ # When
350
+ process_jira_issue(issue_data, self.user, self.jira_user, impacts_data)
351
+
352
+ # Then
353
+ # Check that JiraTicket was created
354
+ assert JiraTicket.objects.filter(key="TEST-123").exists()
355
+
356
+ # Check that all functions were called
357
+ mock_impact_form.assert_called_once_with(impacts_data)
358
+ mock_form_instance.save.assert_called_once_with(incident=ANY)
359
+ mock_set_watchers.assert_called_once_with(ANY)
360
+ mock_alert_slack.assert_called_once_with(ANY)
361
+
362
+
363
+ @pytest.mark.django_db
364
+ class TestSetJiraTicketWatchersRaid:
365
+ """Test set_jira_ticket_watchers_raid function."""
366
+
367
+ def setup_method(self):
368
+ """Set up test data."""
369
+ self.user = UserFactory()
370
+ self.jira_user = JiraUser.objects.create(id="jira-123", user=self.user)
371
+ self.jira_ticket = JiraTicket.objects.create(
372
+ id=10001,
373
+ key="TEST-123",
374
+ summary="Test ticket",
375
+ reporter=self.jira_user,
376
+ )
377
+
378
+ @patch("firefighter.raid.forms.jira_client")
379
+ def test_set_jira_ticket_watchers_success(self, mock_jira_client):
380
+ """Test successful watcher addition."""
381
+ # Given
382
+ mock_default_user = Mock()
383
+ mock_jira_client.get_jira_user_from_jira_id.return_value = mock_default_user
384
+ mock_jira_client.jira.add_watcher.return_value = None
385
+
386
+ # When
387
+ set_jira_ticket_watchers_raid(self.jira_ticket)
388
+
389
+ # Then
390
+ mock_jira_client.jira.add_watcher.assert_called_once_with(
391
+ issue=10001, watcher="jira-123"
392
+ )
393
+
394
+ @patch("firefighter.raid.forms.jira_client")
395
+ def test_set_jira_ticket_watchers_jira_user_not_found(self, mock_jira_client):
396
+ """Test when default JIRA user is not found."""
397
+ # Given
398
+ mock_jira_client.get_jira_user_from_jira_id.side_effect = JiraUserNotFoundError("Not found")
399
+
400
+ # When
401
+ set_jira_ticket_watchers_raid(self.jira_ticket)
402
+
403
+ # Then
404
+ # Should handle the exception and continue
405
+
406
+ @patch("firefighter.raid.forms.jira_client")
407
+ def test_set_jira_ticket_watchers_add_watcher_error(self, mock_jira_client):
408
+ """Test when adding watcher fails."""
409
+ # Given
410
+ mock_default_user = Mock()
411
+ mock_jira_client.get_jira_user_from_jira_id.return_value = mock_default_user
412
+ mock_jira_client.jira.add_watcher.side_effect = JiraAPIError("API Error")
413
+ mock_jira_client.jira.remove_watcher.side_effect = JiraAPIError("Remove Error")
414
+
415
+ # When
416
+ set_jira_ticket_watchers_raid(self.jira_ticket)
417
+
418
+ # Then
419
+ # Should handle both exceptions
420
+
421
+
422
+ @pytest.mark.django_db
423
+ class TestAlertSlackNewJiraTicket:
424
+ """Test alert_slack_new_jira_ticket function."""
425
+
426
+ def setup_method(self):
427
+ """Set up test data."""
428
+ self.user = UserFactory(email="test@example.com")
429
+ self.slack_user = SlackUser.objects.create(
430
+ user=self.user, slack_id="U123456"
431
+ )
432
+ self.jira_user = JiraUser.objects.create(id="jira-123", user=self.user)
433
+ self.jira_ticket = JiraTicket.objects.create(
434
+ id=10001,
435
+ key="TEST-123",
436
+ summary="Test ticket",
437
+ reporter=self.jira_user,
438
+ )
439
+
440
+ def test_alert_slack_new_jira_ticket_with_incident_raises_error(self):
441
+ """Test that function raises ValueError for critical incidents."""
442
+ # Given - Create an incident and link it to the ticket
443
+ incident = IncidentFactory()
444
+ self.jira_ticket.incident = incident
445
+ self.jira_ticket.save()
446
+
447
+ # When & Then
448
+ with pytest.raises(ValueError, match="This is a critical incident"):
449
+ alert_slack_new_jira_ticket(self.jira_ticket)
450
+
451
+ @patch("firefighter.raid.forms.get_partner_alert_conversations")
452
+ @patch("firefighter.raid.forms.get_internal_alert_conversations")
453
+ @patch("firefighter.raid.forms.SlackMessageRaidCreatedIssue")
454
+ def test_alert_slack_new_jira_ticket_no_reporter_user(
455
+ self, mock_message_class, mock_get_internal, mock_get_partner
456
+ ):
457
+ """Test when reporter user is None."""
458
+ # Given
459
+ mock_get_internal.return_value = Conversation.objects.none()
460
+ mock_get_partner.return_value = Conversation.objects.none()
461
+
462
+ # Mock message class to return proper strings instead of MagicMock
463
+ mock_message_instance = Mock()
464
+ mock_message_instance.get_text.return_value = "Test message"
465
+ mock_message_instance.get_blocks.return_value = []
466
+ mock_message_instance.get_metadata.return_value = {}
467
+ mock_message_class.return_value = mock_message_instance
468
+
469
+ # When
470
+ alert_slack_new_jira_ticket(self.jira_ticket, reporter_user=None)
471
+
472
+ # Then
473
+ # Should log warning and return early
474
+
475
+ @patch("firefighter.raid.forms.get_partner_alert_conversations")
476
+ @patch("firefighter.raid.forms.get_internal_alert_conversations")
477
+ @patch("firefighter.raid.forms.SlackMessageRaidCreatedIssue")
478
+ def test_alert_slack_new_jira_ticket_with_slack_user(
479
+ self, mock_message_class, mock_get_internal, mock_get_partner
480
+ ):
481
+ """Test with valid Slack user."""
482
+ # Given
483
+ mock_message = Mock()
484
+ mock_message_class.return_value = mock_message
485
+ mock_get_internal.return_value = Conversation.objects.none()
486
+ mock_get_partner.return_value = Conversation.objects.none()
487
+
488
+ # When
489
+ with patch.object(self.slack_user, "send_private_message") as mock_send:
490
+ alert_slack_new_jira_ticket(self.jira_ticket, reporter_user=self.user)
491
+
492
+ # Then
493
+ mock_send.assert_called_once_with(
494
+ mock_message, unfurl_links=False
495
+ )
496
+
497
+ @patch("firefighter.raid.forms.get_partner_alert_conversations")
498
+ @patch("firefighter.raid.forms.get_internal_alert_conversations")
499
+ @patch("firefighter.raid.forms.SlackMessageRaidCreatedIssue")
500
+ def test_alert_slack_new_jira_ticket_messages_disabled(
501
+ self, mock_message_class, mock_get_internal, mock_get_partner
502
+ ):
503
+ """Test when user has disabled private messages."""
504
+ # Given
505
+ mock_message = Mock()
506
+ mock_message_class.return_value = mock_message
507
+ mock_get_internal.return_value = Conversation.objects.none()
508
+ mock_get_partner.return_value = Conversation.objects.none()
509
+
510
+ slack_error = SlackApiError("Error", response={"error": "messages_tab_disabled"})
511
+
512
+ # When
513
+ with patch.object(self.slack_user, "send_private_message", side_effect=slack_error):
514
+ alert_slack_new_jira_ticket(self.jira_ticket, reporter_user=self.user)
515
+
516
+ # Then
517
+ # Should log warning about disabled messages
518
+
519
+
520
+ @pytest.mark.django_db
521
+ class TestAlertSlackUpdateTicket:
522
+ """Test alert_slack_update_ticket function."""
523
+
524
+ @patch("firefighter.raid.forms.send_message_to_watchers")
525
+ @patch("firefighter.raid.forms.SlackMessageRaidModifiedIssue")
526
+ def test_alert_slack_update_ticket(self, mock_message_class, mock_send_message):
527
+ """Test alert_slack_update_ticket function."""
528
+ # Given
529
+ mock_message = Mock()
530
+ mock_message_class.return_value = mock_message
531
+ mock_send_message.return_value = True
532
+
533
+ # When
534
+ result = alert_slack_update_ticket(
535
+ jira_ticket_id=10001,
536
+ jira_ticket_key="TEST-123",
537
+ jira_author_name="John Doe",
538
+ jira_field_modified="Priority",
539
+ jira_field_from="High",
540
+ jira_field_to="Critical"
541
+ )
542
+
543
+ # Then
544
+ assert result is True
545
+ mock_message_class.assert_called_once()
546
+ mock_send_message.assert_called_once_with(jira_issue_id=10001, message=mock_message)
547
+
548
+
549
+ @pytest.mark.django_db
550
+ class TestAlertSlackCommentTicket:
551
+ """Test alert_slack_comment_ticket function."""
552
+
553
+ @patch("firefighter.raid.forms.send_message_to_watchers")
554
+ @patch("firefighter.raid.forms.SlackMessageRaidComment")
555
+ def test_alert_slack_comment_ticket(self, mock_message_class, mock_send_message):
556
+ """Test alert_slack_comment_ticket function."""
557
+ # Given
558
+ mock_message = Mock()
559
+ mock_message_class.return_value = mock_message
560
+ mock_send_message.return_value = True
561
+
562
+ # When
563
+ result = alert_slack_comment_ticket(
564
+ webhook_event="comment_created",
565
+ jira_ticket_id=10001,
566
+ jira_ticket_key="TEST-123",
567
+ author_jira_name="Jane Smith",
568
+ comment="This is a test comment"
569
+ )
570
+
571
+ # Then
572
+ assert result is True
573
+ mock_message_class.assert_called_once()
574
+ mock_send_message.assert_called_once_with(jira_issue_id=10001, message=mock_message)
575
+
576
+
577
+ @pytest.mark.django_db
578
+ class TestSendMessageToWatchers:
579
+ """Test send_message_to_watchers function."""
580
+
581
+ @patch("firefighter.raid.forms.jira_client")
582
+ def test_send_message_to_watchers_no_watchers(self, mock_jira_client):
583
+ """Test when no watchers are found."""
584
+ # Given
585
+ mock_jira_client.get_watchers_from_jira_ticket.return_value = None
586
+ mock_message = Mock()
587
+
588
+ # When
589
+ result = send_message_to_watchers(10001, mock_message)
590
+
591
+ # Then
592
+ assert result is True
593
+
594
+ @patch("firefighter.raid.forms.jira_client")
595
+ def test_send_message_to_watchers_with_app_watcher(self, mock_jira_client):
596
+ """Test when watcher is an app (should be skipped)."""
597
+ # Given
598
+ watchers = [
599
+ {"accountId": "app-123", "accountType": "app"}
600
+ ]
601
+ mock_jira_client.get_watchers_from_jira_ticket.return_value = watchers
602
+ mock_message = Mock()
603
+
604
+ # When
605
+ result = send_message_to_watchers(10001, mock_message)
606
+
607
+ # Then
608
+ assert result is True
609
+
610
+ @patch("firefighter.raid.forms.jira_client")
611
+ def test_send_message_to_watchers_no_account_id(self, mock_jira_client):
612
+ """Test when watcher has no accountId."""
613
+ # Given
614
+ watchers = [
615
+ {"displayName": "Test User"} # No accountId
616
+ ]
617
+ mock_jira_client.get_watchers_from_jira_ticket.return_value = watchers
618
+ mock_message = Mock()
619
+
620
+ # When
621
+ result = send_message_to_watchers(10001, mock_message)
622
+
623
+ # Then
624
+ assert result is True
625
+
626
+ @patch("firefighter.raid.forms.jira_client")
627
+ def test_send_message_to_watchers_successful(self, mock_jira_client):
628
+ """Test successful message sending to watchers."""
629
+ # Given
630
+ user = UserFactory()
631
+ slack_user = SlackUser.objects.create(user=user, slack_id="U123456")
632
+ jira_user = JiraUser.objects.create(id="jira-watcher", user=user)
633
+
634
+ watchers = [
635
+ {"accountId": "jira-watcher", "accountType": "atlassian"}
636
+ ]
637
+ mock_jira_client.get_watchers_from_jira_ticket.return_value = watchers
638
+ mock_jira_client.get_jira_user_from_jira_id.return_value = jira_user
639
+ mock_message = Mock()
640
+
641
+ # When
642
+ with patch.object(slack_user, "send_private_message") as mock_send:
643
+ result = send_message_to_watchers(10001, mock_message)
644
+
645
+ # Then
646
+ assert result is True
647
+ mock_send.assert_called_once_with(
648
+ mock_message, unfurl_links=False
649
+ )
650
+
651
+ @patch("firefighter.raid.forms.jira_client")
652
+ def test_send_message_to_watchers_no_slack_user(self, mock_jira_client):
653
+ """Test when watcher has no Slack user."""
654
+ # Given
655
+ user = UserFactory()
656
+ jira_user = JiraUser.objects.create(id="jira-watcher", user=user)
657
+
658
+ watchers = [
659
+ {"accountId": "jira-watcher", "accountType": "atlassian"}
660
+ ]
661
+ mock_jira_client.get_watchers_from_jira_ticket.return_value = watchers
662
+ mock_jira_client.get_jira_user_from_jira_id.return_value = jira_user
663
+ mock_message = Mock()
664
+
665
+ # When
666
+ result = send_message_to_watchers(10001, mock_message)
667
+
668
+ # Then
669
+ assert result is True
670
+
671
+ @patch("firefighter.raid.forms.jira_client")
672
+ def test_send_message_to_watchers_slack_api_error(self, mock_jira_client):
673
+ """Test when Slack API error occurs."""
674
+ # Given
675
+ user = UserFactory()
676
+ slack_user = SlackUser.objects.create(user=user, slack_id="U123456")
677
+ jira_user = JiraUser.objects.create(id="jira-watcher", user=user)
678
+
679
+ watchers = [
680
+ {"accountId": "jira-watcher", "accountType": "atlassian"}
681
+ ]
682
+ mock_jira_client.get_watchers_from_jira_ticket.return_value = watchers
683
+ mock_jira_client.get_jira_user_from_jira_id.return_value = jira_user
684
+ mock_message = Mock()
685
+
686
+ # When
687
+ with patch.object(slack_user, "send_private_message", side_effect=SlackApiError("API Error", response={})):
688
+ result = send_message_to_watchers(10001, mock_message)
689
+
690
+ # Then
691
+ assert result is True
692
+
693
+
694
+ @pytest.mark.django_db
695
+ class TestGetBusinessImpact:
696
+ """Test get_business_impact function."""
697
+
698
+ @patch("firefighter.raid.forms.SelectImpactForm")
699
+ def test_get_business_impact(self, mock_impact_form):
700
+ """Test get_business_impact function."""
701
+ # Given
702
+ impacts_data = {"business_impact": "High"}
703
+ mock_form_instance = Mock()
704
+ mock_form_instance.business_impact_new = "High"
705
+ mock_impact_form.return_value = mock_form_instance
706
+
707
+ # When
708
+ result = get_business_impact(impacts_data)
709
+
710
+ # Then
711
+ assert result == "High"
712
+ mock_impact_form.assert_called_once_with(impacts_data)
713
+
714
+
715
+ @pytest.mark.django_db
716
+ class TestGetPartnerAlertConversations:
717
+ """Test get_partner_alert_conversations function."""
718
+
719
+ def test_get_partner_alert_conversations(self):
720
+ """Test get_partner_alert_conversations function."""
721
+ # Given
722
+ domain = "example.com"
723
+ conversation = Conversation.objects.create(
724
+ channel_id="C123456",
725
+ name="test-channel",
726
+ tag=f"raid_alert__{domain}",
727
+ )
728
+ Conversation.objects.create(
729
+ channel_id="C789012",
730
+ name="other-channel",
731
+ tag="other_tag",
732
+ )
733
+
734
+ # When
735
+ result = get_partner_alert_conversations(domain)
736
+
737
+ # Then
738
+ assert conversation in result
739
+ assert result.count() == 1
740
+
741
+
742
+ @pytest.mark.django_db
743
+ class TestGetInternalAlertConversations:
744
+ """Test get_internal_alert_conversations function."""
745
+
746
+ def setup_method(self):
747
+ """Set up test data."""
748
+ self.user = UserFactory()
749
+ self.jira_user = JiraUser.objects.create(id="jira-123", user=self.user)
750
+
751
+ def test_get_internal_alert_conversations_high_impact_sbi(self):
752
+ """Test with high business impact and SBI project."""
753
+ # Given
754
+ jira_ticket = JiraTicket.objects.create(
755
+ id=10001,
756
+ key="SBI-123",
757
+ summary="Test ticket",
758
+ reporter=self.jira_user,
759
+ business_impact="High",
760
+ project_key="SBI",
761
+ )
762
+ conversation = Conversation.objects.create(
763
+ channel_id="C123456",
764
+ name="sbi-high-channel",
765
+ tag="raid_alert__sbi_high",
766
+ )
767
+
768
+ # When
769
+ result = get_internal_alert_conversations(jira_ticket)
770
+
771
+ # Then
772
+ assert conversation in result
773
+
774
+ def test_get_internal_alert_conversations_normal_impact_incidents(self):
775
+ """Test with normal business impact and non-SBI project."""
776
+ # Given
777
+ jira_ticket = JiraTicket.objects.create(
778
+ id=10002,
779
+ key="TEST-124",
780
+ summary="Test ticket",
781
+ reporter=self.jira_user,
782
+ business_impact="Medium",
783
+ project_key="OTHER",
784
+ )
785
+ conversation = Conversation.objects.create(
786
+ channel_id="C789012",
787
+ name="incidents-normal-channel",
788
+ tag="raid_alert__incidents_normal",
789
+ )
790
+
791
+ # When
792
+ result = get_internal_alert_conversations(jira_ticket)
793
+
794
+ # Then
795
+ assert conversation in result