firefighter-incident 0.0.22__py3-none-any.whl → 0.0.23__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.
- firefighter/_version.py +2 -2
- firefighter/api/serializers.py +18 -0
- firefighter/api/views/incidents.py +3 -0
- firefighter/confluence/models.py +66 -6
- firefighter/confluence/signals/incident_updated.py +8 -26
- firefighter/firefighter/settings/components/jira_app.py +33 -0
- firefighter/incidents/admin.py +3 -0
- firefighter/incidents/models/impact.py +3 -5
- firefighter/incidents/models/incident.py +24 -9
- firefighter/incidents/views/views.py +2 -0
- firefighter/jira_app/admin.py +15 -1
- firefighter/jira_app/apps.py +3 -0
- firefighter/jira_app/client.py +151 -3
- firefighter/jira_app/management/__init__.py +1 -0
- firefighter/jira_app/management/commands/__init__.py +1 -0
- firefighter/jira_app/migrations/0002_add_jira_postmortem_model.py +71 -0
- firefighter/jira_app/models.py +50 -0
- firefighter/jira_app/service_postmortem.py +292 -0
- firefighter/jira_app/signals/__init__.py +10 -0
- firefighter/jira_app/signals/incident_key_events_updated.py +88 -0
- firefighter/jira_app/signals/postmortem_created.py +155 -0
- firefighter/jira_app/templates/jira/postmortem/impact.txt +12 -0
- firefighter/jira_app/templates/jira/postmortem/incident_summary.txt +17 -0
- firefighter/jira_app/templates/jira/postmortem/mitigation_actions.txt +9 -0
- firefighter/jira_app/templates/jira/postmortem/root_causes.txt +12 -0
- firefighter/jira_app/templates/jira/postmortem/timeline.txt +7 -0
- firefighter/raid/signals/incident_updated.py +31 -11
- firefighter/slack/messages/slack_messages.py +39 -3
- firefighter/slack/signals/postmortem_created.py +51 -3
- firefighter/slack/views/modals/closure_reason.py +15 -0
- firefighter/slack/views/modals/key_event_message.py +9 -0
- firefighter/slack/views/modals/postmortem.py +32 -40
- firefighter/slack/views/modals/update_status.py +7 -1
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/RECORD +50 -31
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/WHEEL +1 -1
- firefighter_tests/test_api/test_renderer.py +41 -0
- firefighter_tests/test_incidents/test_models/test_incident_model.py +29 -0
- firefighter_tests/test_jira_app/test_incident_key_events_sync.py +112 -0
- firefighter_tests/test_jira_app/test_models.py +138 -0
- firefighter_tests/test_jira_app/test_postmortem_issue_link.py +201 -0
- firefighter_tests/test_jira_app/test_postmortem_service.py +416 -0
- firefighter_tests/test_jira_app/test_timeline_template.py +135 -0
- firefighter_tests/test_raid/test_raid_signals.py +50 -8
- firefighter_tests/test_slack/messages/test_slack_messages.py +112 -23
- firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +18 -2
- firefighter_tests/test_slack/views/modals/test_key_event_message.py +30 -0
- firefighter_tests/test_slack/views/modals/test_update_status.py +161 -129
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,7 +9,7 @@ from pytest_mock import MockerFixture
|
|
|
9
9
|
|
|
10
10
|
from firefighter.incidents.enums import IncidentStatus
|
|
11
11
|
from firefighter.incidents.factories import IncidentFactory, UserFactory
|
|
12
|
-
from firefighter.incidents.models import Incident,
|
|
12
|
+
from firefighter.incidents.models import Incident, MilestoneType
|
|
13
13
|
from firefighter.slack.views import UpdateStatusModal
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
@@ -64,7 +64,9 @@ class TestUpdateStatusModal:
|
|
|
64
64
|
# Create a valid submission that transitions from OPEN to INVESTIGATING (valid workflow)
|
|
65
65
|
valid_submission_copy = dict(valid_submission)
|
|
66
66
|
# Change status to INVESTIGATING (20) which is valid from OPEN
|
|
67
|
-
valid_submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
67
|
+
valid_submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
68
|
+
"selected_option"
|
|
69
|
+
] = {
|
|
68
70
|
"text": {"type": "plain_text", "text": "Investigating", "emoji": True},
|
|
69
71
|
"value": "20",
|
|
70
72
|
}
|
|
@@ -78,31 +80,60 @@ class TestUpdateStatusModal:
|
|
|
78
80
|
trigger_incident_workflow.assert_called_once()
|
|
79
81
|
|
|
80
82
|
@staticmethod
|
|
81
|
-
def test_cannot_close_without_required_key_events(
|
|
83
|
+
def test_cannot_close_without_required_key_events(
|
|
84
|
+
mocker: MockerFixture, priority_factory
|
|
85
|
+
) -> None:
|
|
82
86
|
"""Test that closing is prevented when required key events are missing.
|
|
83
87
|
|
|
84
88
|
This tests the scenario where a P3+ incident (no postmortem needed) is in
|
|
85
89
|
MITIGATED status and tries to close, but missing key events blocks it.
|
|
90
|
+
|
|
91
|
+
This test does NOT mock can_be_closed - it uses real milestone validation.
|
|
86
92
|
"""
|
|
93
|
+
# Ensure required milestone types exist (they will be missing from the incident)
|
|
94
|
+
MilestoneType.objects.update_or_create(
|
|
95
|
+
event_type="detected",
|
|
96
|
+
defaults={
|
|
97
|
+
"name": "Detected",
|
|
98
|
+
"summary": "When the incident was first detected",
|
|
99
|
+
"required": True,
|
|
100
|
+
"user_editable": True,
|
|
101
|
+
"asked_for": True,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
MilestoneType.objects.update_or_create(
|
|
105
|
+
event_type="started",
|
|
106
|
+
defaults={
|
|
107
|
+
"name": "Started",
|
|
108
|
+
"summary": "When work started on the incident",
|
|
109
|
+
"required": True,
|
|
110
|
+
"user_editable": True,
|
|
111
|
+
"asked_for": True,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
|
|
87
115
|
# Create a user first
|
|
88
116
|
user = UserFactory.build()
|
|
89
117
|
user.save()
|
|
90
118
|
|
|
91
|
-
# Create
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
incident
|
|
119
|
+
# Create P3 priority (needs_postmortem=False)
|
|
120
|
+
p3_priority = priority_factory(value=3, name="P3", needs_postmortem=False)
|
|
121
|
+
|
|
122
|
+
# Create a P3 incident in MITIGATED status (can go directly to CLOSED)
|
|
123
|
+
# This incident will have missing milestones (detected, started)
|
|
124
|
+
incident = IncidentFactory.create(
|
|
95
125
|
_status=IncidentStatus.MITIGATED,
|
|
96
126
|
created_by=user,
|
|
97
127
|
priority=p3_priority,
|
|
98
128
|
)
|
|
99
|
-
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
129
|
+
|
|
130
|
+
# Verify that can_be_closed returns False due to missing milestones (real check, no mock)
|
|
131
|
+
can_close, reasons = incident.can_be_closed
|
|
132
|
+
assert can_close is False, (
|
|
133
|
+
f"Incident should not be closable without required milestones. Got: {can_close}, reasons: {reasons}"
|
|
134
|
+
)
|
|
135
|
+
assert any("MISSING_REQUIRED_KEY_EVENTS" in reason[0] for reason in reasons), (
|
|
136
|
+
f"Expected MISSING_REQUIRED_KEY_EVENTS in reasons, got: {reasons}"
|
|
106
137
|
)
|
|
107
138
|
|
|
108
139
|
modal = UpdateStatusModal()
|
|
@@ -111,13 +142,13 @@ class TestUpdateStatusModal:
|
|
|
111
142
|
)
|
|
112
143
|
|
|
113
144
|
ack = MagicMock()
|
|
114
|
-
user = UserFactory.build()
|
|
115
|
-
user.save()
|
|
116
145
|
|
|
117
146
|
# Create a submission trying to close the incident
|
|
118
147
|
submission_copy = dict(valid_submission)
|
|
119
148
|
# Change status to CLOSED (60)
|
|
120
|
-
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
149
|
+
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
150
|
+
"selected_option"
|
|
151
|
+
] = {
|
|
121
152
|
"text": {"type": "plain_text", "text": "Closed", "emoji": True},
|
|
122
153
|
"value": "60",
|
|
123
154
|
}
|
|
@@ -129,44 +160,84 @@ class TestUpdateStatusModal:
|
|
|
129
160
|
)
|
|
130
161
|
|
|
131
162
|
# Assert that ack was called with errors (may be 1 or 2 calls depending on form validation)
|
|
132
|
-
assert ack.called
|
|
163
|
+
assert ack.called, "ack should have been called"
|
|
133
164
|
# Check the last call (the error response)
|
|
134
165
|
last_call_kwargs = ack.call_args.kwargs
|
|
135
|
-
assert "response_action" in last_call_kwargs
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
assert "
|
|
166
|
+
assert "response_action" in last_call_kwargs, (
|
|
167
|
+
f"Expected 'response_action' in ack, got: {last_call_kwargs}"
|
|
168
|
+
)
|
|
169
|
+
assert last_call_kwargs["response_action"] == "errors", (
|
|
170
|
+
f"Expected response_action='errors', got: {last_call_kwargs.get('response_action')}"
|
|
171
|
+
)
|
|
172
|
+
assert "errors" in last_call_kwargs, (
|
|
173
|
+
f"Expected 'errors' in ack, got: {last_call_kwargs}"
|
|
174
|
+
)
|
|
175
|
+
assert "status" in last_call_kwargs["errors"], (
|
|
176
|
+
f"Expected 'status' in errors, got: {last_call_kwargs.get('errors')}"
|
|
177
|
+
)
|
|
139
178
|
# Check that the error message mentions the missing key events
|
|
140
179
|
error_msg = last_call_kwargs["errors"]["status"]
|
|
141
|
-
assert "Cannot close this incident" in error_msg
|
|
142
|
-
|
|
180
|
+
assert "Cannot close this incident" in error_msg, (
|
|
181
|
+
f"Expected closure error, got: {error_msg}"
|
|
182
|
+
)
|
|
183
|
+
assert "key events" in error_msg.lower(), (
|
|
184
|
+
f"Expected 'key events' in error, got: {error_msg}"
|
|
185
|
+
)
|
|
143
186
|
|
|
144
187
|
# Verify that incident update was NOT triggered
|
|
145
188
|
trigger_incident_workflow.assert_not_called()
|
|
146
189
|
|
|
147
190
|
@staticmethod
|
|
148
|
-
def test_cannot_close_from_postmortem_without_key_events(
|
|
191
|
+
def test_cannot_close_from_postmortem_without_key_events(
|
|
192
|
+
mocker: MockerFixture,
|
|
193
|
+
) -> None:
|
|
149
194
|
"""Test that closing from POST_MORTEM is prevented when key events missing.
|
|
150
195
|
|
|
151
196
|
This tests a P1/P2 incident in POST_MORTEM trying to close but blocked
|
|
152
197
|
by missing key events.
|
|
198
|
+
|
|
199
|
+
This test does NOT mock can_be_closed - it uses real milestone validation.
|
|
153
200
|
"""
|
|
201
|
+
# Ensure required milestone types exist (they will be missing from the incident)
|
|
202
|
+
MilestoneType.objects.update_or_create(
|
|
203
|
+
event_type="detected",
|
|
204
|
+
defaults={
|
|
205
|
+
"name": "Detected",
|
|
206
|
+
"summary": "When the incident was first detected",
|
|
207
|
+
"required": True,
|
|
208
|
+
"user_editable": True,
|
|
209
|
+
"asked_for": True,
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
MilestoneType.objects.update_or_create(
|
|
213
|
+
event_type="started",
|
|
214
|
+
defaults={
|
|
215
|
+
"name": "Started",
|
|
216
|
+
"summary": "When work started on the incident",
|
|
217
|
+
"required": True,
|
|
218
|
+
"user_editable": True,
|
|
219
|
+
"asked_for": True,
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
154
223
|
# Create a user first
|
|
155
224
|
user = UserFactory.build()
|
|
156
225
|
user.save()
|
|
157
226
|
|
|
158
227
|
# Create a P1/P2 incident in POST_MORTEM status
|
|
159
|
-
incident
|
|
228
|
+
# This incident will have missing milestones (detected, started)
|
|
229
|
+
incident = IncidentFactory.create(
|
|
160
230
|
_status=IncidentStatus.POST_MORTEM,
|
|
161
231
|
created_by=user,
|
|
162
232
|
)
|
|
163
|
-
|
|
164
|
-
#
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
"
|
|
168
|
-
|
|
169
|
-
|
|
233
|
+
|
|
234
|
+
# Verify that can_be_closed returns False due to missing milestones
|
|
235
|
+
can_close, reasons = incident.can_be_closed
|
|
236
|
+
assert can_close is False, (
|
|
237
|
+
"Incident should not be closable without required milestones"
|
|
238
|
+
)
|
|
239
|
+
assert any("MISSING_REQUIRED_KEY_EVENTS" in reason[0] for reason in reasons), (
|
|
240
|
+
f"Expected MISSING_REQUIRED_KEY_EVENTS in reasons, got: {reasons}"
|
|
170
241
|
)
|
|
171
242
|
|
|
172
243
|
modal = UpdateStatusModal()
|
|
@@ -175,12 +246,12 @@ class TestUpdateStatusModal:
|
|
|
175
246
|
)
|
|
176
247
|
|
|
177
248
|
ack = MagicMock()
|
|
178
|
-
user = UserFactory.build()
|
|
179
|
-
user.save()
|
|
180
249
|
|
|
181
250
|
# Create a submission trying to close the incident
|
|
182
251
|
submission_copy = dict(valid_submission)
|
|
183
|
-
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
252
|
+
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
253
|
+
"selected_option"
|
|
254
|
+
] = {
|
|
184
255
|
"text": {"type": "plain_text", "text": "Closed", "emoji": True},
|
|
185
256
|
"value": "60",
|
|
186
257
|
}
|
|
@@ -191,97 +262,35 @@ class TestUpdateStatusModal:
|
|
|
191
262
|
)
|
|
192
263
|
|
|
193
264
|
# Assert that ack was called with errors
|
|
194
|
-
assert ack.called
|
|
265
|
+
assert ack.called, "ack should have been called"
|
|
195
266
|
last_call_kwargs = ack.call_args.kwargs
|
|
196
|
-
assert "response_action" in last_call_kwargs
|
|
197
|
-
|
|
198
|
-
assert "errors" in last_call_kwargs
|
|
199
|
-
assert "status" in last_call_kwargs["errors"]
|
|
200
|
-
error_msg = last_call_kwargs["errors"]["status"]
|
|
201
|
-
assert "Cannot close this incident" in error_msg
|
|
202
|
-
assert "Missing key events" in error_msg
|
|
203
|
-
|
|
204
|
-
# Verify that incident update was NOT triggered
|
|
205
|
-
trigger_incident_workflow.assert_not_called()
|
|
206
|
-
|
|
207
|
-
@staticmethod
|
|
208
|
-
def test_cannot_close_p1_p2_without_postmortem(mocker: MockerFixture, priority_factory, environment_factory) -> None:
|
|
209
|
-
"""Test that P1/P2 incidents in PRD cannot be closed directly from INVESTIGATING.
|
|
210
|
-
|
|
211
|
-
For P1/P2 incidents requiring post-mortem, although the form allows CLOSED as an option
|
|
212
|
-
from INVESTIGATING status, the can_be_closed validation should prevent closure with
|
|
213
|
-
an error message about needing to go through post-mortem.
|
|
214
|
-
"""
|
|
215
|
-
# Create a user first
|
|
216
|
-
user = UserFactory.build()
|
|
217
|
-
user.save()
|
|
218
|
-
|
|
219
|
-
# Create P1 priority (needs_postmortem=True) and PRD environment
|
|
220
|
-
p1_priority = priority_factory(value=1, name="P1", needs_postmortem=True)
|
|
221
|
-
prd_environment = environment_factory(value="PRD", name="Production")
|
|
222
|
-
|
|
223
|
-
# Create a P1/P2 incident in INVESTIGATING status
|
|
224
|
-
# From INVESTIGATING, the form allows transitioning to CLOSED (but can_be_closed will block it)
|
|
225
|
-
incident = IncidentFactory.build(
|
|
226
|
-
_status=IncidentStatus.INVESTIGATING,
|
|
227
|
-
created_by=user,
|
|
228
|
-
priority=p1_priority,
|
|
229
|
-
environment=prd_environment,
|
|
230
|
-
)
|
|
231
|
-
incident.save()
|
|
232
|
-
# Mock can_be_closed to return False with STATUS_NOT_POST_MORTEM reason
|
|
233
|
-
mocker.patch.object(
|
|
234
|
-
type(incident),
|
|
235
|
-
"can_be_closed",
|
|
236
|
-
new_callable=PropertyMock,
|
|
237
|
-
return_value=(False, [("STATUS_NOT_POST_MORTEM", "Incident is not in PostMortem status, and needs one because of its priority and environment (P1/PRD).")])
|
|
267
|
+
assert "response_action" in last_call_kwargs, (
|
|
268
|
+
f"Expected 'response_action' in ack call, got: {last_call_kwargs}"
|
|
238
269
|
)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
# Mock handle_update_status_close_request to NOT show closure reason modal
|
|
243
|
-
# This allows the test to reach the can_be_closed validation
|
|
244
|
-
mocker.patch(
|
|
245
|
-
"firefighter.slack.views.modals.update_status.handle_update_status_close_request",
|
|
246
|
-
return_value=False
|
|
270
|
+
assert last_call_kwargs["response_action"] == "errors", (
|
|
271
|
+
f"Expected response_action='errors', got: {last_call_kwargs.get('response_action')}"
|
|
247
272
|
)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
modal, "_trigger_incident_workflow"
|
|
273
|
+
assert "errors" in last_call_kwargs, (
|
|
274
|
+
f"Expected 'errors' in ack call, got: {last_call_kwargs}"
|
|
251
275
|
)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
user = UserFactory.build()
|
|
255
|
-
user.save()
|
|
256
|
-
|
|
257
|
-
# Create a submission trying to close the incident
|
|
258
|
-
submission_copy = dict(valid_submission)
|
|
259
|
-
submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
|
|
260
|
-
"text": {"type": "plain_text", "text": "Closed", "emoji": True},
|
|
261
|
-
"value": "60",
|
|
262
|
-
}
|
|
263
|
-
submission_copy["view"]["private_metadata"] = str(incident.id)
|
|
264
|
-
|
|
265
|
-
modal.handle_modal_fn(
|
|
266
|
-
ack=ack, body=submission_copy, incident=incident, user=user
|
|
276
|
+
assert "status" in last_call_kwargs["errors"], (
|
|
277
|
+
f"Expected 'status' in errors, got: {last_call_kwargs.get('errors')}"
|
|
267
278
|
)
|
|
268
|
-
|
|
269
|
-
# Assert that ack was called with errors
|
|
270
|
-
assert ack.called
|
|
271
|
-
last_call_kwargs = ack.call_args.kwargs
|
|
272
|
-
assert "response_action" in last_call_kwargs
|
|
273
|
-
assert last_call_kwargs["response_action"] == "errors"
|
|
274
|
-
assert "errors" in last_call_kwargs
|
|
275
|
-
assert "status" in last_call_kwargs["errors"]
|
|
276
279
|
error_msg = last_call_kwargs["errors"]["status"]
|
|
277
|
-
assert "Cannot close this incident" in error_msg
|
|
278
|
-
|
|
280
|
+
assert "Cannot close this incident" in error_msg, (
|
|
281
|
+
f"Expected closure error message, got: {error_msg}"
|
|
282
|
+
)
|
|
283
|
+
assert "key events" in error_msg.lower(), (
|
|
284
|
+
f"Expected 'key events' in error message, got: {error_msg}"
|
|
285
|
+
)
|
|
279
286
|
|
|
280
287
|
# Verify that incident update was NOT triggered
|
|
281
288
|
trigger_incident_workflow.assert_not_called()
|
|
282
289
|
|
|
283
290
|
@staticmethod
|
|
284
|
-
def test_closure_reason_modal_shown_when_closing_from_investigating(
|
|
291
|
+
def test_closure_reason_modal_shown_when_closing_from_investigating(
|
|
292
|
+
mocker: MockerFixture,
|
|
293
|
+
) -> None:
|
|
285
294
|
"""Test that closure reason modal is shown when trying to close from INVESTIGATING.
|
|
286
295
|
|
|
287
296
|
This tests that handle_update_status_close_request correctly shows the
|
|
@@ -297,7 +306,7 @@ class TestUpdateStatusModal:
|
|
|
297
306
|
# Mock handle_update_status_close_request to return True (modal shown)
|
|
298
307
|
mock_handle_close = mocker.patch(
|
|
299
308
|
"firefighter.slack.views.modals.update_status.handle_update_status_close_request",
|
|
300
|
-
return_value=True
|
|
309
|
+
return_value=True,
|
|
301
310
|
)
|
|
302
311
|
|
|
303
312
|
trigger_incident_workflow = mocker.patch.object(
|
|
@@ -310,7 +319,9 @@ class TestUpdateStatusModal:
|
|
|
310
319
|
|
|
311
320
|
# Create a submission trying to close the incident
|
|
312
321
|
submission_copy = dict(valid_submission)
|
|
313
|
-
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
322
|
+
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
323
|
+
"selected_option"
|
|
324
|
+
] = {
|
|
314
325
|
"text": {"type": "plain_text", "text": "Closed", "emoji": True},
|
|
315
326
|
"value": "60",
|
|
316
327
|
}
|
|
@@ -321,13 +332,17 @@ class TestUpdateStatusModal:
|
|
|
321
332
|
)
|
|
322
333
|
|
|
323
334
|
# Verify handle_update_status_close_request was called
|
|
324
|
-
mock_handle_close.assert_called_once_with(
|
|
335
|
+
mock_handle_close.assert_called_once_with(
|
|
336
|
+
ack, submission_copy, incident, IncidentStatus.CLOSED
|
|
337
|
+
)
|
|
325
338
|
|
|
326
339
|
# Verify that incident update was NOT triggered (because closure reason modal was shown)
|
|
327
340
|
trigger_incident_workflow.assert_not_called()
|
|
328
341
|
|
|
329
342
|
@staticmethod
|
|
330
|
-
def test_can_close_when_all_conditions_met(
|
|
343
|
+
def test_can_close_when_all_conditions_met(
|
|
344
|
+
mocker: MockerFixture, priority_factory, environment_factory
|
|
345
|
+
) -> None:
|
|
331
346
|
"""Test that closing is allowed when all conditions are met for P3+ incidents."""
|
|
332
347
|
# Create a user first
|
|
333
348
|
user = UserFactory.build()
|
|
@@ -352,7 +367,7 @@ class TestUpdateStatusModal:
|
|
|
352
367
|
type(incident),
|
|
353
368
|
"can_be_closed",
|
|
354
369
|
new_callable=PropertyMock,
|
|
355
|
-
return_value=(True, [])
|
|
370
|
+
return_value=(True, []),
|
|
356
371
|
)
|
|
357
372
|
|
|
358
373
|
modal = UpdateStatusModal()
|
|
@@ -364,7 +379,9 @@ class TestUpdateStatusModal:
|
|
|
364
379
|
|
|
365
380
|
# Create a submission to close the incident
|
|
366
381
|
submission_copy = dict(valid_submission)
|
|
367
|
-
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
382
|
+
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
383
|
+
"selected_option"
|
|
384
|
+
] = {
|
|
368
385
|
"text": {"type": "plain_text", "text": "Closed", "emoji": True},
|
|
369
386
|
"value": "60",
|
|
370
387
|
}
|
|
@@ -376,14 +393,18 @@ class TestUpdateStatusModal:
|
|
|
376
393
|
|
|
377
394
|
# Assert that ack was called successfully (no errors)
|
|
378
395
|
# The first call is the successful ack() without errors
|
|
379
|
-
first_call_kwargs =
|
|
396
|
+
first_call_kwargs = (
|
|
397
|
+
ack.call_args_list[0][1] if ack.call_args_list else ack.call_args.kwargs
|
|
398
|
+
)
|
|
380
399
|
assert first_call_kwargs == {} or "errors" not in first_call_kwargs
|
|
381
400
|
|
|
382
401
|
# Verify that incident update WAS triggered
|
|
383
402
|
trigger_incident_workflow.assert_called_once()
|
|
384
403
|
|
|
385
404
|
@staticmethod
|
|
386
|
-
def test_can_update_priority_without_changing_status(
|
|
405
|
+
def test_can_update_priority_without_changing_status(
|
|
406
|
+
mocker: MockerFixture, priority_factory
|
|
407
|
+
) -> None:
|
|
387
408
|
"""Test that priority can be updated without changing status.
|
|
388
409
|
|
|
389
410
|
This reproduces the bug where trying to update only the priority of a P4
|
|
@@ -420,12 +441,16 @@ class TestUpdateStatusModal:
|
|
|
420
441
|
# The status field will have MITIGATED (40) as initial value, but it's not in the available choices
|
|
421
442
|
submission_copy = dict(valid_submission)
|
|
422
443
|
# Status unchanged - keeps MITIGATED (40)
|
|
423
|
-
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
444
|
+
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
445
|
+
"selected_option"
|
|
446
|
+
] = {
|
|
424
447
|
"text": {"type": "plain_text", "text": "Mitigated", "emoji": True},
|
|
425
448
|
"value": "40", # This should cause validation error with current code
|
|
426
449
|
}
|
|
427
450
|
# Change priority to P4
|
|
428
|
-
submission_copy["view"]["state"]["values"]["priority"]["priority"][
|
|
451
|
+
submission_copy["view"]["state"]["values"]["priority"]["priority"][
|
|
452
|
+
"selected_option"
|
|
453
|
+
] = {
|
|
429
454
|
"text": {"type": "plain_text", "text": "P4", "emoji": True},
|
|
430
455
|
"value": str(p4_priority.id),
|
|
431
456
|
}
|
|
@@ -437,9 +462,12 @@ class TestUpdateStatusModal:
|
|
|
437
462
|
|
|
438
463
|
# Assert that ack was called successfully WITHOUT errors
|
|
439
464
|
# With the bug, this would fail with "Select a valid choice. 40 is not one of the available choices"
|
|
440
|
-
first_call_kwargs =
|
|
441
|
-
|
|
465
|
+
first_call_kwargs = (
|
|
466
|
+
ack.call_args_list[0][1] if ack.call_args_list else ack.call_args.kwargs
|
|
467
|
+
)
|
|
468
|
+
assert first_call_kwargs == {} or "errors" not in first_call_kwargs, (
|
|
442
469
|
f"Should allow updating priority without changing status. Got errors: {first_call_kwargs.get('errors')}"
|
|
470
|
+
)
|
|
443
471
|
|
|
444
472
|
# Verify that incident update WAS triggered (priority changed)
|
|
445
473
|
trigger_incident_workflow.assert_called_once()
|
|
@@ -639,7 +667,11 @@ valid_submission = {
|
|
|
639
667
|
{
|
|
640
668
|
"type": "input",
|
|
641
669
|
"block_id": "incident_category",
|
|
642
|
-
"label": {
|
|
670
|
+
"label": {
|
|
671
|
+
"type": "plain_text",
|
|
672
|
+
"text": "Issue category",
|
|
673
|
+
"emoji": True,
|
|
674
|
+
},
|
|
643
675
|
"optional": False,
|
|
644
676
|
"dispatch_action": False,
|
|
645
677
|
"element": {
|
{firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|