firefighter-incident 0.0.14__py3-none-any.whl → 0.0.16__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 (64) hide show
  1. firefighter/_version.py +2 -2
  2. firefighter/api/serializers.py +9 -0
  3. firefighter/confluence/signals/incident_updated.py +2 -2
  4. firefighter/incidents/enums.py +22 -2
  5. firefighter/incidents/forms/closure_reason.py +45 -0
  6. firefighter/incidents/forms/unified_incident.py +406 -0
  7. firefighter/incidents/forms/update_status.py +87 -1
  8. firefighter/incidents/migrations/0027_add_closure_fields.py +40 -0
  9. firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
  10. firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
  11. firefighter/incidents/models/incident.py +32 -5
  12. firefighter/incidents/static/css/main.min.css +1 -1
  13. firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
  14. firefighter/incidents/views/reports.py +3 -3
  15. firefighter/raid/apps.py +9 -26
  16. firefighter/raid/client.py +2 -2
  17. firefighter/raid/forms.py +75 -238
  18. firefighter/raid/signals/incident_created.py +38 -13
  19. firefighter/raid/signals/incident_updated.py +3 -2
  20. firefighter/slack/messages/slack_messages.py +19 -4
  21. firefighter/slack/rules.py +1 -1
  22. firefighter/slack/signals/create_incident_conversation.py +6 -0
  23. firefighter/slack/signals/incident_updated.py +7 -1
  24. firefighter/slack/views/modals/__init__.py +4 -0
  25. firefighter/slack/views/modals/base_modal/form_utils.py +63 -0
  26. firefighter/slack/views/modals/close.py +15 -2
  27. firefighter/slack/views/modals/closure_reason.py +193 -0
  28. firefighter/slack/views/modals/open.py +60 -13
  29. firefighter/slack/views/modals/opening/details/unified.py +203 -0
  30. firefighter/slack/views/modals/opening/select_impact.py +1 -1
  31. firefighter/slack/views/modals/opening/set_details.py +3 -2
  32. firefighter/slack/views/modals/postmortem.py +10 -2
  33. firefighter/slack/views/modals/update_status.py +28 -2
  34. firefighter/slack/views/modals/utils.py +51 -0
  35. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/METADATA +1 -1
  36. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/RECORD +62 -38
  37. firefighter_tests/test_incidents/test_enums.py +100 -0
  38. firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
  39. firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
  40. firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
  41. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
  42. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
  43. firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
  44. firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
  45. firefighter_tests/test_incidents/test_models/test_incident_model.py +68 -0
  46. firefighter_tests/test_raid/conftest.py +154 -0
  47. firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
  48. firefighter_tests/test_raid/test_raid_forms.py +10 -253
  49. firefighter_tests/test_raid/test_raid_signals.py +187 -0
  50. firefighter_tests/test_slack/messages/__init__.py +0 -0
  51. firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
  52. firefighter_tests/test_slack/views/modals/conftest.py +140 -0
  53. firefighter_tests/test_slack/views/modals/test_close.py +65 -3
  54. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
  55. firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
  56. firefighter_tests/test_slack/views/modals/test_open.py +146 -2
  57. firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
  58. firefighter_tests/test_slack/views/modals/test_update_status.py +327 -3
  59. firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
  60. firefighter/raid/views/open_normal.py +0 -139
  61. firefighter/slack/views/modals/opening/details/critical.py +0 -88
  62. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/WHEEL +0 -0
  63. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/entry_points.txt +0 -0
  64. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/licenses/LICENSE +0 -0
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from unittest.mock import MagicMock
4
+ from unittest.mock import MagicMock, PropertyMock
5
5
 
6
6
  import pytest
7
7
  from django.conf import settings
8
8
  from pytest_mock import MockerFixture
9
9
 
10
+ from firefighter.incidents.enums import IncidentStatus
10
11
  from firefighter.incidents.factories import IncidentFactory, UserFactory
11
12
  from firefighter.incidents.models import Incident
12
13
  from firefighter.slack.views import UpdateStatusModal
@@ -47,7 +48,10 @@ class TestUpdateStatusModal:
47
48
  modal.handle_modal_fn(ack=ack, body={}, incident=incident, user=user)
48
49
 
49
50
  @staticmethod
50
- def test_submit_valid_form(mocker: MockerFixture, incident: Incident) -> None:
51
+ def test_submit_valid_form(mocker: MockerFixture) -> None:
52
+ # Create an incident in OPEN status so we can transition to INVESTIGATING
53
+ incident = IncidentFactory.build(_status=IncidentStatus.OPEN) # OPEN status
54
+
51
55
  modal = UpdateStatusModal()
52
56
  trigger_incident_workflow = mocker.patch.object(
53
57
  modal, "_trigger_incident_workflow"
@@ -56,14 +60,326 @@ class TestUpdateStatusModal:
56
60
  ack = MagicMock()
57
61
  user = UserFactory.build()
58
62
  user.save()
63
+
64
+ # Create a valid submission that transitions from OPEN to INVESTIGATING (valid workflow)
65
+ valid_submission_copy = dict(valid_submission)
66
+ # Change status to INVESTIGATING (20) which is valid from OPEN
67
+ valid_submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
68
+ "text": {"type": "plain_text", "text": "Investigating", "emoji": True},
69
+ "value": "20",
70
+ }
71
+
59
72
  modal.handle_modal_fn(
60
- ack=ack, body=valid_submission, incident=incident, user=user
73
+ ack=ack, body=valid_submission_copy, incident=incident, user=user
61
74
  )
62
75
 
63
76
  # Assert
64
77
  ack.assert_called_once_with()
65
78
  trigger_incident_workflow.assert_called_once()
66
79
 
80
+ @staticmethod
81
+ def test_cannot_close_without_required_key_events(mocker: MockerFixture) -> None:
82
+ """Test that closing is prevented when required key events are missing.
83
+
84
+ This tests the scenario where a P3+ incident (no postmortem needed) is in
85
+ MITIGATED status and tries to close, but missing key events blocks it.
86
+ """
87
+ # Create a user first
88
+ user = UserFactory.build()
89
+ user.save()
90
+
91
+ # Create a P3+ incident in MITIGATED status (can go directly to CLOSED)
92
+ incident = IncidentFactory.build(
93
+ _status=IncidentStatus.MITIGATED,
94
+ created_by=user,
95
+ )
96
+ incident.save()
97
+ # Mock needs_postmortem to return False (P3+ incident)
98
+ mocker.patch.object(
99
+ type(incident),
100
+ "needs_postmortem",
101
+ new_callable=PropertyMock,
102
+ return_value=False
103
+ )
104
+ # Mock can_be_closed to return False with MISSING_REQUIRED_KEY_EVENTS reason
105
+ mocker.patch.object(
106
+ type(incident),
107
+ "can_be_closed",
108
+ new_callable=PropertyMock,
109
+ return_value=(False, [("MISSING_REQUIRED_KEY_EVENTS", "Missing key events: detected, started")])
110
+ )
111
+
112
+ modal = UpdateStatusModal()
113
+ trigger_incident_workflow = mocker.patch.object(
114
+ modal, "_trigger_incident_workflow"
115
+ )
116
+
117
+ ack = MagicMock()
118
+ user = UserFactory.build()
119
+ user.save()
120
+
121
+ # Create a submission trying to close the incident
122
+ submission_copy = dict(valid_submission)
123
+ # Change status to CLOSED (60)
124
+ submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
125
+ "text": {"type": "plain_text", "text": "Closed", "emoji": True},
126
+ "value": "60",
127
+ }
128
+ # Update the private_metadata to match our test incident
129
+ submission_copy["view"]["private_metadata"] = str(incident.id)
130
+
131
+ modal.handle_modal_fn(
132
+ ack=ack, body=submission_copy, incident=incident, user=user
133
+ )
134
+
135
+ # Assert that ack was called with errors (may be 1 or 2 calls depending on form validation)
136
+ assert ack.called
137
+ # Check the last call (the error response)
138
+ last_call_kwargs = ack.call_args.kwargs
139
+ assert "response_action" in last_call_kwargs
140
+ assert last_call_kwargs["response_action"] == "errors"
141
+ assert "errors" in last_call_kwargs
142
+ assert "status" in last_call_kwargs["errors"]
143
+ # Check that the error message mentions the missing key events
144
+ error_msg = last_call_kwargs["errors"]["status"]
145
+ assert "Cannot close this incident" in error_msg
146
+ assert "Missing key events" in error_msg
147
+
148
+ # Verify that incident update was NOT triggered
149
+ trigger_incident_workflow.assert_not_called()
150
+
151
+ @staticmethod
152
+ def test_cannot_close_from_postmortem_without_key_events(mocker: MockerFixture) -> None:
153
+ """Test that closing from POST_MORTEM is prevented when key events missing.
154
+
155
+ This tests a P1/P2 incident in POST_MORTEM trying to close but blocked
156
+ by missing key events.
157
+ """
158
+ # Create a user first
159
+ user = UserFactory.build()
160
+ user.save()
161
+
162
+ # Create a P1/P2 incident in POST_MORTEM status
163
+ incident = IncidentFactory.build(
164
+ _status=IncidentStatus.POST_MORTEM,
165
+ created_by=user,
166
+ )
167
+ incident.save()
168
+ # Mock can_be_closed to return False with MISSING_REQUIRED_KEY_EVENTS reason
169
+ mocker.patch.object(
170
+ type(incident),
171
+ "can_be_closed",
172
+ new_callable=PropertyMock,
173
+ return_value=(False, [("MISSING_REQUIRED_KEY_EVENTS", "Missing key events: detected, started")])
174
+ )
175
+
176
+ modal = UpdateStatusModal()
177
+ trigger_incident_workflow = mocker.patch.object(
178
+ modal, "_trigger_incident_workflow"
179
+ )
180
+
181
+ ack = MagicMock()
182
+ user = UserFactory.build()
183
+ user.save()
184
+
185
+ # Create a submission trying to close the incident
186
+ submission_copy = dict(valid_submission)
187
+ submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
188
+ "text": {"type": "plain_text", "text": "Closed", "emoji": True},
189
+ "value": "60",
190
+ }
191
+ submission_copy["view"]["private_metadata"] = str(incident.id)
192
+
193
+ modal.handle_modal_fn(
194
+ ack=ack, body=submission_copy, incident=incident, user=user
195
+ )
196
+
197
+ # Assert that ack was called with errors
198
+ assert ack.called
199
+ last_call_kwargs = ack.call_args.kwargs
200
+ assert "response_action" in last_call_kwargs
201
+ assert last_call_kwargs["response_action"] == "errors"
202
+ assert "errors" in last_call_kwargs
203
+ assert "status" in last_call_kwargs["errors"]
204
+ error_msg = last_call_kwargs["errors"]["status"]
205
+ assert "Cannot close this incident" in error_msg
206
+ assert "Missing key events" in error_msg
207
+
208
+ # Verify that incident update was NOT triggered
209
+ trigger_incident_workflow.assert_not_called()
210
+
211
+ @staticmethod
212
+ def test_cannot_close_p1_p2_without_postmortem(mocker: MockerFixture, priority_factory, environment_factory) -> None:
213
+ """Test that P1/P2 incidents in PRD cannot be closed directly from INVESTIGATING.
214
+
215
+ For P1/P2 incidents requiring post-mortem, although the form allows CLOSED as an option
216
+ from INVESTIGATING status, the can_be_closed validation should prevent closure with
217
+ an error message about needing to go through post-mortem.
218
+ """
219
+ # Create a user first
220
+ user = UserFactory.build()
221
+ user.save()
222
+
223
+ # Create P1 priority (needs_postmortem=True) and PRD environment
224
+ p1_priority = priority_factory(value=1, name="P1", needs_postmortem=True)
225
+ prd_environment = environment_factory(value="PRD", name="Production")
226
+
227
+ # Create a P1/P2 incident in INVESTIGATING status
228
+ # From INVESTIGATING, the form allows transitioning to CLOSED (but can_be_closed will block it)
229
+ incident = IncidentFactory.build(
230
+ _status=IncidentStatus.INVESTIGATING,
231
+ created_by=user,
232
+ priority=p1_priority,
233
+ environment=prd_environment,
234
+ )
235
+ incident.save()
236
+ # Mock can_be_closed to return False with STATUS_NOT_POST_MORTEM reason
237
+ mocker.patch.object(
238
+ type(incident),
239
+ "can_be_closed",
240
+ new_callable=PropertyMock,
241
+ return_value=(False, [("STATUS_NOT_POST_MORTEM", "Incident is not in PostMortem status, and needs one because of its priority and environment (P1/PRD).")])
242
+ )
243
+
244
+ modal = UpdateStatusModal()
245
+
246
+ # Mock handle_update_status_close_request to NOT show closure reason modal
247
+ # This allows the test to reach the can_be_closed validation
248
+ mocker.patch(
249
+ "firefighter.slack.views.modals.update_status.handle_update_status_close_request",
250
+ return_value=False
251
+ )
252
+
253
+ trigger_incident_workflow = mocker.patch.object(
254
+ modal, "_trigger_incident_workflow"
255
+ )
256
+
257
+ ack = MagicMock()
258
+ user = UserFactory.build()
259
+ user.save()
260
+
261
+ # Create a submission trying to close the incident
262
+ submission_copy = dict(valid_submission)
263
+ submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
264
+ "text": {"type": "plain_text", "text": "Closed", "emoji": True},
265
+ "value": "60",
266
+ }
267
+ submission_copy["view"]["private_metadata"] = str(incident.id)
268
+
269
+ modal.handle_modal_fn(
270
+ ack=ack, body=submission_copy, incident=incident, user=user
271
+ )
272
+
273
+ # Assert that ack was called with errors
274
+ assert ack.called
275
+ last_call_kwargs = ack.call_args.kwargs
276
+ assert "response_action" in last_call_kwargs
277
+ assert last_call_kwargs["response_action"] == "errors"
278
+ assert "errors" in last_call_kwargs
279
+ assert "status" in last_call_kwargs["errors"]
280
+ error_msg = last_call_kwargs["errors"]["status"]
281
+ assert "Cannot close this incident" in error_msg
282
+ assert "PostMortem status" in error_msg
283
+
284
+ # Verify that incident update was NOT triggered
285
+ trigger_incident_workflow.assert_not_called()
286
+
287
+ @staticmethod
288
+ def test_closure_reason_modal_shown_when_closing_from_investigating(mocker: MockerFixture) -> None:
289
+ """Test that closure reason modal is shown when trying to close from INVESTIGATING.
290
+
291
+ This tests that handle_update_status_close_request correctly shows the
292
+ closure reason modal and returns True, blocking the normal closure flow.
293
+ """
294
+ # Create an incident in INVESTIGATING status
295
+ incident = IncidentFactory.build(
296
+ _status=IncidentStatus.INVESTIGATING,
297
+ )
298
+
299
+ modal = UpdateStatusModal()
300
+
301
+ # Mock handle_update_status_close_request to return True (modal shown)
302
+ mock_handle_close = mocker.patch(
303
+ "firefighter.slack.views.modals.update_status.handle_update_status_close_request",
304
+ return_value=True
305
+ )
306
+
307
+ trigger_incident_workflow = mocker.patch.object(
308
+ modal, "_trigger_incident_workflow"
309
+ )
310
+
311
+ ack = MagicMock()
312
+ user = UserFactory.build()
313
+ user.save()
314
+
315
+ # Create a submission trying to close the incident
316
+ submission_copy = dict(valid_submission)
317
+ submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
318
+ "text": {"type": "plain_text", "text": "Closed", "emoji": True},
319
+ "value": "60",
320
+ }
321
+ submission_copy["view"]["private_metadata"] = str(incident.id)
322
+
323
+ modal.handle_modal_fn(
324
+ ack=ack, body=submission_copy, incident=incident, user=user
325
+ )
326
+
327
+ # Verify handle_update_status_close_request was called
328
+ mock_handle_close.assert_called_once_with(ack, submission_copy, incident, IncidentStatus.CLOSED)
329
+
330
+ # Verify that incident update was NOT triggered (because closure reason modal was shown)
331
+ trigger_incident_workflow.assert_not_called()
332
+
333
+ @staticmethod
334
+ def test_can_close_when_all_conditions_met(mocker: MockerFixture) -> None:
335
+ """Test that closing is allowed when all conditions are met."""
336
+ # Create a user first
337
+ user = UserFactory.build()
338
+ user.save()
339
+
340
+ # Create an incident in MITIGATED status with all conditions met
341
+ incident = IncidentFactory.build(
342
+ _status=IncidentStatus.MITIGATED,
343
+ created_by=user,
344
+ )
345
+ # IMPORTANT: Save the incident so it has an ID for the form to reference
346
+ incident.save()
347
+
348
+ # Mock can_be_closed to return True (all conditions met)
349
+ mocker.patch.object(
350
+ type(incident),
351
+ "can_be_closed",
352
+ new_callable=PropertyMock,
353
+ return_value=(True, [])
354
+ )
355
+
356
+ modal = UpdateStatusModal()
357
+ trigger_incident_workflow = mocker.patch.object(
358
+ modal, "_trigger_incident_workflow"
359
+ )
360
+
361
+ ack = MagicMock()
362
+
363
+ # Create a submission to close the incident
364
+ submission_copy = dict(valid_submission)
365
+ submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
366
+ "text": {"type": "plain_text", "text": "Closed", "emoji": True},
367
+ "value": "60",
368
+ }
369
+ submission_copy["view"]["private_metadata"] = str(incident.id)
370
+
371
+ modal.handle_modal_fn(
372
+ ack=ack, body=submission_copy, incident=incident, user=user
373
+ )
374
+
375
+ # Assert that ack was called successfully (no errors)
376
+ # The first call is the successful ack() without errors
377
+ first_call_kwargs = ack.call_args_list[0][1] if ack.call_args_list else ack.call_args.kwargs
378
+ assert first_call_kwargs == {} or "errors" not in first_call_kwargs
379
+
380
+ # Verify that incident update WAS triggered
381
+ trigger_incident_workflow.assert_called_once()
382
+
67
383
 
68
384
  valid_submission = {
69
385
  "type": "view_submission",
@@ -162,6 +478,14 @@ valid_submission = {
162
478
  },
163
479
  "value": "50",
164
480
  },
481
+ {
482
+ "text": {
483
+ "type": "plain_text",
484
+ "text": "Closed",
485
+ "emoji": True,
486
+ },
487
+ "value": "60",
488
+ },
165
489
  ],
166
490
  },
167
491
  },
@@ -0,0 +1,135 @@
1
+ """Test the modal utils module."""
2
+ from __future__ import annotations
3
+
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import pytest
7
+
8
+ from firefighter.incidents.enums import IncidentStatus
9
+ from firefighter.incidents.factories import IncidentFactory
10
+ from firefighter.slack.views.modals.utils import (
11
+ get_close_modal_view,
12
+ handle_close_modal_callback,
13
+ handle_update_status_close_request,
14
+ )
15
+
16
+
17
+ @pytest.mark.django_db
18
+ class TestModalUtils:
19
+ """Test modal utility functions."""
20
+
21
+ def test_get_close_modal_view_requires_reason(self):
22
+ """Test get_close_modal_view when closure reason is required."""
23
+ # Create incident in OPEN status (requires closure reason)
24
+ incident = IncidentFactory.create(_status=IncidentStatus.OPEN)
25
+ body = {}
26
+
27
+ with patch("firefighter.slack.views.modals.utils.UpdateStatusForm.requires_closure_reason", return_value=True), \
28
+ patch("firefighter.slack.views.modals.utils.modal_closure_reason.build_modal_fn") as mock_build:
29
+ mock_build.return_value = MagicMock()
30
+
31
+ result = get_close_modal_view(body, incident)
32
+
33
+ assert result is not None
34
+ mock_build.assert_called_once_with(body, incident)
35
+
36
+ def test_get_close_modal_view_no_reason_required(self):
37
+ """Test get_close_modal_view when closure reason is not required."""
38
+ # Create incident in POST_MORTEM status (doesn't require closure reason)
39
+ incident = IncidentFactory.create(_status=IncidentStatus.POST_MORTEM)
40
+ body = {}
41
+
42
+ with patch("firefighter.slack.views.modals.utils.UpdateStatusForm.requires_closure_reason", return_value=False):
43
+ result = get_close_modal_view(body, incident)
44
+
45
+ assert result is None
46
+
47
+ def test_handle_close_modal_callback_closure_reason(self):
48
+ """Test handle_close_modal_callback for closure reason modal."""
49
+ incident = IncidentFactory.create()
50
+ user = MagicMock()
51
+ ack = MagicMock()
52
+
53
+ body = {
54
+ "view": {
55
+ "callback_id": "incident_closure_reason"
56
+ }
57
+ }
58
+
59
+ with patch("firefighter.slack.views.modals.utils.modal_closure_reason.handle_modal_fn") as mock_handle:
60
+ mock_handle.return_value = True
61
+
62
+ result = handle_close_modal_callback(ack, body, incident, user)
63
+
64
+ assert result is True
65
+ mock_handle.assert_called_once_with(ack, body, incident, user)
66
+
67
+ def test_handle_close_modal_callback_normal_modal(self):
68
+ """Test handle_close_modal_callback for normal close modal."""
69
+ incident = IncidentFactory.create()
70
+ user = MagicMock()
71
+ ack = MagicMock()
72
+
73
+ body = {
74
+ "view": {
75
+ "callback_id": "incident_close" # Not closure reason
76
+ }
77
+ }
78
+
79
+ result = handle_close_modal_callback(ack, body, incident, user)
80
+
81
+ assert result is None
82
+
83
+ def test_handle_update_status_close_request_requires_reason(self):
84
+ """Test handle_update_status_close_request when reason is required."""
85
+ incident = IncidentFactory.create(_status=IncidentStatus.OPEN)
86
+ ack = MagicMock()
87
+ body = {}
88
+ target_status = IncidentStatus.CLOSED
89
+
90
+ with patch("firefighter.slack.views.modals.utils.UpdateStatusForm.requires_closure_reason", return_value=True), \
91
+ patch("firefighter.slack.views.modals.utils.modal_closure_reason.build_modal_fn") as mock_build:
92
+ mock_build.return_value = MagicMock()
93
+
94
+ result = handle_update_status_close_request(ack, body, incident, target_status)
95
+
96
+ assert result is True
97
+ ack.assert_called_once_with(response_action="push", view=mock_build.return_value)
98
+ mock_build.assert_called_once_with(body, incident)
99
+
100
+ def test_handle_update_status_close_request_no_reason_required(self):
101
+ """Test handle_update_status_close_request when reason is not required."""
102
+ incident = IncidentFactory.create(_status=IncidentStatus.POST_MORTEM)
103
+ ack = MagicMock()
104
+ body = {}
105
+ target_status = IncidentStatus.CLOSED
106
+
107
+ with patch("firefighter.slack.views.modals.utils.UpdateStatusForm.requires_closure_reason", return_value=False):
108
+ result = handle_update_status_close_request(ack, body, incident, target_status)
109
+
110
+ assert result is False
111
+ ack.assert_not_called()
112
+
113
+ def test_handle_update_status_close_request_non_close_status(self):
114
+ """Test handle_update_status_close_request for non-close status."""
115
+ incident = IncidentFactory.create()
116
+ ack = MagicMock()
117
+ body = {}
118
+ target_status = IncidentStatus.INVESTIGATING
119
+
120
+ result = handle_update_status_close_request(ack, body, incident, target_status)
121
+
122
+ assert result is False
123
+ ack.assert_not_called()
124
+
125
+ def test_handle_close_modal_callback_missing_view(self):
126
+ """Test handle_close_modal_callback with missing view in body."""
127
+ incident = IncidentFactory.create()
128
+ user = MagicMock()
129
+ ack = MagicMock()
130
+
131
+ body = {} # No view key
132
+
133
+ result = handle_close_modal_callback(ack, body, incident, user)
134
+
135
+ assert result is None
@@ -1,139 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- from typing import TYPE_CHECKING
5
-
6
- from slack_sdk.models.blocks.basic_components import MarkdownTextObject
7
- from slack_sdk.models.blocks.blocks import ContextBlock
8
-
9
- from firefighter.raid.forms import (
10
- CreateNormalCustomerIncidentForm,
11
- CreateRaidDocumentationRequestIncidentForm,
12
- CreateRaidFeatureRequestIncidentForm,
13
- CreateRaidInternalIncidentForm,
14
- RaidCreateIncidentSellerForm,
15
- )
16
- from firefighter.slack.views.modals.opening.set_details import SetIncidentDetails
17
-
18
- if TYPE_CHECKING:
19
- from firefighter.slack.views.modals.base_modal.form_utils import (
20
- SlackFormAttributesDict,
21
- )
22
-
23
- logger = logging.getLogger(__name__)
24
-
25
- slack_fields: SlackFormAttributesDict = {
26
- "title": {
27
- "input": {
28
- "multiline": False,
29
- "placeholder": "Summary of the issue.",
30
- },
31
- "block": {"hint": None},
32
- },
33
- "description": {
34
- "input": {
35
- "multiline": True,
36
- "placeholder": "Explain your issue in English giving as much details as possible. It helps people handling the issue. \nThis description can be edited later.",
37
- },
38
- "block": {"hint": None},
39
- },
40
- "suggested_team_routing": {
41
- "widget": {
42
- "post_block": ContextBlock(
43
- elements=[
44
- MarkdownTextObject(
45
- text="Feature Team or Train that should own the issue. If you don't know access <https://manomano.atlassian.net/wiki/spaces/QRAFT/pages/3970335291/Teams+and+owners|here> for guidance."
46
- ),
47
- ]
48
- )
49
- },
50
- },
51
- "incident_category": {
52
- "input": {
53
- "placeholder": "Select incident category",
54
- },
55
- },
56
- }
57
-
58
-
59
- class CreateRaidCustomerIncidentFormSlack(CreateNormalCustomerIncidentForm):
60
- slack_fields: SlackFormAttributesDict = slack_fields
61
-
62
-
63
- class OpeningRaidCustomerModal(SetIncidentDetails[CreateRaidCustomerIncidentFormSlack]):
64
- open_action: str = "open_incident_raid_customer_request"
65
- push_action: str = "push_raid_customer_request"
66
- callback_id: str = "open_incident_raid_customer_request"
67
- id = "raid_customer_request"
68
- title = "New Customer Incident"
69
-
70
- form_class = CreateRaidCustomerIncidentFormSlack
71
-
72
-
73
- class CreateRaidDocumentationRequestIncidentFormSlack(
74
- CreateRaidDocumentationRequestIncidentForm
75
- ):
76
- slack_fields: SlackFormAttributesDict = slack_fields
77
-
78
-
79
- class CreateRaidFeatureRequestIncidentFormSlack(CreateRaidFeatureRequestIncidentForm):
80
- slack_fields: SlackFormAttributesDict = slack_fields
81
-
82
-
83
- class OpeningRaidFeatureRequestModal(
84
- SetIncidentDetails[CreateRaidFeatureRequestIncidentFormSlack]
85
- ):
86
- open_action: str = "open_incident_raid_feature_request"
87
- push_action: str = "push_raid_feature_request"
88
- callback_id: str = "open_incident_raid_feature_request"
89
-
90
- title = "New Feature Request"
91
- form_class = CreateRaidFeatureRequestIncidentFormSlack
92
-
93
-
94
- class OpeningRaidDocumentationRequestModal(
95
- SetIncidentDetails[CreateRaidDocumentationRequestIncidentFormSlack]
96
- ):
97
- open_action: str = "open_incident_raid_documentation_request"
98
- push_action: str = "push_raid_documentation_request"
99
- callback_id: str = "open_incident_raid_documentation_request"
100
-
101
- title = "New Documentation Request"
102
- form_class = CreateRaidDocumentationRequestIncidentFormSlack
103
-
104
-
105
- class CreateRaidInternalIncidentFormSlack(CreateRaidInternalIncidentForm):
106
- slack_fields: SlackFormAttributesDict = slack_fields
107
-
108
-
109
- class OpeningRaidInternalModal(SetIncidentDetails[CreateRaidInternalIncidentFormSlack]):
110
- open_action: str = "open_incident_raid_internal_request"
111
- push_action: str = "push_raid_internal_request"
112
- callback_id: str = "open_incident_raid_internal_request"
113
-
114
- title = "New Internal Incident"
115
-
116
- form_class = CreateRaidInternalIncidentFormSlack
117
-
118
-
119
- class CreateRaidSellerIncidentFormSlack(RaidCreateIncidentSellerForm):
120
- slack_fields: SlackFormAttributesDict = slack_fields
121
-
122
-
123
- class OpeningRaidSellerModal(SetIncidentDetails[CreateRaidSellerIncidentFormSlack]):
124
- open_action: str = "open_incident_raid_seller_request"
125
- push_action: str = "push_raid_seller_request"
126
- callback_id: str = "open_incident_raid_seller_request"
127
-
128
- title = "New Seller Incident"
129
- form_class = CreateRaidSellerIncidentFormSlack
130
-
131
-
132
- # Instantiate all the modals to register actions
133
- _modals = [
134
- raid_seller := OpeningRaidSellerModal(),
135
- raid_internal := OpeningRaidInternalModal(),
136
- raid_customer := OpeningRaidCustomerModal(),
137
- raid_feature := OpeningRaidFeatureRequestModal(),
138
- raid_documentation := OpeningRaidDocumentationRequestModal(),
139
- ]