firefighter-incident 0.0.21__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/components/avatar/avatar.html +2 -2
- firefighter/components/avatar/avatar.py +2 -2
- 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.21.dist-info → firefighter_incident-0.0.23.dist-info}/METADATA +2 -1
- {firefighter_incident-0.0.21.dist-info → firefighter_incident-0.0.23.dist-info}/RECORD +52 -33
- {firefighter_incident-0.0.21.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 -133
- {firefighter_incident-0.0.21.dist-info → firefighter_incident-0.0.23.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.21.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,35 +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
|
-
|
|
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(
|
|
93
125
|
_status=IncidentStatus.MITIGATED,
|
|
94
126
|
created_by=user,
|
|
127
|
+
priority=p3_priority,
|
|
95
128
|
)
|
|
96
|
-
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"
|
|
101
|
-
new_callable=PropertyMock,
|
|
102
|
-
return_value=False
|
|
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}"
|
|
103
134
|
)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
type(incident),
|
|
107
|
-
"can_be_closed",
|
|
108
|
-
new_callable=PropertyMock,
|
|
109
|
-
return_value=(False, [("MISSING_REQUIRED_KEY_EVENTS", "Missing key events: detected, started")])
|
|
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}"
|
|
110
137
|
)
|
|
111
138
|
|
|
112
139
|
modal = UpdateStatusModal()
|
|
@@ -115,13 +142,13 @@ class TestUpdateStatusModal:
|
|
|
115
142
|
)
|
|
116
143
|
|
|
117
144
|
ack = MagicMock()
|
|
118
|
-
user = UserFactory.build()
|
|
119
|
-
user.save()
|
|
120
145
|
|
|
121
146
|
# Create a submission trying to close the incident
|
|
122
147
|
submission_copy = dict(valid_submission)
|
|
123
148
|
# Change status to CLOSED (60)
|
|
124
|
-
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
149
|
+
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
150
|
+
"selected_option"
|
|
151
|
+
] = {
|
|
125
152
|
"text": {"type": "plain_text", "text": "Closed", "emoji": True},
|
|
126
153
|
"value": "60",
|
|
127
154
|
}
|
|
@@ -133,44 +160,84 @@ class TestUpdateStatusModal:
|
|
|
133
160
|
)
|
|
134
161
|
|
|
135
162
|
# Assert that ack was called with errors (may be 1 or 2 calls depending on form validation)
|
|
136
|
-
assert ack.called
|
|
163
|
+
assert ack.called, "ack should have been called"
|
|
137
164
|
# Check the last call (the error response)
|
|
138
165
|
last_call_kwargs = ack.call_args.kwargs
|
|
139
|
-
assert "response_action" in last_call_kwargs
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
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
|
+
)
|
|
143
178
|
# Check that the error message mentions the missing key events
|
|
144
179
|
error_msg = last_call_kwargs["errors"]["status"]
|
|
145
|
-
assert "Cannot close this incident" in error_msg
|
|
146
|
-
|
|
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
|
+
)
|
|
147
186
|
|
|
148
187
|
# Verify that incident update was NOT triggered
|
|
149
188
|
trigger_incident_workflow.assert_not_called()
|
|
150
189
|
|
|
151
190
|
@staticmethod
|
|
152
|
-
def test_cannot_close_from_postmortem_without_key_events(
|
|
191
|
+
def test_cannot_close_from_postmortem_without_key_events(
|
|
192
|
+
mocker: MockerFixture,
|
|
193
|
+
) -> None:
|
|
153
194
|
"""Test that closing from POST_MORTEM is prevented when key events missing.
|
|
154
195
|
|
|
155
196
|
This tests a P1/P2 incident in POST_MORTEM trying to close but blocked
|
|
156
197
|
by missing key events.
|
|
198
|
+
|
|
199
|
+
This test does NOT mock can_be_closed - it uses real milestone validation.
|
|
157
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
|
+
|
|
158
223
|
# Create a user first
|
|
159
224
|
user = UserFactory.build()
|
|
160
225
|
user.save()
|
|
161
226
|
|
|
162
227
|
# Create a P1/P2 incident in POST_MORTEM status
|
|
163
|
-
incident
|
|
228
|
+
# This incident will have missing milestones (detected, started)
|
|
229
|
+
incident = IncidentFactory.create(
|
|
164
230
|
_status=IncidentStatus.POST_MORTEM,
|
|
165
231
|
created_by=user,
|
|
166
232
|
)
|
|
167
|
-
|
|
168
|
-
#
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"
|
|
172
|
-
|
|
173
|
-
|
|
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}"
|
|
174
241
|
)
|
|
175
242
|
|
|
176
243
|
modal = UpdateStatusModal()
|
|
@@ -179,12 +246,12 @@ class TestUpdateStatusModal:
|
|
|
179
246
|
)
|
|
180
247
|
|
|
181
248
|
ack = MagicMock()
|
|
182
|
-
user = UserFactory.build()
|
|
183
|
-
user.save()
|
|
184
249
|
|
|
185
250
|
# Create a submission trying to close the incident
|
|
186
251
|
submission_copy = dict(valid_submission)
|
|
187
|
-
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
252
|
+
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
253
|
+
"selected_option"
|
|
254
|
+
] = {
|
|
188
255
|
"text": {"type": "plain_text", "text": "Closed", "emoji": True},
|
|
189
256
|
"value": "60",
|
|
190
257
|
}
|
|
@@ -195,97 +262,35 @@ class TestUpdateStatusModal:
|
|
|
195
262
|
)
|
|
196
263
|
|
|
197
264
|
# Assert that ack was called with errors
|
|
198
|
-
assert ack.called
|
|
265
|
+
assert ack.called, "ack should have been called"
|
|
199
266
|
last_call_kwargs = ack.call_args.kwargs
|
|
200
|
-
assert "response_action" in last_call_kwargs
|
|
201
|
-
|
|
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,
|
|
267
|
+
assert "response_action" in last_call_kwargs, (
|
|
268
|
+
f"Expected 'response_action' in ack call, got: {last_call_kwargs}"
|
|
234
269
|
)
|
|
235
|
-
|
|
236
|
-
|
|
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).")])
|
|
270
|
+
assert last_call_kwargs["response_action"] == "errors", (
|
|
271
|
+
f"Expected response_action='errors', got: {last_call_kwargs.get('response_action')}"
|
|
242
272
|
)
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
273
|
+
assert "errors" in last_call_kwargs, (
|
|
274
|
+
f"Expected 'errors' in ack call, got: {last_call_kwargs}"
|
|
251
275
|
)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
modal, "_trigger_incident_workflow"
|
|
276
|
+
assert "status" in last_call_kwargs["errors"], (
|
|
277
|
+
f"Expected 'status' in errors, got: {last_call_kwargs.get('errors')}"
|
|
255
278
|
)
|
|
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
279
|
error_msg = last_call_kwargs["errors"]["status"]
|
|
281
|
-
assert "Cannot close this incident" in error_msg
|
|
282
|
-
|
|
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
|
+
)
|
|
283
286
|
|
|
284
287
|
# Verify that incident update was NOT triggered
|
|
285
288
|
trigger_incident_workflow.assert_not_called()
|
|
286
289
|
|
|
287
290
|
@staticmethod
|
|
288
|
-
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:
|
|
289
294
|
"""Test that closure reason modal is shown when trying to close from INVESTIGATING.
|
|
290
295
|
|
|
291
296
|
This tests that handle_update_status_close_request correctly shows the
|
|
@@ -301,7 +306,7 @@ class TestUpdateStatusModal:
|
|
|
301
306
|
# Mock handle_update_status_close_request to return True (modal shown)
|
|
302
307
|
mock_handle_close = mocker.patch(
|
|
303
308
|
"firefighter.slack.views.modals.update_status.handle_update_status_close_request",
|
|
304
|
-
return_value=True
|
|
309
|
+
return_value=True,
|
|
305
310
|
)
|
|
306
311
|
|
|
307
312
|
trigger_incident_workflow = mocker.patch.object(
|
|
@@ -314,7 +319,9 @@ class TestUpdateStatusModal:
|
|
|
314
319
|
|
|
315
320
|
# Create a submission trying to close the incident
|
|
316
321
|
submission_copy = dict(valid_submission)
|
|
317
|
-
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
322
|
+
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
323
|
+
"selected_option"
|
|
324
|
+
] = {
|
|
318
325
|
"text": {"type": "plain_text", "text": "Closed", "emoji": True},
|
|
319
326
|
"value": "60",
|
|
320
327
|
}
|
|
@@ -325,13 +332,17 @@ class TestUpdateStatusModal:
|
|
|
325
332
|
)
|
|
326
333
|
|
|
327
334
|
# Verify handle_update_status_close_request was called
|
|
328
|
-
mock_handle_close.assert_called_once_with(
|
|
335
|
+
mock_handle_close.assert_called_once_with(
|
|
336
|
+
ack, submission_copy, incident, IncidentStatus.CLOSED
|
|
337
|
+
)
|
|
329
338
|
|
|
330
339
|
# Verify that incident update was NOT triggered (because closure reason modal was shown)
|
|
331
340
|
trigger_incident_workflow.assert_not_called()
|
|
332
341
|
|
|
333
342
|
@staticmethod
|
|
334
|
-
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:
|
|
335
346
|
"""Test that closing is allowed when all conditions are met for P3+ incidents."""
|
|
336
347
|
# Create a user first
|
|
337
348
|
user = UserFactory.build()
|
|
@@ -356,7 +367,7 @@ class TestUpdateStatusModal:
|
|
|
356
367
|
type(incident),
|
|
357
368
|
"can_be_closed",
|
|
358
369
|
new_callable=PropertyMock,
|
|
359
|
-
return_value=(True, [])
|
|
370
|
+
return_value=(True, []),
|
|
360
371
|
)
|
|
361
372
|
|
|
362
373
|
modal = UpdateStatusModal()
|
|
@@ -368,7 +379,9 @@ class TestUpdateStatusModal:
|
|
|
368
379
|
|
|
369
380
|
# Create a submission to close the incident
|
|
370
381
|
submission_copy = dict(valid_submission)
|
|
371
|
-
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
382
|
+
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
383
|
+
"selected_option"
|
|
384
|
+
] = {
|
|
372
385
|
"text": {"type": "plain_text", "text": "Closed", "emoji": True},
|
|
373
386
|
"value": "60",
|
|
374
387
|
}
|
|
@@ -380,14 +393,18 @@ class TestUpdateStatusModal:
|
|
|
380
393
|
|
|
381
394
|
# Assert that ack was called successfully (no errors)
|
|
382
395
|
# The first call is the successful ack() without errors
|
|
383
|
-
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
|
+
)
|
|
384
399
|
assert first_call_kwargs == {} or "errors" not in first_call_kwargs
|
|
385
400
|
|
|
386
401
|
# Verify that incident update WAS triggered
|
|
387
402
|
trigger_incident_workflow.assert_called_once()
|
|
388
403
|
|
|
389
404
|
@staticmethod
|
|
390
|
-
def test_can_update_priority_without_changing_status(
|
|
405
|
+
def test_can_update_priority_without_changing_status(
|
|
406
|
+
mocker: MockerFixture, priority_factory
|
|
407
|
+
) -> None:
|
|
391
408
|
"""Test that priority can be updated without changing status.
|
|
392
409
|
|
|
393
410
|
This reproduces the bug where trying to update only the priority of a P4
|
|
@@ -424,12 +441,16 @@ class TestUpdateStatusModal:
|
|
|
424
441
|
# The status field will have MITIGATED (40) as initial value, but it's not in the available choices
|
|
425
442
|
submission_copy = dict(valid_submission)
|
|
426
443
|
# Status unchanged - keeps MITIGATED (40)
|
|
427
|
-
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
444
|
+
submission_copy["view"]["state"]["values"]["status"]["status"][
|
|
445
|
+
"selected_option"
|
|
446
|
+
] = {
|
|
428
447
|
"text": {"type": "plain_text", "text": "Mitigated", "emoji": True},
|
|
429
448
|
"value": "40", # This should cause validation error with current code
|
|
430
449
|
}
|
|
431
450
|
# Change priority to P4
|
|
432
|
-
submission_copy["view"]["state"]["values"]["priority"]["priority"][
|
|
451
|
+
submission_copy["view"]["state"]["values"]["priority"]["priority"][
|
|
452
|
+
"selected_option"
|
|
453
|
+
] = {
|
|
433
454
|
"text": {"type": "plain_text", "text": "P4", "emoji": True},
|
|
434
455
|
"value": str(p4_priority.id),
|
|
435
456
|
}
|
|
@@ -441,9 +462,12 @@ class TestUpdateStatusModal:
|
|
|
441
462
|
|
|
442
463
|
# Assert that ack was called successfully WITHOUT errors
|
|
443
464
|
# With the bug, this would fail with "Select a valid choice. 40 is not one of the available choices"
|
|
444
|
-
first_call_kwargs =
|
|
445
|
-
|
|
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, (
|
|
446
469
|
f"Should allow updating priority without changing status. Got errors: {first_call_kwargs.get('errors')}"
|
|
470
|
+
)
|
|
447
471
|
|
|
448
472
|
# Verify that incident update WAS triggered (priority changed)
|
|
449
473
|
trigger_incident_workflow.assert_called_once()
|
|
@@ -643,7 +667,11 @@ valid_submission = {
|
|
|
643
667
|
{
|
|
644
668
|
"type": "input",
|
|
645
669
|
"block_id": "incident_category",
|
|
646
|
-
"label": {
|
|
670
|
+
"label": {
|
|
671
|
+
"type": "plain_text",
|
|
672
|
+
"text": "Issue category",
|
|
673
|
+
"emoji": True,
|
|
674
|
+
},
|
|
647
675
|
"optional": False,
|
|
648
676
|
"dispatch_action": False,
|
|
649
677
|
"element": {
|
{firefighter_incident-0.0.21.dist-info → firefighter_incident-0.0.23.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{firefighter_incident-0.0.21.dist-info → firefighter_incident-0.0.23.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|