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,442 @@
1
+ """Tests for raid.service module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import Mock, patch
6
+
7
+ import pytest
8
+
9
+ from firefighter.jira_app.client import JiraAPIError, JiraUserNotFoundError
10
+ from firefighter.raid.service import (
11
+ CustomerIssueData,
12
+ check_issue_id,
13
+ create_issue_customer,
14
+ create_issue_documentation_request,
15
+ create_issue_feature_request,
16
+ create_issue_internal,
17
+ create_issue_seller,
18
+ get_jira_user_from_user,
19
+ )
20
+
21
+
22
+ class TestCustomerIssueData:
23
+ """Test the CustomerIssueData dataclass."""
24
+
25
+ def test_customer_issue_data_creation(self):
26
+ """Test creating CustomerIssueData with all fields."""
27
+ data = CustomerIssueData(
28
+ priority=1,
29
+ labels=["urgent", "customer"],
30
+ platform="web",
31
+ business_impact="high",
32
+ team_to_be_routed="backend-team",
33
+ area="payments",
34
+ zendesk_ticket_id="12345",
35
+ incident_category="payment-issue",
36
+ )
37
+
38
+ assert data.priority == 1
39
+ assert data.labels == ["urgent", "customer"]
40
+ assert data.platform == "web"
41
+ assert data.business_impact == "high"
42
+ assert data.team_to_be_routed == "backend-team"
43
+ assert data.area == "payments"
44
+ assert data.zendesk_ticket_id == "12345"
45
+ assert data.incident_category == "payment-issue"
46
+
47
+ def test_customer_issue_data_defaults(self):
48
+ """Test CustomerIssueData with default values."""
49
+ data = CustomerIssueData(
50
+ priority=None,
51
+ labels=None,
52
+ platform="mobile",
53
+ business_impact=None,
54
+ team_to_be_routed=None,
55
+ area=None,
56
+ zendesk_ticket_id=None,
57
+ )
58
+
59
+ assert data.priority is None
60
+ assert data.labels is None
61
+ assert data.platform == "mobile"
62
+ assert data.business_impact is None
63
+ assert data.team_to_be_routed is None
64
+ assert data.area is None
65
+ assert data.zendesk_ticket_id is None
66
+ assert data.incident_category is None # Default value
67
+
68
+
69
+ @pytest.mark.django_db
70
+ class TestGetJiraUserFromUser:
71
+ """Test get_jira_user_from_user function."""
72
+
73
+ @patch("firefighter.raid.service.jira_client")
74
+ def test_get_jira_user_success(self, mock_jira_client, admin_user):
75
+ """Test successful get_jira_user_from_user."""
76
+ mock_jira_user = Mock()
77
+ mock_jira_user.id = "test_jira_id"
78
+ mock_jira_client.get_jira_user_from_user.return_value = mock_jira_user
79
+
80
+ result = get_jira_user_from_user(admin_user)
81
+
82
+ mock_jira_client.get_jira_user_from_user.assert_called_once_with(admin_user)
83
+ assert result == mock_jira_user
84
+
85
+ @patch("firefighter.raid.service.jira_client")
86
+ def test_get_jira_user_fallback_to_default(self, mock_jira_client, admin_user):
87
+ """Test fallback to default user when jira_client fails."""
88
+
89
+ # Make the first call fail
90
+ mock_jira_client.get_jira_user_from_user.side_effect = JiraAPIError("API error")
91
+
92
+ # Mock the fallback call to succeed
93
+ mock_fallback_user = Mock()
94
+ mock_fallback_user.id = "fallback_id"
95
+ mock_jira_client.get_jira_user_from_jira_id.return_value = mock_fallback_user
96
+
97
+ result = get_jira_user_from_user(admin_user)
98
+
99
+ # Should call the fallback method
100
+ mock_jira_client.get_jira_user_from_jira_id.assert_called_once()
101
+ assert result == mock_fallback_user
102
+
103
+ @patch("firefighter.raid.service.jira_client")
104
+ @patch("firefighter.raid.service.JiraUser")
105
+ def test_get_jira_user_fallback_to_db(self, mock_jira_user_model, mock_jira_client, admin_user):
106
+ """Test fallback to database when both API calls fail."""
107
+
108
+ # Make both API calls fail
109
+ mock_jira_client.get_jira_user_from_user.side_effect = JiraAPIError("API error")
110
+ mock_jira_client.get_jira_user_from_jira_id.side_effect = JiraUserNotFoundError("User not found")
111
+
112
+ # Mock database fallback
113
+ mock_db_user = Mock()
114
+ mock_db_user.id = "db_user_id"
115
+ mock_jira_user_model.objects.get.return_value = mock_db_user
116
+
117
+ result = get_jira_user_from_user(admin_user)
118
+
119
+ # Should try both API calls then fallback to DB
120
+ mock_jira_client.get_jira_user_from_user.assert_called_once_with(admin_user)
121
+ mock_jira_client.get_jira_user_from_jira_id.assert_called_once()
122
+ mock_jira_user_model.objects.get.assert_called_once()
123
+ assert result == mock_db_user
124
+
125
+
126
+ class TestCheckIssueId:
127
+ """Test check_issue_id function."""
128
+
129
+ def test_check_issue_id_with_valid_issue(self):
130
+ """Test check_issue_id with valid issue object."""
131
+ mock_issue = {"id": "TICKET-123"}
132
+
133
+ result = check_issue_id(mock_issue, "Test Title", "test_reporter")
134
+ assert result == "TICKET-123"
135
+
136
+ def test_check_issue_id_with_none_issue(self):
137
+ """Test check_issue_id with None issue should raise AttributeError."""
138
+ # The actual code doesn't check for None, so it raises AttributeError
139
+ with pytest.raises(AttributeError):
140
+ check_issue_id(None, "Test Title", "test_reporter")
141
+
142
+ def test_check_issue_id_with_issue_without_id(self):
143
+ """Test check_issue_id with issue object without id."""
144
+ mock_issue = {"id": None}
145
+
146
+ with pytest.raises(JiraAPIError):
147
+ check_issue_id(mock_issue, "Test Title", "test_reporter")
148
+
149
+
150
+ @pytest.mark.django_db
151
+ class TestCreateIssueFunctions:
152
+ """Test the create_issue_* functions."""
153
+
154
+ @patch("firefighter.raid.service.jira_client")
155
+ def test_create_issue_customer(self, mock_jira_client):
156
+ """Test create_issue_customer function."""
157
+ # Setup mock
158
+ mock_issue = Mock()
159
+ mock_issue.id = "CUST-123"
160
+ mock_jira_client.create_issue.return_value = mock_issue
161
+
162
+ # Create test data
163
+ issue_data = CustomerIssueData(
164
+ priority=1,
165
+ labels=["customer"],
166
+ platform="web",
167
+ business_impact="high",
168
+ team_to_be_routed="support-team",
169
+ area="billing",
170
+ zendesk_ticket_id="ZD-456",
171
+ incident_category="billing-issue",
172
+ )
173
+
174
+ # Call function
175
+ result = create_issue_customer(
176
+ title="Customer Issue",
177
+ description="Customer is experiencing billing problems",
178
+ reporter="reporter_id",
179
+ issue_data=issue_data,
180
+ )
181
+
182
+ # Verify jira_client.create_issue was called with correct parameters
183
+ mock_jira_client.create_issue.assert_called_once_with(
184
+ issuetype="Incident",
185
+ summary="Customer Issue",
186
+ description="Customer is experiencing billing problems",
187
+ assignee=None,
188
+ reporter="reporter_id",
189
+ priority=1,
190
+ labels=["customer"],
191
+ platform="web",
192
+ business_impact="high",
193
+ suggested_team_routing="support-team",
194
+ zendesk_ticket_id="ZD-456",
195
+ incident_category="billing-issue",
196
+ )
197
+
198
+ assert result == mock_issue
199
+
200
+ @patch("firefighter.raid.service.jira_client")
201
+ def test_create_issue_feature_request(self, mock_jira_client):
202
+ """Test create_issue_feature_request function."""
203
+ mock_issue = {"id": "FEAT-123"}
204
+ mock_jira_client.create_issue.return_value = mock_issue
205
+
206
+ result = create_issue_feature_request(
207
+ title="New Feature",
208
+ description="Feature description",
209
+ reporter="reporter_id",
210
+ priority=2,
211
+ labels=["enhancement"],
212
+ platform="mobile",
213
+ )
214
+
215
+ mock_jira_client.create_issue.assert_called_once()
216
+ call_args = mock_jira_client.create_issue.call_args
217
+ assert call_args[1]["issuetype"] == "Feature Request"
218
+ assert call_args[1]["summary"] == "New Feature"
219
+ assert call_args[1]["description"] == "Feature description"
220
+ assert call_args[1]["reporter"] == "reporter_id"
221
+ assert call_args[1]["priority"] == 2
222
+
223
+ assert result == mock_issue
224
+
225
+ @patch("firefighter.raid.service.jira_client")
226
+ def test_create_issue_feature_request_with_none_labels(self, mock_jira_client):
227
+ """Test create_issue_feature_request with None labels (covers line 75)."""
228
+ mock_issue = {"id": "FEAT-124"}
229
+ mock_jira_client.create_issue.return_value = mock_issue
230
+
231
+ result = create_issue_feature_request(
232
+ title="Feature with None labels",
233
+ description="Feature description",
234
+ reporter="reporter_id",
235
+ priority=1,
236
+ labels=None, # This will trigger line 75: labels = [""]
237
+ platform="web",
238
+ )
239
+
240
+ mock_jira_client.create_issue.assert_called_once()
241
+ call_args = mock_jira_client.create_issue.call_args
242
+ # Should have default labels + feature-request
243
+ expected_labels = ["", "feature-request"]
244
+ assert call_args[1]["labels"] == expected_labels
245
+ assert result == mock_issue
246
+
247
+ @patch("firefighter.raid.service.jira_client")
248
+ def test_create_issue_feature_request_with_existing_label(self, mock_jira_client):
249
+ """Test create_issue_feature_request when label already exists (covers branch 76->78)."""
250
+ mock_issue = {"id": "FEAT-125"}
251
+ mock_jira_client.create_issue.return_value = mock_issue
252
+
253
+ result = create_issue_feature_request(
254
+ title="Feature with existing label",
255
+ description="Feature description",
256
+ reporter="reporter_id",
257
+ priority=1,
258
+ labels=["custom", "feature-request"], # Already has feature-request
259
+ platform="web",
260
+ )
261
+
262
+ mock_jira_client.create_issue.assert_called_once()
263
+ call_args = mock_jira_client.create_issue.call_args
264
+ # Should not duplicate the feature-request label
265
+ expected_labels = ["custom", "feature-request"]
266
+ assert call_args[1]["labels"] == expected_labels
267
+ assert result == mock_issue
268
+
269
+ @patch("firefighter.raid.service.jira_client")
270
+ def test_create_issue_documentation_request(self, mock_jira_client):
271
+ """Test create_issue_documentation_request function."""
272
+ mock_issue = {"id": "DOC-123"}
273
+ mock_jira_client.create_issue.return_value = mock_issue
274
+
275
+ result = create_issue_documentation_request(
276
+ title="Update Documentation",
277
+ description="Documentation needs updating",
278
+ reporter="reporter_id",
279
+ priority=3,
280
+ labels=["docs"],
281
+ platform="web",
282
+ )
283
+
284
+ mock_jira_client.create_issue.assert_called_once()
285
+ call_args = mock_jira_client.create_issue.call_args
286
+ assert call_args[1]["issuetype"] == "Documentation/Process Request"
287
+ assert call_args[1]["summary"] == "Update Documentation"
288
+
289
+ assert result == mock_issue
290
+
291
+ @patch("firefighter.raid.service.jira_client")
292
+ def test_create_issue_documentation_request_with_none_labels(self, mock_jira_client):
293
+ """Test create_issue_documentation_request with None labels (covers line 111)."""
294
+ mock_issue = {"id": "DOC-124"}
295
+ mock_jira_client.create_issue.return_value = mock_issue
296
+
297
+ result = create_issue_documentation_request(
298
+ title="Doc with None labels",
299
+ description="Documentation description",
300
+ reporter="reporter_id",
301
+ priority=2,
302
+ labels=None, # This will trigger line 111: labels = [""]
303
+ platform="mobile",
304
+ )
305
+
306
+ mock_jira_client.create_issue.assert_called_once()
307
+ call_args = mock_jira_client.create_issue.call_args
308
+ # Should have default labels + documentation-request
309
+ expected_labels = ["", "documentation-request"]
310
+ assert call_args[1]["labels"] == expected_labels
311
+ assert result == mock_issue
312
+
313
+ @patch("firefighter.raid.service.jira_client")
314
+ def test_create_issue_documentation_request_with_existing_label(self, mock_jira_client):
315
+ """Test create_issue_documentation_request when label already exists (covers branch 112->114)."""
316
+ mock_issue = {"id": "DOC-125"}
317
+ mock_jira_client.create_issue.return_value = mock_issue
318
+
319
+ result = create_issue_documentation_request(
320
+ title="Doc with existing label",
321
+ description="Documentation description",
322
+ reporter="reporter_id",
323
+ priority=2,
324
+ labels=["help", "documentation-request"], # Already has documentation-request
325
+ platform="mobile",
326
+ )
327
+
328
+ mock_jira_client.create_issue.assert_called_once()
329
+ call_args = mock_jira_client.create_issue.call_args
330
+ # Should not duplicate the documentation-request label
331
+ expected_labels = ["help", "documentation-request"]
332
+ assert call_args[1]["labels"] == expected_labels
333
+ assert result == mock_issue
334
+
335
+ @patch("firefighter.raid.service.jira_client")
336
+ def test_create_issue_internal(self, mock_jira_client):
337
+ """Test create_issue_internal function."""
338
+ mock_issue = {"id": "INT-123"}
339
+ mock_jira_client.create_issue.return_value = mock_issue
340
+
341
+ result = create_issue_internal(
342
+ title="Internal Issue",
343
+ description="Internal issue description",
344
+ reporter="reporter_id",
345
+ priority=1,
346
+ labels=["internal"],
347
+ platform="api",
348
+ business_impact="critical",
349
+ team_to_be_routed="backend-team",
350
+ incident_category="infrastructure",
351
+ )
352
+
353
+ mock_jira_client.create_issue.assert_called_once()
354
+ call_args = mock_jira_client.create_issue.call_args
355
+ assert call_args[1]["issuetype"] == "Incident"
356
+ assert call_args[1]["summary"] == "Internal Issue"
357
+
358
+ assert result == mock_issue
359
+
360
+ @patch("firefighter.raid.service.jira_client")
361
+ def test_create_issue_seller(self, mock_jira_client):
362
+ """Test create_issue_seller function."""
363
+ mock_issue = {"id": "SELL-123"}
364
+ mock_jira_client.create_issue.return_value = mock_issue
365
+
366
+ result = create_issue_seller(
367
+ title="Seller Issue",
368
+ description="Seller issue description",
369
+ reporter="reporter_id",
370
+ priority=2,
371
+ labels=["seller"],
372
+ platform="web",
373
+ business_impact="medium",
374
+ team_to_be_routed="seller-team",
375
+ incident_category="marketplace",
376
+ seller_contract_id="CONTRACT-123",
377
+ is_key_account=True,
378
+ is_seller_in_golden_list=False,
379
+ zoho_desk_ticket_id="ZOHO-456",
380
+ )
381
+
382
+ mock_jira_client.create_issue.assert_called_once()
383
+ call_args = mock_jira_client.create_issue.call_args
384
+ assert call_args[1]["issuetype"] == "Incident"
385
+ assert call_args[1]["summary"] == "Seller Issue"
386
+
387
+ assert result == mock_issue
388
+
389
+
390
+ @pytest.mark.django_db
391
+ class TestCreateIssueErrorHandling:
392
+ """Test error handling in create_issue functions."""
393
+
394
+ @patch("firefighter.raid.service.jira_client")
395
+ def test_create_issue_customer_with_jira_client_returning_none(self, mock_jira_client):
396
+ """Test create_issue_customer when jira_client returns None."""
397
+ mock_jira_client.create_issue.return_value = None
398
+
399
+ issue_data = CustomerIssueData(
400
+ priority=1,
401
+ labels=["test"],
402
+ platform="web",
403
+ business_impact="low",
404
+ team_to_be_routed="test-team",
405
+ area="test",
406
+ zendesk_ticket_id="123",
407
+ )
408
+
409
+ with pytest.raises(AttributeError):
410
+ create_issue_customer(
411
+ title="Test",
412
+ description="Test description",
413
+ reporter="test_reporter",
414
+ issue_data=issue_data,
415
+ )
416
+
417
+ @patch("firefighter.raid.service.jira_client")
418
+ def test_create_issue_customer_with_empty_issue_data(self, mock_jira_client):
419
+ """Test create_issue_customer with minimal issue_data."""
420
+ mock_issue = {"id": "TEST-123"}
421
+ mock_jira_client.create_issue.return_value = mock_issue
422
+
423
+ issue_data = CustomerIssueData(
424
+ priority=None,
425
+ labels=None,
426
+ platform="test",
427
+ business_impact=None,
428
+ team_to_be_routed=None,
429
+ area=None,
430
+ zendesk_ticket_id=None,
431
+ )
432
+
433
+ result = create_issue_customer(
434
+ title="Minimal Test",
435
+ description="Minimal test description",
436
+ reporter="test_reporter",
437
+ issue_data=issue_data,
438
+ )
439
+
440
+ # Should still work with None values
441
+ mock_jira_client.create_issue.assert_called_once()
442
+ assert result == mock_issue
@@ -0,0 +1,187 @@
1
+ """Tests for RAID signal handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import Mock, patch
6
+
7
+ import pytest
8
+
9
+ from firefighter.incidents.enums import IncidentStatus
10
+ from firefighter.incidents.models.incident_update import IncidentUpdate
11
+ from firefighter.raid.signals.incident_updated import (
12
+ incident_updated_close_ticket_when_mitigated_or_postmortem,
13
+ )
14
+
15
+
16
+ @pytest.mark.django_db
17
+ class TestIncidentUpdatedCloseJiraTicket:
18
+ """Test that Jira tickets are closed when incidents reach terminal statuses."""
19
+
20
+ @patch("firefighter.raid.signals.incident_updated.client.close_issue")
21
+ def test_close_jira_ticket_when_status_changes_to_mitigated(
22
+ self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory
23
+ ) -> None:
24
+ """Test that Jira ticket is closed when incident status changes to MITIGATED."""
25
+ user = user_factory()
26
+ incident = incident_factory(created_by=user)
27
+ jira_ticket = jira_ticket_factory(incident=incident)
28
+ incident.jira_ticket = jira_ticket
29
+
30
+ incident_update = IncidentUpdate(
31
+ incident=incident,
32
+ status=IncidentStatus.MITIGATED,
33
+ created_by=user,
34
+ )
35
+
36
+ # Call the signal handler
37
+ incident_updated_close_ticket_when_mitigated_or_postmortem(
38
+ sender="update_status",
39
+ incident=incident,
40
+ incident_update=incident_update,
41
+ updated_fields=["_status"],
42
+ )
43
+
44
+ # Verify close_issue was called
45
+ mock_close_issue.assert_called_once_with(issue_id=jira_ticket.id)
46
+
47
+ @patch("firefighter.raid.signals.incident_updated.client.close_issue")
48
+ def test_close_jira_ticket_when_status_changes_to_postmortem(
49
+ self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory
50
+ ) -> None:
51
+ """Test that Jira ticket is closed when incident status changes to POST_MORTEM."""
52
+ user = user_factory()
53
+ incident = incident_factory(created_by=user)
54
+ jira_ticket = jira_ticket_factory(incident=incident)
55
+ incident.jira_ticket = jira_ticket
56
+
57
+ incident_update = IncidentUpdate(
58
+ incident=incident,
59
+ status=IncidentStatus.POST_MORTEM,
60
+ created_by=user,
61
+ )
62
+
63
+ # Call the signal handler
64
+ incident_updated_close_ticket_when_mitigated_or_postmortem(
65
+ sender="update_status",
66
+ incident=incident,
67
+ incident_update=incident_update,
68
+ updated_fields=["_status"],
69
+ )
70
+
71
+ # Verify close_issue was called
72
+ mock_close_issue.assert_called_once_with(issue_id=jira_ticket.id)
73
+
74
+ @patch("firefighter.raid.signals.incident_updated.client.close_issue")
75
+ def test_close_jira_ticket_when_status_changes_to_closed(
76
+ self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory
77
+ ) -> None:
78
+ """Test that Jira ticket is closed when incident status changes to CLOSED (direct close)."""
79
+ user = user_factory()
80
+ incident = incident_factory(created_by=user)
81
+ jira_ticket = jira_ticket_factory(incident=incident)
82
+ incident.jira_ticket = jira_ticket
83
+
84
+ incident_update = IncidentUpdate(
85
+ incident=incident,
86
+ status=IncidentStatus.CLOSED,
87
+ created_by=user,
88
+ )
89
+
90
+ # Call the signal handler
91
+ incident_updated_close_ticket_when_mitigated_or_postmortem(
92
+ sender="update_status",
93
+ incident=incident,
94
+ incident_update=incident_update,
95
+ updated_fields=["_status"],
96
+ )
97
+
98
+ # Verify close_issue was called
99
+ mock_close_issue.assert_called_once_with(issue_id=jira_ticket.id)
100
+
101
+ @patch("firefighter.raid.signals.incident_updated.client.close_issue")
102
+ def test_do_not_close_jira_ticket_when_status_not_terminal(
103
+ self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory
104
+ ) -> None:
105
+ """Test that Jira ticket is NOT closed for non-terminal statuses."""
106
+ user = user_factory()
107
+ incident = incident_factory(created_by=user)
108
+ jira_ticket = jira_ticket_factory(incident=incident)
109
+ incident.jira_ticket = jira_ticket
110
+
111
+ incident_update = IncidentUpdate(
112
+ incident=incident,
113
+ status=IncidentStatus.INVESTIGATING,
114
+ created_by=user,
115
+ )
116
+
117
+ # Call the signal handler
118
+ incident_updated_close_ticket_when_mitigated_or_postmortem(
119
+ sender="update_status",
120
+ incident=incident,
121
+ incident_update=incident_update,
122
+ updated_fields=["_status"],
123
+ )
124
+
125
+ # Verify close_issue was NOT called
126
+ mock_close_issue.assert_not_called()
127
+
128
+ @patch("firefighter.raid.signals.incident_updated.client.close_issue")
129
+ def test_do_not_close_jira_ticket_when_status_not_updated(
130
+ self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory
131
+ ) -> None:
132
+ """Test that Jira ticket is NOT closed when _status is not in updated_fields."""
133
+ user = user_factory()
134
+ incident = incident_factory(created_by=user)
135
+ jira_ticket = jira_ticket_factory(incident=incident)
136
+ incident.jira_ticket = jira_ticket
137
+
138
+ incident_update = IncidentUpdate(
139
+ incident=incident,
140
+ status=IncidentStatus.CLOSED,
141
+ created_by=user,
142
+ )
143
+
144
+ # Call the signal handler with updated_fields that don't include _status
145
+ incident_updated_close_ticket_when_mitigated_or_postmortem(
146
+ sender="update_status",
147
+ incident=incident,
148
+ incident_update=incident_update,
149
+ updated_fields=["priority_id"], # Not _status
150
+ )
151
+
152
+ # Verify close_issue was NOT called
153
+ mock_close_issue.assert_not_called()
154
+
155
+ @patch("firefighter.raid.signals.incident_updated.client.close_issue")
156
+ @patch("firefighter.raid.signals.incident_updated.logger")
157
+ def test_do_not_crash_when_jira_ticket_missing(
158
+ self,
159
+ mock_logger: Mock,
160
+ mock_close_issue: Mock,
161
+ incident_factory,
162
+ user_factory,
163
+ ) -> None:
164
+ """Test that signal handler handles gracefully when Jira ticket is missing."""
165
+ user = user_factory()
166
+ incident = incident_factory(created_by=user)
167
+ # No jira_ticket attached to incident
168
+
169
+ incident_update = IncidentUpdate(
170
+ incident=incident,
171
+ status=IncidentStatus.CLOSED,
172
+ created_by=user,
173
+ )
174
+
175
+ # Call the signal handler - should not crash
176
+ incident_updated_close_ticket_when_mitigated_or_postmortem(
177
+ sender="update_status",
178
+ incident=incident,
179
+ incident_update=incident_update,
180
+ updated_fields=["_status"],
181
+ )
182
+
183
+ # Verify close_issue was NOT called
184
+ mock_close_issue.assert_not_called()
185
+
186
+ # Verify a warning was logged
187
+ mock_logger.warning.assert_called_once()