firefighter-incident 0.0.16__py3-none-any.whl → 0.0.17__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/incidents/forms/edit.py +5 -3
- firefighter/incidents/forms/unified_incident.py +180 -56
- firefighter/incidents/forms/update_status.py +94 -58
- firefighter/incidents/forms/utils.py +14 -0
- firefighter/incidents/models/incident.py +3 -2
- firefighter/raid/apps.py +0 -1
- firefighter/slack/signals/__init__.py +16 -0
- firefighter/slack/signals/incident_updated.py +43 -1
- firefighter/slack/utils.py +43 -6
- firefighter/slack/views/modals/base_modal/form_utils.py +3 -1
- firefighter/slack/views/modals/downgrade_workflow.py +3 -1
- firefighter/slack/views/modals/edit.py +53 -7
- firefighter/slack/views/modals/opening/set_details.py +20 -0
- firefighter_fixtures/incidents/priorities.json +1 -1
- {firefighter_incident-0.0.16.dist-info → firefighter_incident-0.0.17.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.16.dist-info → firefighter_incident-0.0.17.dist-info}/RECORD +28 -29
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +160 -23
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +38 -60
- firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +35 -20
- firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +8 -6
- firefighter_tests/test_slack/test_signals_downgrade.py +147 -0
- firefighter_tests/test_slack/views/modals/test_edit.py +324 -0
- firefighter_tests/test_slack/views/modals/test_opening_unified.py +42 -0
- firefighter_tests/test_slack/views/modals/test_update_status.py +72 -2
- firefighter/raid/signals/incident_created.py +0 -129
- firefighter_tests/test_raid/test_p1_p3_jira_fields.py +0 -372
- firefighter_tests/test_raid/test_priority_mapping.py +0 -267
- {firefighter_incident-0.0.16.dist-info → firefighter_incident-0.0.17.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.16.dist-info → firefighter_incident-0.0.17.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.16.dist-info → firefighter_incident-0.0.17.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Tests for incident priority downgrade signal handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from firefighter.incidents.factories import IncidentFactory, UserFactory
|
|
10
|
+
from firefighter.incidents.models import Priority
|
|
11
|
+
from firefighter.slack.factories import IncidentChannelFactory, SlackUserFactory
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pytest_mock import MockerFixture
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.django_db
|
|
18
|
+
class TestIncidentDowngradeSignal:
|
|
19
|
+
"""Test the incident_updated_check_dowmgrade_handler signal."""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def test_downgrade_hint_shown_for_critical_to_normal(mocker: MockerFixture) -> None:
|
|
23
|
+
"""Test that downgrade hint is shown when downgrading from P1/P2/P3 to P4/P5."""
|
|
24
|
+
# Create a user with a Slack user
|
|
25
|
+
user = UserFactory.build()
|
|
26
|
+
user.save()
|
|
27
|
+
slack_user = SlackUserFactory.build(user=user)
|
|
28
|
+
slack_user.save()
|
|
29
|
+
|
|
30
|
+
# Get priorities from DB
|
|
31
|
+
p2 = Priority.objects.get(name="P2")
|
|
32
|
+
p4 = Priority.objects.get(name="P4")
|
|
33
|
+
|
|
34
|
+
# Create an incident with P2 priority
|
|
35
|
+
incident = IncidentFactory.build(priority=p2, created_by=user)
|
|
36
|
+
incident.save()
|
|
37
|
+
|
|
38
|
+
# Create an incident channel (conversation) for this incident
|
|
39
|
+
conversation = IncidentChannelFactory.build(incident=incident)
|
|
40
|
+
conversation.save()
|
|
41
|
+
|
|
42
|
+
# Mock the send_message_ephemeral method
|
|
43
|
+
mock_send = mocker.patch.object(conversation, "send_message_ephemeral")
|
|
44
|
+
|
|
45
|
+
# Downgrade from P2 to P4
|
|
46
|
+
incident.create_incident_update(
|
|
47
|
+
created_by=user, priority_id=p4.id, message="Downgrading to P4"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Verify the downgrade hint message was sent
|
|
51
|
+
mock_send.assert_called_once()
|
|
52
|
+
call_args = mock_send.call_args
|
|
53
|
+
assert call_args is not None
|
|
54
|
+
# Check that the message is about the incident not needing an incident channel
|
|
55
|
+
message = call_args.kwargs["message"]
|
|
56
|
+
message_text = message.get_text().lower()
|
|
57
|
+
assert "might not need an incident channel" in message_text or "p4" in message_text or "p5" in message_text
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def test_no_hint_when_staying_in_critical_range(mocker: MockerFixture) -> None:
|
|
61
|
+
"""Test that no hint is shown when changing priority within critical range (P1/P2/P3)."""
|
|
62
|
+
# Create a user
|
|
63
|
+
user = UserFactory.build()
|
|
64
|
+
user.save()
|
|
65
|
+
|
|
66
|
+
# Get priorities from DB
|
|
67
|
+
p1 = Priority.objects.get(name="P1")
|
|
68
|
+
p3 = Priority.objects.get(name="P3")
|
|
69
|
+
|
|
70
|
+
# Create an incident with P1 priority
|
|
71
|
+
incident = IncidentFactory.build(priority=p1, created_by=user)
|
|
72
|
+
incident.save()
|
|
73
|
+
|
|
74
|
+
# Create an incident channel (conversation) for this incident
|
|
75
|
+
conversation = IncidentChannelFactory.build(incident=incident)
|
|
76
|
+
conversation.save()
|
|
77
|
+
|
|
78
|
+
# Mock the send_message_ephemeral method
|
|
79
|
+
mock_send = mocker.patch.object(conversation, "send_message_ephemeral")
|
|
80
|
+
|
|
81
|
+
# Update from P1 to P3 (both critical)
|
|
82
|
+
incident.create_incident_update(
|
|
83
|
+
created_by=user, priority_id=p3.id, message="Updating to P3"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Verify NO downgrade hint was sent
|
|
87
|
+
mock_send.assert_not_called()
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def test_no_hint_when_staying_in_normal_range(mocker: MockerFixture) -> None:
|
|
91
|
+
"""Test that no hint is shown when changing priority within normal range (P4/P5)."""
|
|
92
|
+
# Create a user
|
|
93
|
+
user = UserFactory.build()
|
|
94
|
+
user.save()
|
|
95
|
+
|
|
96
|
+
# Get priorities from DB
|
|
97
|
+
p4 = Priority.objects.get(name="P4")
|
|
98
|
+
p5 = Priority.objects.get(name="P5")
|
|
99
|
+
|
|
100
|
+
# Create an incident with P4 priority
|
|
101
|
+
incident = IncidentFactory.build(priority=p4, created_by=user)
|
|
102
|
+
incident.save()
|
|
103
|
+
|
|
104
|
+
# Create an incident channel (conversation) for this incident
|
|
105
|
+
conversation = IncidentChannelFactory.build(incident=incident)
|
|
106
|
+
conversation.save()
|
|
107
|
+
|
|
108
|
+
# Mock the send_message_ephemeral method
|
|
109
|
+
mock_send = mocker.patch.object(conversation, "send_message_ephemeral")
|
|
110
|
+
|
|
111
|
+
# Update from P4 to P5 (both normal)
|
|
112
|
+
incident.create_incident_update(
|
|
113
|
+
created_by=user, priority_id=p5.id, message="Updating to P5"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Verify NO downgrade hint was sent
|
|
117
|
+
mock_send.assert_not_called()
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def test_no_hint_when_upgrading_from_normal_to_critical(mocker: MockerFixture) -> None:
|
|
121
|
+
"""Test that no hint is shown when upgrading from P4/P5 to P1/P2/P3."""
|
|
122
|
+
# Create a user
|
|
123
|
+
user = UserFactory.build()
|
|
124
|
+
user.save()
|
|
125
|
+
|
|
126
|
+
# Get priorities from DB
|
|
127
|
+
p5 = Priority.objects.get(name="P5")
|
|
128
|
+
p2 = Priority.objects.get(name="P2")
|
|
129
|
+
|
|
130
|
+
# Create an incident with P5 priority
|
|
131
|
+
incident = IncidentFactory.build(priority=p5, created_by=user)
|
|
132
|
+
incident.save()
|
|
133
|
+
|
|
134
|
+
# Create an incident channel (conversation) for this incident
|
|
135
|
+
conversation = IncidentChannelFactory.build(incident=incident)
|
|
136
|
+
conversation.save()
|
|
137
|
+
|
|
138
|
+
# Mock the send_message_ephemeral method
|
|
139
|
+
mock_send = mocker.patch.object(conversation, "send_message_ephemeral")
|
|
140
|
+
|
|
141
|
+
# Upgrade from P5 to P2
|
|
142
|
+
incident.create_incident_update(
|
|
143
|
+
created_by=user, priority_id=p2.id, message="Escalating to P2"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Verify NO downgrade hint was sent (this is an upgrade, not a downgrade)
|
|
147
|
+
mock_send.assert_not_called()
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from unittest.mock import MagicMock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from pytest_mock import MockerFixture
|
|
8
|
+
|
|
9
|
+
from firefighter.incidents.factories import IncidentFactory, UserFactory
|
|
10
|
+
from firefighter.incidents.models import Environment, Incident
|
|
11
|
+
from firefighter.slack.views import EditMetaModal
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.django_db
|
|
17
|
+
class TestEditMetaModal:
|
|
18
|
+
@staticmethod
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def incident(environment_factory) -> Incident:
|
|
21
|
+
"""Returns a valid incident with environment and custom_fields."""
|
|
22
|
+
env_prd = environment_factory(value="PRD", name="Production", order=0)
|
|
23
|
+
return IncidentFactory.create(
|
|
24
|
+
environment=env_prd,
|
|
25
|
+
custom_fields={"environments": ["PRD"]}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def multi_env_incident(environment_factory) -> Incident:
|
|
31
|
+
"""Returns an incident with multiple environments in custom_fields."""
|
|
32
|
+
env_prd = environment_factory(value="PRD", name="Production", order=0)
|
|
33
|
+
environment_factory(value="STG", name="Staging", order=1)
|
|
34
|
+
environment_factory(value="INT", name="Integration", order=2)
|
|
35
|
+
|
|
36
|
+
return IncidentFactory.create(
|
|
37
|
+
environment=env_prd,
|
|
38
|
+
custom_fields={"environments": ["PRD", "STG", "INT"]}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def test_build_modal_fn_single_environment(incident: Incident) -> None:
|
|
43
|
+
"""Test building modal with single environment."""
|
|
44
|
+
modal = EditMetaModal()
|
|
45
|
+
res = modal.build_modal_fn(incident)
|
|
46
|
+
|
|
47
|
+
# Validate the JSON structure
|
|
48
|
+
assert res.to_dict()
|
|
49
|
+
|
|
50
|
+
values = res.to_dict()
|
|
51
|
+
assert "blocks" in values
|
|
52
|
+
assert values["title"]["text"] == f"Update incident #{incident.id}"[:24]
|
|
53
|
+
assert values["submit"]["text"] == "Update incident"
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def test_build_modal_fn_multiple_environments(multi_env_incident: Incident) -> None:
|
|
57
|
+
"""Test building modal with multiple environments in custom_fields."""
|
|
58
|
+
modal = EditMetaModal()
|
|
59
|
+
res = modal.build_modal_fn(multi_env_incident)
|
|
60
|
+
|
|
61
|
+
# Validate the JSON structure
|
|
62
|
+
assert res.to_dict()
|
|
63
|
+
|
|
64
|
+
values = res.to_dict()
|
|
65
|
+
assert "blocks" in values
|
|
66
|
+
|
|
67
|
+
# The form should show all three environments from custom_fields
|
|
68
|
+
# We verify this by checking the initial values passed to the form
|
|
69
|
+
assert multi_env_incident.custom_fields["environments"] == ["PRD", "STG", "INT"]
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def test_build_modal_fn_fallback_to_single_environment(environment_factory) -> None:
|
|
73
|
+
"""Test building modal falls back to single environment when custom_fields is empty."""
|
|
74
|
+
env_prd = environment_factory(value="PRD", name="Production", order=0)
|
|
75
|
+
incident = IncidentFactory.create(
|
|
76
|
+
environment=env_prd,
|
|
77
|
+
custom_fields={} # No environments in custom_fields
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
modal = EditMetaModal()
|
|
81
|
+
res = modal.build_modal_fn(incident)
|
|
82
|
+
|
|
83
|
+
# Validate the JSON structure
|
|
84
|
+
assert res.to_dict()
|
|
85
|
+
|
|
86
|
+
values = res.to_dict()
|
|
87
|
+
assert "blocks" in values
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def test_build_modal_fn_empty_custom_fields(environment_factory) -> None:
|
|
91
|
+
"""Test building modal when custom_fields has no environments."""
|
|
92
|
+
env_stg = environment_factory(value="STG", name="Staging", order=1)
|
|
93
|
+
incident = IncidentFactory.create(
|
|
94
|
+
environment=env_stg,
|
|
95
|
+
custom_fields={} # Empty custom_fields
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
modal = EditMetaModal()
|
|
99
|
+
res = modal.build_modal_fn(incident)
|
|
100
|
+
|
|
101
|
+
# Should build successfully and fallback to single environment
|
|
102
|
+
assert res.to_dict()
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def test_handle_modal_fn_update_title_and_description(
|
|
106
|
+
mocker: MockerFixture, incident: Incident
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Test handling modal submission with title and description updates."""
|
|
109
|
+
modal = EditMetaModal()
|
|
110
|
+
trigger_workflow = mocker.patch.object(modal, "_trigger_incident_workflow")
|
|
111
|
+
|
|
112
|
+
ack = MagicMock()
|
|
113
|
+
user = UserFactory.create()
|
|
114
|
+
|
|
115
|
+
# Create a submission with updated title and description
|
|
116
|
+
submission = create_edit_submission(
|
|
117
|
+
incident=incident,
|
|
118
|
+
title="Updated Incident Title",
|
|
119
|
+
description="Updated incident description with more details.",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
modal.handle_modal_fn(ack=ack, body=submission, incident=incident, user=user)
|
|
123
|
+
|
|
124
|
+
# Assert
|
|
125
|
+
ack.assert_called_once_with()
|
|
126
|
+
trigger_workflow.assert_called_once()
|
|
127
|
+
call_kwargs = trigger_workflow.call_args.kwargs
|
|
128
|
+
assert call_kwargs["title"] == "Updated Incident Title"
|
|
129
|
+
assert call_kwargs["description"] == "Updated incident description with more details."
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def test_handle_modal_fn_update_environments(
|
|
133
|
+
mocker: MockerFixture, environment_factory
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Test handling modal submission with environment updates."""
|
|
136
|
+
env_prd = environment_factory(value="PRD", name="Production", order=0)
|
|
137
|
+
env_stg = environment_factory(value="STG", name="Staging", order=1)
|
|
138
|
+
env_int = environment_factory(value="INT", name="Integration", order=2)
|
|
139
|
+
|
|
140
|
+
incident = IncidentFactory.create(
|
|
141
|
+
environment=env_prd,
|
|
142
|
+
custom_fields={"environments": ["PRD"]}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
modal = EditMetaModal()
|
|
146
|
+
trigger_workflow = mocker.patch.object(modal, "_trigger_incident_workflow")
|
|
147
|
+
|
|
148
|
+
ack = MagicMock()
|
|
149
|
+
user = UserFactory.create()
|
|
150
|
+
|
|
151
|
+
# Create a submission with multiple environments selected
|
|
152
|
+
submission = create_edit_submission(
|
|
153
|
+
incident=incident,
|
|
154
|
+
environment_ids=[str(env_prd.id), str(env_stg.id), str(env_int.id)],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
modal.handle_modal_fn(ack=ack, body=submission, incident=incident, user=user)
|
|
158
|
+
|
|
159
|
+
# Assert
|
|
160
|
+
ack.assert_called_once_with()
|
|
161
|
+
|
|
162
|
+
# Verify custom_fields were updated
|
|
163
|
+
incident.refresh_from_db()
|
|
164
|
+
assert set(incident.custom_fields["environments"]) == {"PRD", "STG", "INT"}
|
|
165
|
+
|
|
166
|
+
# Verify primary environment is PRD (highest priority)
|
|
167
|
+
trigger_workflow.assert_called_once()
|
|
168
|
+
call_kwargs = trigger_workflow.call_args.kwargs
|
|
169
|
+
assert call_kwargs["environment_id"] == env_prd.id
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def test_handle_modal_fn_change_primary_environment(
|
|
173
|
+
mocker: MockerFixture, environment_factory
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Test changing from one environment to another."""
|
|
176
|
+
env_prd = environment_factory(value="PRD", name="Production", order=0)
|
|
177
|
+
env_stg = environment_factory(value="STG", name="Staging", order=1)
|
|
178
|
+
|
|
179
|
+
incident = IncidentFactory.create(
|
|
180
|
+
environment=env_prd,
|
|
181
|
+
custom_fields={"environments": ["PRD"]}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
modal = EditMetaModal()
|
|
185
|
+
trigger_workflow = mocker.patch.object(modal, "_trigger_incident_workflow")
|
|
186
|
+
|
|
187
|
+
ack = MagicMock()
|
|
188
|
+
user = UserFactory.create()
|
|
189
|
+
|
|
190
|
+
# Change to STG only
|
|
191
|
+
submission = create_edit_submission(
|
|
192
|
+
incident=incident,
|
|
193
|
+
environment_ids=[str(env_stg.id)],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
modal.handle_modal_fn(ack=ack, body=submission, incident=incident, user=user)
|
|
197
|
+
|
|
198
|
+
# Assert
|
|
199
|
+
ack.assert_called_once_with()
|
|
200
|
+
|
|
201
|
+
# Verify custom_fields were updated to STG
|
|
202
|
+
incident.refresh_from_db()
|
|
203
|
+
assert incident.custom_fields["environments"] == ["STG"]
|
|
204
|
+
|
|
205
|
+
# Verify primary environment is STG
|
|
206
|
+
trigger_workflow.assert_called_once()
|
|
207
|
+
call_kwargs = trigger_workflow.call_args.kwargs
|
|
208
|
+
assert call_kwargs["environment_id"] == env_stg.id
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def test_handle_modal_fn_no_changes(mocker: MockerFixture, incident: Incident) -> None:
|
|
212
|
+
"""Test handling modal submission with no changes."""
|
|
213
|
+
modal = EditMetaModal()
|
|
214
|
+
trigger_workflow = mocker.patch.object(modal, "_trigger_incident_workflow")
|
|
215
|
+
|
|
216
|
+
ack = MagicMock()
|
|
217
|
+
user = UserFactory.create()
|
|
218
|
+
|
|
219
|
+
# Create a submission with same values as current incident
|
|
220
|
+
submission = create_edit_submission(
|
|
221
|
+
incident=incident,
|
|
222
|
+
title=incident.title,
|
|
223
|
+
description=incident.description,
|
|
224
|
+
environment_ids=[str(incident.environment.id)],
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
modal.handle_modal_fn(ack=ack, body=submission, incident=incident, user=user)
|
|
228
|
+
|
|
229
|
+
# Assert that workflow was not triggered (no changes)
|
|
230
|
+
ack.assert_called_once_with()
|
|
231
|
+
trigger_workflow.assert_not_called()
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def test_handle_modal_fn_empty_body(incident: Incident) -> None:
|
|
235
|
+
"""Test handling modal with empty body raises TypeError."""
|
|
236
|
+
modal = EditMetaModal()
|
|
237
|
+
ack = MagicMock()
|
|
238
|
+
user = UserFactory.create()
|
|
239
|
+
|
|
240
|
+
with pytest.raises(TypeError, match="Expected a values dict in the body"):
|
|
241
|
+
modal.handle_modal_fn(ack=ack, body={}, incident=incident, user=user)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def create_edit_submission(
|
|
245
|
+
incident: Incident,
|
|
246
|
+
title: str | None = None,
|
|
247
|
+
description: str | None = None,
|
|
248
|
+
environment_ids: list[str] | None = None,
|
|
249
|
+
) -> dict:
|
|
250
|
+
"""Helper function to create a valid edit submission body."""
|
|
251
|
+
# Use current values as defaults
|
|
252
|
+
title = title if title is not None else incident.title
|
|
253
|
+
description = description if description is not None else incident.description
|
|
254
|
+
|
|
255
|
+
# Build environment options
|
|
256
|
+
if environment_ids is None:
|
|
257
|
+
# Use current incident environment
|
|
258
|
+
environment_ids = [str(incident.environment.id)] if incident.environment else []
|
|
259
|
+
|
|
260
|
+
# Convert environment IDs to options format
|
|
261
|
+
env_selected_options = []
|
|
262
|
+
if environment_ids:
|
|
263
|
+
for env_id in environment_ids:
|
|
264
|
+
env_obj = Environment.objects.get(id=env_id)
|
|
265
|
+
env_selected_options.append({
|
|
266
|
+
"text": {"type": "plain_text", "text": f"{env_obj.value} - {env_obj.description}", "emoji": True},
|
|
267
|
+
"value": env_id,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"type": "view_submission",
|
|
272
|
+
"team": {"id": "T01FJ0NNFQD", "domain": "team-firefighter"},
|
|
273
|
+
"user": {
|
|
274
|
+
"id": "U03L9K8P5SA",
|
|
275
|
+
"username": "john.doe",
|
|
276
|
+
"name": "john.doe",
|
|
277
|
+
"team_id": "T01FJ0NNFQD",
|
|
278
|
+
},
|
|
279
|
+
"api_app_id": "A03SXN0ENM9",
|
|
280
|
+
"token": "fake_token",
|
|
281
|
+
"trigger_id": "3924659449141.1528022763829.670d1c03f8d04cf6655676963267ca4e",
|
|
282
|
+
"view": {
|
|
283
|
+
"id": "V03T304049L",
|
|
284
|
+
"team_id": "T01FJ0NNFQD",
|
|
285
|
+
"type": "modal",
|
|
286
|
+
"private_metadata": str(incident.id),
|
|
287
|
+
"callback_id": "incident_edit_incident",
|
|
288
|
+
"state": {
|
|
289
|
+
"values": {
|
|
290
|
+
"title": {
|
|
291
|
+
"title": {
|
|
292
|
+
"type": "plain_text_input",
|
|
293
|
+
"value": title,
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
"description": {
|
|
297
|
+
"description": {
|
|
298
|
+
"type": "plain_text_input",
|
|
299
|
+
"value": description,
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
"environment": {
|
|
303
|
+
"environment": {
|
|
304
|
+
"type": "multi_static_select",
|
|
305
|
+
"selected_options": env_selected_options,
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
"hash": "1660220674.c7elhik9",
|
|
311
|
+
"title": {"type": "plain_text", "text": f"Update incident #{incident.id}"[:24], "emoji": True},
|
|
312
|
+
"clear_on_close": False,
|
|
313
|
+
"notify_on_close": False,
|
|
314
|
+
"close": None,
|
|
315
|
+
"submit": {"type": "plain_text", "text": "Update incident", "emoji": True},
|
|
316
|
+
"app_id": "A03SXN0ENM9",
|
|
317
|
+
"external_id": "",
|
|
318
|
+
"app_installed_team_id": "T01FJ0NNFQD",
|
|
319
|
+
"bot_id": "B03T08W83AQ",
|
|
320
|
+
},
|
|
321
|
+
"response_urls": [],
|
|
322
|
+
"is_enterprise_install": False,
|
|
323
|
+
"enterprise": None,
|
|
324
|
+
}
|
|
@@ -419,3 +419,45 @@ class TestOpeningUnifiedModal:
|
|
|
419
419
|
|
|
420
420
|
# Verify that suggested_team_routing is NOT in the form (critical incident)
|
|
421
421
|
assert "suggested_team_routing" not in form.fields, "suggested_team_routing should not be present for critical incidents"
|
|
422
|
+
|
|
423
|
+
def test_build_modal_fn_handles_environment_uuid_list(
|
|
424
|
+
self, priority_factory, environment_factory
|
|
425
|
+
):
|
|
426
|
+
"""build_modal_fn should handle list of environment UUIDs and preserve order."""
|
|
427
|
+
priority_factory(value=1, default=True)
|
|
428
|
+
env_prd = environment_factory(value="PRD", default=True)
|
|
429
|
+
env_stg = environment_factory(value="STG", default=False)
|
|
430
|
+
|
|
431
|
+
modal = OpeningUnifiedModal()
|
|
432
|
+
|
|
433
|
+
# Simulate editing scenario where details_form_data contains list of environment UUIDs
|
|
434
|
+
# IMPORTANT: Order is STG first, then PRD (not alphabetical order)
|
|
435
|
+
open_incident_context = {
|
|
436
|
+
"response_type": "critical",
|
|
437
|
+
"impact_form_data": {},
|
|
438
|
+
"details_form_data": {
|
|
439
|
+
"environment": [str(env_stg.id), str(env_prd.id)] # List of UUIDs (STG, PRD)
|
|
440
|
+
},
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
# This should NOT raise ValidationError about invalid UUID
|
|
444
|
+
view = modal.build_modal_fn(open_incident_context=open_incident_context)
|
|
445
|
+
|
|
446
|
+
# Should build view without errors
|
|
447
|
+
assert view is not None
|
|
448
|
+
assert view.type == "modal"
|
|
449
|
+
assert len(view.blocks) > 0
|
|
450
|
+
|
|
451
|
+
# Verify that the environment block preserves the order (STG, PRD)
|
|
452
|
+
env_blocks = [b for b in view.blocks if hasattr(b, "block_id") and b.block_id == "environment"]
|
|
453
|
+
assert len(env_blocks) == 1, "Should find exactly one environment block"
|
|
454
|
+
env_block = env_blocks[0]
|
|
455
|
+
|
|
456
|
+
# Check initial_options preserves order: STG first, then PRD
|
|
457
|
+
assert hasattr(env_block, "element"), "Block should have an element attribute"
|
|
458
|
+
assert hasattr(env_block.element, "initial_options"), "Element should have initial_options"
|
|
459
|
+
initial_options = env_block.element.initial_options
|
|
460
|
+
assert initial_options is not None, "initial_options should not be None"
|
|
461
|
+
assert len(initial_options) == 2, "Should have both environments in initial_options"
|
|
462
|
+
assert initial_options[0].value == str(env_stg.id), "First option should be STG (preserving order)"
|
|
463
|
+
assert initial_options[1].value == str(env_prd.id), "Second option should be PRD (preserving order)"
|
|
@@ -332,12 +332,12 @@ class TestUpdateStatusModal:
|
|
|
332
332
|
|
|
333
333
|
@staticmethod
|
|
334
334
|
def test_can_close_when_all_conditions_met(mocker: MockerFixture) -> None:
|
|
335
|
-
"""Test that closing is allowed when all conditions are met."""
|
|
335
|
+
"""Test that closing is allowed when all conditions are met for P3+ incidents."""
|
|
336
336
|
# Create a user first
|
|
337
337
|
user = UserFactory.build()
|
|
338
338
|
user.save()
|
|
339
339
|
|
|
340
|
-
# Create
|
|
340
|
+
# Create a P3+ incident in MITIGATED status with all conditions met
|
|
341
341
|
incident = IncidentFactory.build(
|
|
342
342
|
_status=IncidentStatus.MITIGATED,
|
|
343
343
|
created_by=user,
|
|
@@ -345,6 +345,14 @@ class TestUpdateStatusModal:
|
|
|
345
345
|
# IMPORTANT: Save the incident so it has an ID for the form to reference
|
|
346
346
|
incident.save()
|
|
347
347
|
|
|
348
|
+
# Mock needs_postmortem to return False (P3+ incident)
|
|
349
|
+
mocker.patch.object(
|
|
350
|
+
type(incident),
|
|
351
|
+
"needs_postmortem",
|
|
352
|
+
new_callable=PropertyMock,
|
|
353
|
+
return_value=False
|
|
354
|
+
)
|
|
355
|
+
|
|
348
356
|
# Mock can_be_closed to return True (all conditions met)
|
|
349
357
|
mocker.patch.object(
|
|
350
358
|
type(incident),
|
|
@@ -380,6 +388,68 @@ class TestUpdateStatusModal:
|
|
|
380
388
|
# Verify that incident update WAS triggered
|
|
381
389
|
trigger_incident_workflow.assert_called_once()
|
|
382
390
|
|
|
391
|
+
@staticmethod
|
|
392
|
+
def test_can_update_priority_without_changing_status(mocker: MockerFixture, priority_factory) -> None:
|
|
393
|
+
"""Test that priority can be updated without changing status.
|
|
394
|
+
|
|
395
|
+
This reproduces the bug where trying to update only the priority of a P4
|
|
396
|
+
incident in MITIGATED status fails with:
|
|
397
|
+
"Select a valid choice. 40 is not one of the available choices."
|
|
398
|
+
|
|
399
|
+
The issue is that the form restricts status choices based on current status,
|
|
400
|
+
but the initial value (MITIGATED=40) may not be in those restricted choices.
|
|
401
|
+
"""
|
|
402
|
+
# Create a user first
|
|
403
|
+
user = UserFactory.build()
|
|
404
|
+
user.save()
|
|
405
|
+
|
|
406
|
+
# Create P3 and P4 priorities
|
|
407
|
+
p3_priority = priority_factory(value=3, name="P3")
|
|
408
|
+
p4_priority = priority_factory(value=4, name="P4")
|
|
409
|
+
|
|
410
|
+
# Create a P3 incident in MITIGATED status
|
|
411
|
+
incident = IncidentFactory.build(
|
|
412
|
+
_status=IncidentStatus.MITIGATED,
|
|
413
|
+
created_by=user,
|
|
414
|
+
priority=p3_priority,
|
|
415
|
+
)
|
|
416
|
+
incident.save()
|
|
417
|
+
|
|
418
|
+
modal = UpdateStatusModal()
|
|
419
|
+
trigger_incident_workflow = mocker.patch.object(
|
|
420
|
+
modal, "_trigger_incident_workflow"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
ack = MagicMock()
|
|
424
|
+
|
|
425
|
+
# User tries to change priority from P3 to P4 WITHOUT changing status
|
|
426
|
+
# The status field will have MITIGATED (40) as initial value, but it's not in the available choices
|
|
427
|
+
submission_copy = dict(valid_submission)
|
|
428
|
+
# Status unchanged - keeps MITIGATED (40)
|
|
429
|
+
submission_copy["view"]["state"]["values"]["status"]["status"]["selected_option"] = {
|
|
430
|
+
"text": {"type": "plain_text", "text": "Mitigated", "emoji": True},
|
|
431
|
+
"value": "40", # This should cause validation error with current code
|
|
432
|
+
}
|
|
433
|
+
# Change priority to P4
|
|
434
|
+
submission_copy["view"]["state"]["values"]["priority"]["priority"]["selected_option"] = {
|
|
435
|
+
"text": {"type": "plain_text", "text": "P4", "emoji": True},
|
|
436
|
+
"value": str(p4_priority.id),
|
|
437
|
+
}
|
|
438
|
+
submission_copy["view"]["private_metadata"] = str(incident.id)
|
|
439
|
+
|
|
440
|
+
modal.handle_modal_fn(
|
|
441
|
+
ack=ack, body=submission_copy, incident=incident, user=user
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Assert that ack was called successfully WITHOUT errors
|
|
445
|
+
# With the bug, this would fail with "Select a valid choice. 40 is not one of the available choices"
|
|
446
|
+
first_call_kwargs = ack.call_args_list[0][1] if ack.call_args_list else ack.call_args.kwargs
|
|
447
|
+
assert first_call_kwargs == {} or "errors" not in first_call_kwargs, \
|
|
448
|
+
f"Should allow updating priority without changing status. Got errors: {first_call_kwargs.get('errors')}"
|
|
449
|
+
|
|
450
|
+
# Verify that incident update WAS triggered (priority changed)
|
|
451
|
+
trigger_incident_workflow.assert_called_once()
|
|
452
|
+
|
|
383
453
|
|
|
384
454
|
valid_submission = {
|
|
385
455
|
"type": "view_submission",
|