firefighter-incident 0.0.13__py3-none-any.whl → 0.0.15__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 (136) hide show
  1. firefighter/_version.py +16 -3
  2. firefighter/api/serializers.py +17 -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/confluence/signals/incident_updated.py +2 -2
  8. firefighter/firefighter/settings/components/raid.py +3 -0
  9. firefighter/incidents/admin.py +24 -24
  10. firefighter/incidents/enums.py +22 -2
  11. firefighter/incidents/factories.py +14 -5
  12. firefighter/incidents/forms/close_incident.py +4 -4
  13. firefighter/incidents/forms/closure_reason.py +45 -0
  14. firefighter/incidents/forms/create_incident.py +4 -4
  15. firefighter/incidents/forms/unified_incident.py +406 -0
  16. firefighter/incidents/forms/update_status.py +91 -5
  17. firefighter/incidents/menus.py +2 -2
  18. firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +7 -5
  19. firefighter/incidents/migrations/0009_update_sla.py +7 -5
  20. firefighter/incidents/migrations/0020_create_incident_category_model.py +64 -0
  21. firefighter/incidents/migrations/0021_copy_component_data_to_incident_category.py +57 -0
  22. firefighter/incidents/migrations/0022_add_incident_category_fields.py +34 -0
  23. firefighter/incidents/migrations/0023_populate_incident_category_references.py +57 -0
  24. firefighter/incidents/migrations/0024_remove_component_fields_and_model.py +26 -0
  25. firefighter/incidents/migrations/0025_make_incident_category_required.py +24 -0
  26. firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py +39 -0
  27. firefighter/incidents/migrations/0027_add_closure_fields.py +40 -0
  28. firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
  29. firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
  30. firefighter/incidents/models/__init__.py +1 -1
  31. firefighter/incidents/models/group.py +1 -1
  32. firefighter/incidents/models/incident.py +47 -20
  33. firefighter/incidents/models/{component.py → incident_category.py} +30 -29
  34. firefighter/incidents/models/incident_update.py +3 -3
  35. firefighter/incidents/static/css/main.min.css +1 -1
  36. firefighter/incidents/tables.py +9 -9
  37. firefighter/incidents/templates/layouts/partials/incident_card.html +1 -1
  38. firefighter/incidents/templates/layouts/partials/incident_timeline.html +2 -2
  39. firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
  40. firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html} +13 -13
  41. firefighter/incidents/templates/pages/{component_list.html → incident_category_list.html} +2 -2
  42. firefighter/incidents/templates/pages/incident_detail.html +3 -3
  43. firefighter/incidents/urls.py +6 -6
  44. firefighter/incidents/views/components/details.py +9 -9
  45. firefighter/incidents/views/components/list.py +9 -9
  46. firefighter/incidents/views/reports.py +5 -5
  47. firefighter/incidents/views/users/details.py +2 -2
  48. firefighter/incidents/views/views.py +7 -7
  49. firefighter/jira_app/client.py +1 -1
  50. firefighter/logging/custom_json_formatter.py +2 -1
  51. firefighter/pagerduty/tasks/trigger_oncall.py +1 -1
  52. firefighter/raid/admin.py +0 -11
  53. firefighter/raid/apps.py +9 -26
  54. firefighter/raid/client.py +5 -5
  55. firefighter/raid/forms.py +84 -213
  56. firefighter/raid/migrations/0003_delete_raidarea.py +16 -0
  57. firefighter/raid/models.py +2 -21
  58. firefighter/raid/serializers.py +5 -4
  59. firefighter/raid/service.py +29 -27
  60. firefighter/raid/signals/incident_created.py +42 -15
  61. firefighter/raid/signals/incident_updated.py +3 -2
  62. firefighter/raid/utils.py +1 -1
  63. firefighter/raid/views/__init__.py +1 -1
  64. firefighter/slack/admin.py +8 -8
  65. firefighter/slack/management/commands/switch_test_users.py +272 -0
  66. firefighter/slack/messages/slack_messages.py +24 -9
  67. firefighter/slack/migrations/0005_add_incident_categories_fields.py +33 -0
  68. firefighter/slack/migrations/0006_copy_components_to_incident_categories.py +57 -0
  69. firefighter/slack/migrations/0007_remove_components_fields.py +22 -0
  70. firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py +33 -0
  71. firefighter/slack/models/conversation.py +3 -3
  72. firefighter/slack/models/incident_channel.py +1 -1
  73. firefighter/slack/models/user.py +1 -1
  74. firefighter/slack/models/user_group.py +3 -3
  75. firefighter/slack/rules.py +2 -2
  76. firefighter/slack/signals/create_incident_conversation.py +6 -0
  77. firefighter/slack/signals/get_users.py +2 -2
  78. firefighter/slack/signals/incident_updated.py +8 -2
  79. firefighter/slack/utils.py +2 -2
  80. firefighter/slack/views/events/home.py +2 -2
  81. firefighter/slack/views/modals/__init__.py +4 -0
  82. firefighter/slack/views/modals/base_modal/form_utils.py +78 -0
  83. firefighter/slack/views/modals/close.py +18 -5
  84. firefighter/slack/views/modals/closure_reason.py +193 -0
  85. firefighter/slack/views/modals/open.py +83 -12
  86. firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
  87. firefighter/slack/views/modals/opening/details/unified.py +203 -0
  88. firefighter/slack/views/modals/opening/select_impact.py +5 -2
  89. firefighter/slack/views/modals/opening/set_details.py +3 -2
  90. firefighter/slack/views/modals/postmortem.py +10 -2
  91. firefighter/slack/views/modals/update_status.py +32 -6
  92. firefighter/slack/views/modals/utils.py +51 -0
  93. firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
  94. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/METADATA +2 -2
  95. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/RECORD +133 -88
  96. firefighter_tests/conftest.py +4 -5
  97. firefighter_tests/test_api/test_api_landbot.py +1 -1
  98. firefighter_tests/test_firefighter/test_sso.py +146 -0
  99. firefighter_tests/test_incidents/test_enums.py +100 -0
  100. firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
  101. firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
  102. firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
  103. firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
  104. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
  105. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
  106. firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
  107. firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
  108. firefighter_tests/test_incidents/test_incident_urls.py +3 -3
  109. firefighter_tests/test_incidents/test_models/test_incident_category.py +165 -0
  110. firefighter_tests/test_incidents/test_models/test_incident_model.py +70 -2
  111. firefighter_tests/test_raid/conftest.py +154 -0
  112. firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
  113. firefighter_tests/test_raid/test_priority_mapping.py +267 -0
  114. firefighter_tests/test_raid/test_raid_client.py +580 -0
  115. firefighter_tests/test_raid/test_raid_forms.py +552 -0
  116. firefighter_tests/test_raid/test_raid_models.py +185 -0
  117. firefighter_tests/test_raid/test_raid_serializers.py +507 -0
  118. firefighter_tests/test_raid/test_raid_service.py +442 -0
  119. firefighter_tests/test_raid/test_raid_signals.py +187 -0
  120. firefighter_tests/test_raid/test_raid_views.py +196 -0
  121. firefighter_tests/test_slack/messages/__init__.py +0 -0
  122. firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
  123. firefighter_tests/test_slack/views/modals/conftest.py +140 -0
  124. firefighter_tests/test_slack/views/modals/test_close.py +71 -9
  125. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
  126. firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
  127. firefighter_tests/test_slack/views/modals/test_open.py +146 -2
  128. firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
  129. firefighter_tests/test_slack/views/modals/test_update_status.py +331 -7
  130. firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
  131. firefighter/raid/views/open_normal.py +0 -139
  132. firefighter/slack/views/modals/opening/details/critical.py +0 -88
  133. firefighter_fixtures/raid/area.json +0 -1
  134. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/WHEEL +0 -0
  135. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/entry_points.txt +0 -0
  136. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,552 @@
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
+ IncidentFactory,
11
+ PriorityFactory,
12
+ UserFactory,
13
+ )
14
+ from firefighter.incidents.models.priority import Priority
15
+ from firefighter.jira_app.client import JiraAPIError, JiraUserNotFoundError
16
+ from firefighter.jira_app.models import JiraUser
17
+ from firefighter.raid.forms import (
18
+ PlatformChoices,
19
+ alert_slack_comment_ticket,
20
+ alert_slack_new_jira_ticket,
21
+ alert_slack_update_ticket,
22
+ get_business_impact,
23
+ get_internal_alert_conversations,
24
+ get_partner_alert_conversations,
25
+ initial_priority,
26
+ process_jira_issue,
27
+ send_message_to_watchers,
28
+ set_jira_ticket_watchers_raid,
29
+ )
30
+ from firefighter.raid.models import JiraTicket
31
+ from firefighter.slack.models.conversation import Conversation
32
+ from firefighter.slack.models.user import SlackUser
33
+
34
+
35
+ class TestPlatformChoices(TestCase):
36
+ """Test PlatformChoices enum."""
37
+
38
+ def test_platform_choices_values(self):
39
+ """Test that platform choices have correct values."""
40
+ assert PlatformChoices.FR == "platform-FR"
41
+ assert PlatformChoices.DE == "platform-DE"
42
+ assert PlatformChoices.IT == "platform-IT"
43
+ assert PlatformChoices.ES == "platform-ES"
44
+ assert PlatformChoices.UK == "platform-UK"
45
+ assert PlatformChoices.ALL == "platform-All"
46
+ assert PlatformChoices.INTERNAL == "platform-Internal"
47
+
48
+
49
+ @pytest.mark.django_db
50
+ class TestInitialPriority:
51
+ """Test initial_priority function."""
52
+
53
+ def test_initial_priority_returns_default(self):
54
+ """Test that initial_priority returns default priority."""
55
+ # Given
56
+ Priority.objects.all().delete() # Clean slate
57
+ default_priority = PriorityFactory(default=True, value=100)
58
+ PriorityFactory(default=False, value=101) # Create non-default priority
59
+
60
+ # When
61
+ result = initial_priority()
62
+
63
+ # Then
64
+ assert result == default_priority
65
+
66
+
67
+ # NOTE: Form class tests (TestCreateNormalCustomerIncidentForm,
68
+ # TestCreateRaidDocumentationRequestIncidentForm, TestCreateRaidFeatureRequestIncidentForm,
69
+ # TestCreateRaidInternalIncidentForm, TestRaidCreateIncidentSellerForm) have been removed.
70
+ #
71
+ # These forms have been replaced by the unified incident form:
72
+ # firefighter.incidents.forms.unified_incident.UnifiedIncidentForm
73
+ #
74
+ # Tests for the unified form should be added in a new file:
75
+ # tests/test_incidents/test_forms/test_unified_incident.py
76
+
77
+
78
+ @pytest.mark.django_db
79
+ class TestProcessJiraIssue:
80
+ """Test process_jira_issue function."""
81
+
82
+ def setup_method(self):
83
+ """Set up test data."""
84
+ self.user = UserFactory()
85
+ self.jira_user = JiraUser.objects.create(id="jira-123", user=self.user)
86
+
87
+ @patch("firefighter.raid.forms.alert_slack_new_jira_ticket")
88
+ @patch("firefighter.raid.forms.set_jira_ticket_watchers_raid")
89
+ @patch("firefighter.raid.forms.SelectImpactForm")
90
+ def test_process_jira_issue(self, mock_impact_form, mock_set_watchers, mock_alert_slack):
91
+ """Test process_jira_issue function."""
92
+ # Given
93
+ issue_data = {
94
+ "id": "10001",
95
+ "key": "TEST-123",
96
+ "summary": "Test issue",
97
+ "reporter": self.jira_user,
98
+ }
99
+ impacts_data = {"business_impact": "High"}
100
+
101
+ mock_form_instance = Mock()
102
+ mock_impact_form.return_value = mock_form_instance
103
+ mock_set_watchers.return_value = None
104
+ mock_alert_slack.return_value = None
105
+
106
+ # When
107
+ process_jira_issue(issue_data, self.user, self.jira_user, impacts_data)
108
+
109
+ # Then
110
+ # Check that JiraTicket was created
111
+ assert JiraTicket.objects.filter(key="TEST-123").exists()
112
+
113
+ # Check that all functions were called
114
+ mock_impact_form.assert_called_once_with(impacts_data)
115
+ mock_form_instance.save.assert_called_once_with(incident=ANY)
116
+ mock_set_watchers.assert_called_once_with(ANY)
117
+ mock_alert_slack.assert_called_once_with(ANY)
118
+
119
+
120
+ @pytest.mark.django_db
121
+ class TestSetJiraTicketWatchersRaid:
122
+ """Test set_jira_ticket_watchers_raid function."""
123
+
124
+ def setup_method(self):
125
+ """Set up test data."""
126
+ self.user = UserFactory()
127
+ self.jira_user = JiraUser.objects.create(id="jira-123", user=self.user)
128
+ self.jira_ticket = JiraTicket.objects.create(
129
+ id=10001,
130
+ key="TEST-123",
131
+ summary="Test ticket",
132
+ reporter=self.jira_user,
133
+ )
134
+
135
+ @patch("firefighter.raid.forms.jira_client")
136
+ def test_set_jira_ticket_watchers_success(self, mock_jira_client):
137
+ """Test successful watcher addition."""
138
+ # Given
139
+ mock_default_user = Mock()
140
+ mock_jira_client.get_jira_user_from_jira_id.return_value = mock_default_user
141
+ mock_jira_client.jira.add_watcher.return_value = None
142
+
143
+ # When
144
+ set_jira_ticket_watchers_raid(self.jira_ticket)
145
+
146
+ # Then
147
+ mock_jira_client.jira.add_watcher.assert_called_once_with(
148
+ issue=10001, watcher="jira-123"
149
+ )
150
+
151
+ @patch("firefighter.raid.forms.jira_client")
152
+ def test_set_jira_ticket_watchers_jira_user_not_found(self, mock_jira_client):
153
+ """Test when default JIRA user is not found."""
154
+ # Given
155
+ mock_jira_client.get_jira_user_from_jira_id.side_effect = JiraUserNotFoundError("Not found")
156
+
157
+ # When
158
+ set_jira_ticket_watchers_raid(self.jira_ticket)
159
+
160
+ # Then
161
+ # Should handle the exception and continue
162
+
163
+ @patch("firefighter.raid.forms.jira_client")
164
+ def test_set_jira_ticket_watchers_add_watcher_error(self, mock_jira_client):
165
+ """Test when adding watcher fails."""
166
+ # Given
167
+ mock_default_user = Mock()
168
+ mock_jira_client.get_jira_user_from_jira_id.return_value = mock_default_user
169
+ mock_jira_client.jira.add_watcher.side_effect = JiraAPIError("API Error")
170
+ mock_jira_client.jira.remove_watcher.side_effect = JiraAPIError("Remove Error")
171
+
172
+ # When
173
+ set_jira_ticket_watchers_raid(self.jira_ticket)
174
+
175
+ # Then
176
+ # Should handle both exceptions
177
+
178
+
179
+ @pytest.mark.django_db
180
+ class TestAlertSlackNewJiraTicket:
181
+ """Test alert_slack_new_jira_ticket function."""
182
+
183
+ def setup_method(self):
184
+ """Set up test data."""
185
+ self.user = UserFactory(email="test@example.com")
186
+ self.slack_user = SlackUser.objects.create(
187
+ user=self.user, slack_id="U123456"
188
+ )
189
+ self.jira_user = JiraUser.objects.create(id="jira-123", user=self.user)
190
+ self.jira_ticket = JiraTicket.objects.create(
191
+ id=10001,
192
+ key="TEST-123",
193
+ summary="Test ticket",
194
+ reporter=self.jira_user,
195
+ )
196
+
197
+ def test_alert_slack_new_jira_ticket_with_incident_raises_error(self):
198
+ """Test that function raises ValueError for critical incidents."""
199
+ # Given - Create an incident and link it to the ticket
200
+ incident = IncidentFactory()
201
+ self.jira_ticket.incident = incident
202
+ self.jira_ticket.save()
203
+
204
+ # When & Then
205
+ with pytest.raises(ValueError, match="This is a critical incident"):
206
+ alert_slack_new_jira_ticket(self.jira_ticket)
207
+
208
+ @patch("firefighter.raid.forms.get_partner_alert_conversations")
209
+ @patch("firefighter.raid.forms.get_internal_alert_conversations")
210
+ @patch("firefighter.raid.forms.SlackMessageRaidCreatedIssue")
211
+ def test_alert_slack_new_jira_ticket_no_reporter_user(
212
+ self, mock_message_class, mock_get_internal, mock_get_partner
213
+ ):
214
+ """Test when reporter user is None."""
215
+ # Given
216
+ mock_get_internal.return_value = Conversation.objects.none()
217
+ mock_get_partner.return_value = Conversation.objects.none()
218
+
219
+ # Mock message class to return proper strings instead of MagicMock
220
+ mock_message_instance = Mock()
221
+ mock_message_instance.get_text.return_value = "Test message"
222
+ mock_message_instance.get_blocks.return_value = []
223
+ mock_message_instance.get_metadata.return_value = {}
224
+ mock_message_class.return_value = mock_message_instance
225
+
226
+ # When
227
+ alert_slack_new_jira_ticket(self.jira_ticket, reporter_user=None)
228
+
229
+ # Then
230
+ # Should log warning and return early
231
+
232
+ @patch("firefighter.raid.forms.get_partner_alert_conversations")
233
+ @patch("firefighter.raid.forms.get_internal_alert_conversations")
234
+ @patch("firefighter.raid.forms.SlackMessageRaidCreatedIssue")
235
+ def test_alert_slack_new_jira_ticket_with_slack_user(
236
+ self, mock_message_class, mock_get_internal, mock_get_partner
237
+ ):
238
+ """Test with valid Slack user."""
239
+ # Given
240
+ mock_message = Mock()
241
+ mock_message_class.return_value = mock_message
242
+ mock_get_internal.return_value = Conversation.objects.none()
243
+ mock_get_partner.return_value = Conversation.objects.none()
244
+
245
+ # When
246
+ with patch.object(self.slack_user, "send_private_message") as mock_send:
247
+ alert_slack_new_jira_ticket(self.jira_ticket, reporter_user=self.user)
248
+
249
+ # Then
250
+ mock_send.assert_called_once_with(
251
+ mock_message, unfurl_links=False
252
+ )
253
+
254
+ @patch("firefighter.raid.forms.get_partner_alert_conversations")
255
+ @patch("firefighter.raid.forms.get_internal_alert_conversations")
256
+ @patch("firefighter.raid.forms.SlackMessageRaidCreatedIssue")
257
+ def test_alert_slack_new_jira_ticket_messages_disabled(
258
+ self, mock_message_class, mock_get_internal, mock_get_partner
259
+ ):
260
+ """Test when user has disabled private messages."""
261
+ # Given
262
+ mock_message = Mock()
263
+ mock_message_class.return_value = mock_message
264
+ mock_get_internal.return_value = Conversation.objects.none()
265
+ mock_get_partner.return_value = Conversation.objects.none()
266
+
267
+ slack_error = SlackApiError("Error", response={"error": "messages_tab_disabled"})
268
+
269
+ # When
270
+ with patch.object(self.slack_user, "send_private_message", side_effect=slack_error):
271
+ alert_slack_new_jira_ticket(self.jira_ticket, reporter_user=self.user)
272
+
273
+ # Then
274
+ # Should log warning about disabled messages
275
+
276
+
277
+ @pytest.mark.django_db
278
+ class TestAlertSlackUpdateTicket:
279
+ """Test alert_slack_update_ticket function."""
280
+
281
+ @patch("firefighter.raid.forms.send_message_to_watchers")
282
+ @patch("firefighter.raid.forms.SlackMessageRaidModifiedIssue")
283
+ def test_alert_slack_update_ticket(self, mock_message_class, mock_send_message):
284
+ """Test alert_slack_update_ticket function."""
285
+ # Given
286
+ mock_message = Mock()
287
+ mock_message_class.return_value = mock_message
288
+ mock_send_message.return_value = True
289
+
290
+ # When
291
+ result = alert_slack_update_ticket(
292
+ jira_ticket_id=10001,
293
+ jira_ticket_key="TEST-123",
294
+ jira_author_name="John Doe",
295
+ jira_field_modified="Priority",
296
+ jira_field_from="High",
297
+ jira_field_to="Critical"
298
+ )
299
+
300
+ # Then
301
+ assert result is True
302
+ mock_message_class.assert_called_once()
303
+ mock_send_message.assert_called_once_with(jira_issue_id=10001, message=mock_message)
304
+
305
+
306
+ @pytest.mark.django_db
307
+ class TestAlertSlackCommentTicket:
308
+ """Test alert_slack_comment_ticket function."""
309
+
310
+ @patch("firefighter.raid.forms.send_message_to_watchers")
311
+ @patch("firefighter.raid.forms.SlackMessageRaidComment")
312
+ def test_alert_slack_comment_ticket(self, mock_message_class, mock_send_message):
313
+ """Test alert_slack_comment_ticket function."""
314
+ # Given
315
+ mock_message = Mock()
316
+ mock_message_class.return_value = mock_message
317
+ mock_send_message.return_value = True
318
+
319
+ # When
320
+ result = alert_slack_comment_ticket(
321
+ webhook_event="comment_created",
322
+ jira_ticket_id=10001,
323
+ jira_ticket_key="TEST-123",
324
+ author_jira_name="Jane Smith",
325
+ comment="This is a test comment"
326
+ )
327
+
328
+ # Then
329
+ assert result is True
330
+ mock_message_class.assert_called_once()
331
+ mock_send_message.assert_called_once_with(jira_issue_id=10001, message=mock_message)
332
+
333
+
334
+ @pytest.mark.django_db
335
+ class TestSendMessageToWatchers:
336
+ """Test send_message_to_watchers function."""
337
+
338
+ @patch("firefighter.raid.forms.jira_client")
339
+ def test_send_message_to_watchers_no_watchers(self, mock_jira_client):
340
+ """Test when no watchers are found."""
341
+ # Given
342
+ mock_jira_client.get_watchers_from_jira_ticket.return_value = None
343
+ mock_message = Mock()
344
+
345
+ # When
346
+ result = send_message_to_watchers(10001, mock_message)
347
+
348
+ # Then
349
+ assert result is True
350
+
351
+ @patch("firefighter.raid.forms.jira_client")
352
+ def test_send_message_to_watchers_with_app_watcher(self, mock_jira_client):
353
+ """Test when watcher is an app (should be skipped)."""
354
+ # Given
355
+ watchers = [
356
+ {"accountId": "app-123", "accountType": "app"}
357
+ ]
358
+ mock_jira_client.get_watchers_from_jira_ticket.return_value = watchers
359
+ mock_message = Mock()
360
+
361
+ # When
362
+ result = send_message_to_watchers(10001, mock_message)
363
+
364
+ # Then
365
+ assert result is True
366
+
367
+ @patch("firefighter.raid.forms.jira_client")
368
+ def test_send_message_to_watchers_no_account_id(self, mock_jira_client):
369
+ """Test when watcher has no accountId."""
370
+ # Given
371
+ watchers = [
372
+ {"displayName": "Test User"} # No accountId
373
+ ]
374
+ mock_jira_client.get_watchers_from_jira_ticket.return_value = watchers
375
+ mock_message = Mock()
376
+
377
+ # When
378
+ result = send_message_to_watchers(10001, mock_message)
379
+
380
+ # Then
381
+ assert result is True
382
+
383
+ @patch("firefighter.raid.forms.jira_client")
384
+ def test_send_message_to_watchers_successful(self, mock_jira_client):
385
+ """Test successful message sending to watchers."""
386
+ # Given
387
+ user = UserFactory()
388
+ slack_user = SlackUser.objects.create(user=user, slack_id="U123456")
389
+ jira_user = JiraUser.objects.create(id="jira-watcher", user=user)
390
+
391
+ watchers = [
392
+ {"accountId": "jira-watcher", "accountType": "atlassian"}
393
+ ]
394
+ mock_jira_client.get_watchers_from_jira_ticket.return_value = watchers
395
+ mock_jira_client.get_jira_user_from_jira_id.return_value = jira_user
396
+ mock_message = Mock()
397
+
398
+ # When
399
+ with patch.object(slack_user, "send_private_message") as mock_send:
400
+ result = send_message_to_watchers(10001, mock_message)
401
+
402
+ # Then
403
+ assert result is True
404
+ mock_send.assert_called_once_with(
405
+ mock_message, unfurl_links=False
406
+ )
407
+
408
+ @patch("firefighter.raid.forms.jira_client")
409
+ def test_send_message_to_watchers_no_slack_user(self, mock_jira_client):
410
+ """Test when watcher has no Slack user."""
411
+ # Given
412
+ user = UserFactory()
413
+ jira_user = JiraUser.objects.create(id="jira-watcher", user=user)
414
+
415
+ watchers = [
416
+ {"accountId": "jira-watcher", "accountType": "atlassian"}
417
+ ]
418
+ mock_jira_client.get_watchers_from_jira_ticket.return_value = watchers
419
+ mock_jira_client.get_jira_user_from_jira_id.return_value = jira_user
420
+ mock_message = Mock()
421
+
422
+ # When
423
+ result = send_message_to_watchers(10001, mock_message)
424
+
425
+ # Then
426
+ assert result is True
427
+
428
+ @patch("firefighter.raid.forms.jira_client")
429
+ def test_send_message_to_watchers_slack_api_error(self, mock_jira_client):
430
+ """Test when Slack API error occurs."""
431
+ # Given
432
+ user = UserFactory()
433
+ slack_user = SlackUser.objects.create(user=user, slack_id="U123456")
434
+ jira_user = JiraUser.objects.create(id="jira-watcher", user=user)
435
+
436
+ watchers = [
437
+ {"accountId": "jira-watcher", "accountType": "atlassian"}
438
+ ]
439
+ mock_jira_client.get_watchers_from_jira_ticket.return_value = watchers
440
+ mock_jira_client.get_jira_user_from_jira_id.return_value = jira_user
441
+ mock_message = Mock()
442
+
443
+ # When
444
+ with patch.object(slack_user, "send_private_message", side_effect=SlackApiError("API Error", response={})):
445
+ result = send_message_to_watchers(10001, mock_message)
446
+
447
+ # Then
448
+ assert result is True
449
+
450
+
451
+ @pytest.mark.django_db
452
+ class TestGetBusinessImpact:
453
+ """Test get_business_impact function."""
454
+
455
+ @patch("firefighter.raid.forms.SelectImpactForm")
456
+ def test_get_business_impact(self, mock_impact_form):
457
+ """Test get_business_impact function."""
458
+ # Given
459
+ impacts_data = {"business_impact": "High"}
460
+ mock_form_instance = Mock()
461
+ mock_form_instance.business_impact_new = "High"
462
+ mock_impact_form.return_value = mock_form_instance
463
+
464
+ # When
465
+ result = get_business_impact(impacts_data)
466
+
467
+ # Then
468
+ assert result == "High"
469
+ mock_impact_form.assert_called_once_with(impacts_data)
470
+
471
+
472
+ @pytest.mark.django_db
473
+ class TestGetPartnerAlertConversations:
474
+ """Test get_partner_alert_conversations function."""
475
+
476
+ def test_get_partner_alert_conversations(self):
477
+ """Test get_partner_alert_conversations function."""
478
+ # Given
479
+ domain = "example.com"
480
+ conversation = Conversation.objects.create(
481
+ channel_id="C123456",
482
+ name="test-channel",
483
+ tag=f"raid_alert__{domain}",
484
+ )
485
+ Conversation.objects.create(
486
+ channel_id="C789012",
487
+ name="other-channel",
488
+ tag="other_tag",
489
+ )
490
+
491
+ # When
492
+ result = get_partner_alert_conversations(domain)
493
+
494
+ # Then
495
+ assert conversation in result
496
+ assert result.count() == 1
497
+
498
+
499
+ @pytest.mark.django_db
500
+ class TestGetInternalAlertConversations:
501
+ """Test get_internal_alert_conversations function."""
502
+
503
+ def setup_method(self):
504
+ """Set up test data."""
505
+ self.user = UserFactory()
506
+ self.jira_user = JiraUser.objects.create(id="jira-123", user=self.user)
507
+
508
+ def test_get_internal_alert_conversations_high_impact_sbi(self):
509
+ """Test with high business impact and SBI project."""
510
+ # Given
511
+ jira_ticket = JiraTicket.objects.create(
512
+ id=10001,
513
+ key="SBI-123",
514
+ summary="Test ticket",
515
+ reporter=self.jira_user,
516
+ business_impact="High",
517
+ project_key="SBI",
518
+ )
519
+ conversation = Conversation.objects.create(
520
+ channel_id="C123456",
521
+ name="sbi-high-channel",
522
+ tag="raid_alert__sbi_high",
523
+ )
524
+
525
+ # When
526
+ result = get_internal_alert_conversations(jira_ticket)
527
+
528
+ # Then
529
+ assert conversation in result
530
+
531
+ def test_get_internal_alert_conversations_normal_impact_incidents(self):
532
+ """Test with normal business impact and non-SBI project."""
533
+ # Given
534
+ jira_ticket = JiraTicket.objects.create(
535
+ id=10002,
536
+ key="TEST-124",
537
+ summary="Test ticket",
538
+ reporter=self.jira_user,
539
+ business_impact="Medium",
540
+ project_key="OTHER",
541
+ )
542
+ conversation = Conversation.objects.create(
543
+ channel_id="C789012",
544
+ name="incidents-normal-channel",
545
+ tag="raid_alert__incidents_normal",
546
+ )
547
+
548
+ # When
549
+ result = get_internal_alert_conversations(jira_ticket)
550
+
551
+ # Then
552
+ assert conversation in result