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.
- firefighter/_version.py +2 -2
- firefighter/api/serializers.py +9 -0
- firefighter/confluence/signals/incident_updated.py +2 -2
- firefighter/incidents/enums.py +22 -2
- firefighter/incidents/forms/closure_reason.py +45 -0
- firefighter/incidents/forms/unified_incident.py +406 -0
- firefighter/incidents/forms/update_status.py +87 -1
- firefighter/incidents/migrations/0027_add_closure_fields.py +40 -0
- firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
- firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
- firefighter/incidents/models/incident.py +32 -5
- firefighter/incidents/static/css/main.min.css +1 -1
- firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
- firefighter/incidents/views/reports.py +3 -3
- firefighter/raid/apps.py +9 -26
- firefighter/raid/client.py +2 -2
- firefighter/raid/forms.py +75 -238
- firefighter/raid/signals/incident_created.py +38 -13
- firefighter/raid/signals/incident_updated.py +3 -2
- firefighter/slack/messages/slack_messages.py +19 -4
- firefighter/slack/rules.py +1 -1
- firefighter/slack/signals/create_incident_conversation.py +6 -0
- firefighter/slack/signals/incident_updated.py +7 -1
- firefighter/slack/views/modals/__init__.py +4 -0
- firefighter/slack/views/modals/base_modal/form_utils.py +63 -0
- firefighter/slack/views/modals/close.py +15 -2
- firefighter/slack/views/modals/closure_reason.py +193 -0
- firefighter/slack/views/modals/open.py +60 -13
- firefighter/slack/views/modals/opening/details/unified.py +203 -0
- firefighter/slack/views/modals/opening/select_impact.py +1 -1
- firefighter/slack/views/modals/opening/set_details.py +3 -2
- firefighter/slack/views/modals/postmortem.py +10 -2
- firefighter/slack/views/modals/update_status.py +28 -2
- firefighter/slack/views/modals/utils.py +51 -0
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/RECORD +62 -38
- firefighter_tests/test_incidents/test_enums.py +100 -0
- firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
- firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
- firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
- firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
- firefighter_tests/test_incidents/test_models/test_incident_model.py +68 -0
- firefighter_tests/test_raid/conftest.py +154 -0
- firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
- firefighter_tests/test_raid/test_raid_forms.py +10 -253
- firefighter_tests/test_raid/test_raid_signals.py +187 -0
- firefighter_tests/test_slack/messages/__init__.py +0 -0
- firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
- firefighter_tests/test_slack/views/modals/conftest.py +140 -0
- firefighter_tests/test_slack/views/modals/test_close.py +65 -3
- firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
- firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
- firefighter_tests/test_slack/views/modals/test_open.py +146 -2
- firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
- firefighter_tests/test_slack/views/modals/test_update_status.py +327 -3
- firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
- firefighter/raid/views/open_normal.py +0 -139
- firefighter/slack/views/modals/opening/details/critical.py +0 -88
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Tests for ClosureReasonModal - Slack modal for closing incidents with a reason."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from slack_sdk.errors import SlackApiError
|
|
9
|
+
|
|
10
|
+
from firefighter.incidents.enums import ClosureReason, IncidentStatus
|
|
11
|
+
from firefighter.incidents.factories import IncidentFactory, UserFactory
|
|
12
|
+
from firefighter.slack.views.modals.closure_reason import ClosureReasonModal
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.django_db
|
|
16
|
+
class TestClosureReasonModalMessageTabDisabled:
|
|
17
|
+
"""Test ClosureReasonModal handles messages_tab_disabled gracefully."""
|
|
18
|
+
|
|
19
|
+
def test_closure_reason_handles_messages_tab_disabled(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
20
|
+
"""Test that messages_tab_disabled error is handled gracefully with warning log."""
|
|
21
|
+
# Create test data
|
|
22
|
+
user = UserFactory.build()
|
|
23
|
+
user.save()
|
|
24
|
+
incident = IncidentFactory.build(_status=IncidentStatus.INVESTIGATING, created_by=user)
|
|
25
|
+
incident.save()
|
|
26
|
+
|
|
27
|
+
# Create modal and mock
|
|
28
|
+
modal = ClosureReasonModal()
|
|
29
|
+
ack = MagicMock()
|
|
30
|
+
|
|
31
|
+
# Create valid form submission
|
|
32
|
+
body = {
|
|
33
|
+
"view": {
|
|
34
|
+
"state": {
|
|
35
|
+
"values": {
|
|
36
|
+
"closure_reason": {
|
|
37
|
+
"select_closure_reason": {
|
|
38
|
+
"selected_option": {"value": ClosureReason.CANCELLED}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"closure_reference": {
|
|
42
|
+
"input_closure_reference": {"value": ""}
|
|
43
|
+
},
|
|
44
|
+
"closure_message": {
|
|
45
|
+
"input_closure_message": {"value": "Test closure message"}
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"private_metadata": str(incident.id),
|
|
50
|
+
},
|
|
51
|
+
"user": {"id": "U123456"},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Mock respond to raise messages_tab_disabled error
|
|
55
|
+
slack_error_response = MagicMock()
|
|
56
|
+
slack_error_response.get.return_value = "messages_tab_disabled"
|
|
57
|
+
|
|
58
|
+
with patch("firefighter.slack.views.modals.closure_reason.respond") as mock_respond:
|
|
59
|
+
mock_respond.side_effect = SlackApiError(
|
|
60
|
+
message="The request to the Slack API failed.",
|
|
61
|
+
response=slack_error_response
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Execute
|
|
65
|
+
result = modal.handle_modal_fn(
|
|
66
|
+
ack=ack,
|
|
67
|
+
body=body,
|
|
68
|
+
incident=incident,
|
|
69
|
+
user=user
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Assertions
|
|
73
|
+
assert result is True # Incident should still be closed successfully
|
|
74
|
+
|
|
75
|
+
# Verify incident was closed
|
|
76
|
+
incident.refresh_from_db()
|
|
77
|
+
assert incident.status == IncidentStatus.CLOSED
|
|
78
|
+
assert incident.closure_reason == ClosureReason.CANCELLED
|
|
79
|
+
|
|
80
|
+
# Verify warning was logged
|
|
81
|
+
assert any(
|
|
82
|
+
"Cannot send DM to user" in record.message and record.levelname == "WARNING"
|
|
83
|
+
for record in caplog.records
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def test_closure_reason_reraises_other_slack_errors(self) -> None:
|
|
87
|
+
"""Test that other Slack API errors are re-raised."""
|
|
88
|
+
# Create test data
|
|
89
|
+
user = UserFactory.build()
|
|
90
|
+
user.save()
|
|
91
|
+
incident = IncidentFactory.build(_status=IncidentStatus.INVESTIGATING, created_by=user)
|
|
92
|
+
incident.save()
|
|
93
|
+
|
|
94
|
+
# Create modal and mock
|
|
95
|
+
modal = ClosureReasonModal()
|
|
96
|
+
ack = MagicMock()
|
|
97
|
+
|
|
98
|
+
# Create valid form submission
|
|
99
|
+
body = {
|
|
100
|
+
"view": {
|
|
101
|
+
"state": {
|
|
102
|
+
"values": {
|
|
103
|
+
"closure_reason": {
|
|
104
|
+
"select_closure_reason": {
|
|
105
|
+
"selected_option": {"value": ClosureReason.CANCELLED}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"closure_reference": {
|
|
109
|
+
"input_closure_reference": {"value": ""}
|
|
110
|
+
},
|
|
111
|
+
"closure_message": {
|
|
112
|
+
"input_closure_message": {"value": "Test closure message"}
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"private_metadata": str(incident.id),
|
|
117
|
+
},
|
|
118
|
+
"user": {"id": "U123456"},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Mock respond to raise different Slack error
|
|
122
|
+
slack_error_response = MagicMock()
|
|
123
|
+
slack_error_response.get.return_value = "channel_not_found"
|
|
124
|
+
|
|
125
|
+
with patch("firefighter.slack.views.modals.closure_reason.respond") as mock_respond:
|
|
126
|
+
mock_respond.side_effect = SlackApiError(
|
|
127
|
+
message="The request to the Slack API failed.",
|
|
128
|
+
response=slack_error_response
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Execute and expect exception
|
|
132
|
+
with pytest.raises(SlackApiError):
|
|
133
|
+
modal.handle_modal_fn(
|
|
134
|
+
ack=ack,
|
|
135
|
+
body=body,
|
|
136
|
+
incident=incident,
|
|
137
|
+
user=user
|
|
138
|
+
)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Tests for form_utils.py support of multiple choice fields."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from django import forms
|
|
6
|
+
|
|
7
|
+
from firefighter.incidents.models import Environment
|
|
8
|
+
from firefighter.slack.views.modals.base_modal.form_utils import (
|
|
9
|
+
SlackForm,
|
|
10
|
+
slack_view_submission_to_dict,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.django_db
|
|
15
|
+
class TestMultipleChoiceFields:
|
|
16
|
+
"""Test that SlackForm correctly handles ModelMultipleChoiceField and MultipleChoiceField."""
|
|
17
|
+
|
|
18
|
+
def test_model_multiple_choice_field_with_initial_list(self, environment_factory):
|
|
19
|
+
"""Test ModelMultipleChoiceField with list of model instances as initial."""
|
|
20
|
+
class TestForm(forms.Form):
|
|
21
|
+
environments = forms.ModelMultipleChoiceField(
|
|
22
|
+
queryset=Environment.objects.all(),
|
|
23
|
+
required=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Create test environments
|
|
27
|
+
env1 = environment_factory(value="PRD", default=True)
|
|
28
|
+
env2 = environment_factory(value="STG", default=True)
|
|
29
|
+
default_envs = [env1, env2]
|
|
30
|
+
|
|
31
|
+
slack_form = SlackForm(TestForm)(initial={"environments": default_envs})
|
|
32
|
+
blocks = slack_form.slack_blocks()
|
|
33
|
+
|
|
34
|
+
# Should generate blocks without errors
|
|
35
|
+
assert len(blocks) > 0
|
|
36
|
+
|
|
37
|
+
# Find the environment block
|
|
38
|
+
env_block = next(b for b in blocks if b.block_id == "environments")
|
|
39
|
+
assert env_block is not None
|
|
40
|
+
|
|
41
|
+
# Check it's a multi-select element
|
|
42
|
+
assert env_block.element.type == "multi_static_select"
|
|
43
|
+
|
|
44
|
+
# Check initial options
|
|
45
|
+
if default_envs:
|
|
46
|
+
assert len(env_block.element.initial_options) == len(default_envs)
|
|
47
|
+
|
|
48
|
+
def test_model_multiple_choice_field_with_callable_initial(self, environment_factory):
|
|
49
|
+
"""Test ModelMultipleChoiceField with callable returning list."""
|
|
50
|
+
# Create test environments
|
|
51
|
+
environment_factory(value="PRD", default=True)
|
|
52
|
+
environment_factory(value="STG", default=True)
|
|
53
|
+
|
|
54
|
+
def get_default_envs():
|
|
55
|
+
return list(Environment.objects.filter(default=True))
|
|
56
|
+
|
|
57
|
+
class TestForm(forms.Form):
|
|
58
|
+
environments = forms.ModelMultipleChoiceField(
|
|
59
|
+
queryset=Environment.objects.all(),
|
|
60
|
+
initial=get_default_envs,
|
|
61
|
+
required=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
slack_form = SlackForm(TestForm)()
|
|
65
|
+
blocks = slack_form.slack_blocks()
|
|
66
|
+
|
|
67
|
+
# Should generate blocks without errors
|
|
68
|
+
assert len(blocks) > 0
|
|
69
|
+
|
|
70
|
+
def test_multiple_choice_field_with_initial_list(self):
|
|
71
|
+
"""Test MultipleChoiceField with list of values as initial."""
|
|
72
|
+
|
|
73
|
+
class TestForm(forms.Form):
|
|
74
|
+
platforms = forms.MultipleChoiceField(
|
|
75
|
+
choices=[
|
|
76
|
+
("FR", "France"),
|
|
77
|
+
("DE", "Germany"),
|
|
78
|
+
("UK", "United Kingdom"),
|
|
79
|
+
],
|
|
80
|
+
initial=["FR", "UK"],
|
|
81
|
+
required=True,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
slack_form = SlackForm(TestForm)()
|
|
85
|
+
blocks = slack_form.slack_blocks()
|
|
86
|
+
|
|
87
|
+
# Should generate blocks without errors
|
|
88
|
+
assert len(blocks) > 0
|
|
89
|
+
|
|
90
|
+
# Find the platforms block
|
|
91
|
+
platform_block = next(b for b in blocks if b.block_id == "platforms")
|
|
92
|
+
assert platform_block is not None
|
|
93
|
+
|
|
94
|
+
# Check it's a multi-select element
|
|
95
|
+
assert platform_block.element.type == "multi_static_select"
|
|
96
|
+
|
|
97
|
+
# Check initial options
|
|
98
|
+
assert len(platform_block.element.initial_options) == 2
|
|
99
|
+
|
|
100
|
+
def test_multiple_choice_field_empty_initial(self):
|
|
101
|
+
"""Test MultipleChoiceField with no initial value."""
|
|
102
|
+
|
|
103
|
+
class TestForm(forms.Form):
|
|
104
|
+
platforms = forms.MultipleChoiceField(
|
|
105
|
+
choices=[
|
|
106
|
+
("FR", "France"),
|
|
107
|
+
("DE", "Germany"),
|
|
108
|
+
],
|
|
109
|
+
required=False,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
slack_form = SlackForm(TestForm)()
|
|
113
|
+
blocks = slack_form.slack_blocks()
|
|
114
|
+
|
|
115
|
+
assert len(blocks) > 0
|
|
116
|
+
|
|
117
|
+
# Find the platforms block
|
|
118
|
+
platform_block = next(b for b in blocks if b.block_id == "platforms")
|
|
119
|
+
|
|
120
|
+
# Should have no initial_options
|
|
121
|
+
assert not hasattr(platform_block.element, "initial_options") or not platform_block.element.initial_options
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.mark.django_db
|
|
125
|
+
class TestSlackViewSubmissionToDict:
|
|
126
|
+
"""Test parsing of multi-select responses from Slack."""
|
|
127
|
+
|
|
128
|
+
def test_multi_static_select_parsing(self):
|
|
129
|
+
"""Test that multi_static_select responses are parsed as lists."""
|
|
130
|
+
body = {
|
|
131
|
+
"view": {
|
|
132
|
+
"state": {
|
|
133
|
+
"values": {
|
|
134
|
+
"environment": {
|
|
135
|
+
"environment": {
|
|
136
|
+
"type": "multi_static_select",
|
|
137
|
+
"selected_options": [
|
|
138
|
+
{"value": "uuid-1"},
|
|
139
|
+
{"value": "uuid-2"},
|
|
140
|
+
],
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
result = slack_view_submission_to_dict(body)
|
|
149
|
+
|
|
150
|
+
assert "environment" in result
|
|
151
|
+
assert isinstance(result["environment"], list)
|
|
152
|
+
assert result["environment"] == ["uuid-1", "uuid-2"]
|
|
153
|
+
|
|
154
|
+
def test_multi_static_select_empty(self):
|
|
155
|
+
"""Test that empty multi_static_select returns empty list."""
|
|
156
|
+
body = {
|
|
157
|
+
"view": {
|
|
158
|
+
"state": {
|
|
159
|
+
"values": {
|
|
160
|
+
"environment": {
|
|
161
|
+
"environment": {
|
|
162
|
+
"type": "multi_static_select",
|
|
163
|
+
"selected_options": [],
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
result = slack_view_submission_to_dict(body)
|
|
172
|
+
|
|
173
|
+
assert "environment" in result
|
|
174
|
+
assert isinstance(result["environment"], list)
|
|
175
|
+
assert result["environment"] == []
|
|
176
|
+
|
|
177
|
+
def test_checkboxes_checked(self):
|
|
178
|
+
"""Test that checked checkboxes (BooleanField) return True."""
|
|
179
|
+
body = {
|
|
180
|
+
"view": {
|
|
181
|
+
"state": {
|
|
182
|
+
"values": {
|
|
183
|
+
"is_key_account": {
|
|
184
|
+
"is_key_account": {
|
|
185
|
+
"type": "checkboxes",
|
|
186
|
+
"selected_options": [
|
|
187
|
+
{"value": "True"},
|
|
188
|
+
],
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
result = slack_view_submission_to_dict(body)
|
|
197
|
+
|
|
198
|
+
assert "is_key_account" in result
|
|
199
|
+
assert result["is_key_account"] is True
|
|
200
|
+
|
|
201
|
+
def test_checkboxes_unchecked(self):
|
|
202
|
+
"""Test that unchecked checkboxes (BooleanField) return False."""
|
|
203
|
+
body = {
|
|
204
|
+
"view": {
|
|
205
|
+
"state": {
|
|
206
|
+
"values": {
|
|
207
|
+
"is_key_account": {
|
|
208
|
+
"is_key_account": {
|
|
209
|
+
"type": "checkboxes",
|
|
210
|
+
"selected_options": [],
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
result = slack_view_submission_to_dict(body)
|
|
219
|
+
|
|
220
|
+
assert "is_key_account" in result
|
|
221
|
+
assert result["is_key_account"] is False
|
|
222
|
+
|
|
223
|
+
def test_multiple_checkboxes(self):
|
|
224
|
+
"""Test that multiple checkboxes are parsed correctly."""
|
|
225
|
+
body = {
|
|
226
|
+
"view": {
|
|
227
|
+
"state": {
|
|
228
|
+
"values": {
|
|
229
|
+
"is_key_account": {
|
|
230
|
+
"is_key_account": {
|
|
231
|
+
"type": "checkboxes",
|
|
232
|
+
"selected_options": [{"value": "True"}],
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
"is_seller_in_golden_list": {
|
|
236
|
+
"is_seller_in_golden_list": {
|
|
237
|
+
"type": "checkboxes",
|
|
238
|
+
"selected_options": [], # Unchecked
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
result = slack_view_submission_to_dict(body)
|
|
247
|
+
|
|
248
|
+
assert result["is_key_account"] is True
|
|
249
|
+
assert result["is_seller_in_golden_list"] is False
|
|
@@ -6,8 +6,10 @@ from unittest.mock import MagicMock, Mock, patch
|
|
|
6
6
|
import pytest
|
|
7
7
|
from slack_sdk.models.blocks.blocks import (
|
|
8
8
|
ContextBlock,
|
|
9
|
+
SectionBlock,
|
|
9
10
|
)
|
|
10
11
|
|
|
12
|
+
from firefighter.incidents.factories import IncidentCategoryFactory
|
|
11
13
|
from firefighter.incidents.forms.create_incident import CreateIncidentFormBase
|
|
12
14
|
from firefighter.incidents.models.user import User
|
|
13
15
|
from firefighter.slack.views.modals.open import OpeningData, OpenModal
|
|
@@ -64,6 +66,7 @@ def test_validate_details_form_valid() -> None:
|
|
|
64
66
|
details_form_modal_class = MagicMock(spec=SetIncidentDetails)
|
|
65
67
|
details_form_class = MagicMock(spec=CreateIncidentFormBase)
|
|
66
68
|
details_form_data = {"key": "value"}
|
|
69
|
+
open_incident_context = build_opening_data()
|
|
67
70
|
|
|
68
71
|
details_form_modal_class.form_class = details_form_class
|
|
69
72
|
|
|
@@ -72,7 +75,7 @@ def test_validate_details_form_valid() -> None:
|
|
|
72
75
|
details_form_class.return_value = details_form_instance
|
|
73
76
|
|
|
74
77
|
is_valid, returned_form_class, returned_form = OpenModal._validate_details_form(
|
|
75
|
-
details_form_modal_class, details_form_data
|
|
78
|
+
details_form_modal_class, details_form_data, open_incident_context
|
|
76
79
|
)
|
|
77
80
|
|
|
78
81
|
assert is_valid is True
|
|
@@ -84,6 +87,7 @@ def test_validate_details_form_invalid() -> None:
|
|
|
84
87
|
details_form_modal_class = MagicMock(spec=SetIncidentDetails)
|
|
85
88
|
details_form_class = MagicMock(spec=CreateIncidentFormBase)
|
|
86
89
|
details_form_data = {"key": "value"}
|
|
90
|
+
open_incident_context = build_opening_data()
|
|
87
91
|
|
|
88
92
|
details_form_modal_class.form_class = details_form_class
|
|
89
93
|
|
|
@@ -92,7 +96,7 @@ def test_validate_details_form_invalid() -> None:
|
|
|
92
96
|
details_form_class.return_value = details_form_instance
|
|
93
97
|
|
|
94
98
|
is_valid, returned_form_class, returned_form = OpenModal._validate_details_form(
|
|
95
|
-
details_form_modal_class, details_form_data
|
|
99
|
+
details_form_modal_class, details_form_data, open_incident_context
|
|
96
100
|
)
|
|
97
101
|
|
|
98
102
|
assert is_valid is False
|
|
@@ -139,3 +143,143 @@ def test_build_modal_fn_empty(user: User) -> None:
|
|
|
139
143
|
|
|
140
144
|
# View should not have a submit button
|
|
141
145
|
assert view.submit is None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@pytest.mark.django_db
|
|
149
|
+
def test_get_done_review_blocks_with_custom_fields(
|
|
150
|
+
user: User, priority_factory, environment_factory
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Test that get_done_review_blocks doesn't crash when form has custom fields."""
|
|
153
|
+
# Use factories to create DB objects
|
|
154
|
+
priority = priority_factory(value=1, default=True)
|
|
155
|
+
environment = environment_factory(value="PRD", default=True)
|
|
156
|
+
category = IncidentCategoryFactory()
|
|
157
|
+
|
|
158
|
+
# Create a mock form with cleaned_data containing custom fields
|
|
159
|
+
mock_form = MagicMock(spec=CreateIncidentFormBase)
|
|
160
|
+
mock_form.cleaned_data = {
|
|
161
|
+
"title": "Test incident with custom fields",
|
|
162
|
+
"description": "Testing custom fields handling",
|
|
163
|
+
"priority": priority,
|
|
164
|
+
"incident_category": category,
|
|
165
|
+
"environment": [environment],
|
|
166
|
+
"platform": ["platform-FR"],
|
|
167
|
+
# Custom fields that should be removed before creating Incident
|
|
168
|
+
"zendesk_ticket_id": "ZD-12345",
|
|
169
|
+
"seller_contract_id": "SELLER-789",
|
|
170
|
+
"zoho_desk_ticket_id": "ZOHO-456",
|
|
171
|
+
"is_key_account": True,
|
|
172
|
+
"is_seller_in_golden_list": False,
|
|
173
|
+
"suggested_team_routing": None,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
open_incident_context = build_opening_data(response_type="critical")
|
|
177
|
+
|
|
178
|
+
# Should not raise TypeError about unexpected keyword arguments
|
|
179
|
+
blocks = OpenModal.get_done_review_blocks(
|
|
180
|
+
open_incident_context,
|
|
181
|
+
user,
|
|
182
|
+
details_form_done=True,
|
|
183
|
+
details_form_class=type(mock_form),
|
|
184
|
+
details_form=mock_form,
|
|
185
|
+
can_submit=True,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Should return blocks (at least the divider and tada block)
|
|
189
|
+
assert len(blocks) >= 2
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@pytest.mark.django_db
|
|
193
|
+
def test_get_done_review_blocks_critical_includes_slack_and_jira_messages(
|
|
194
|
+
user: User, priority_factory, environment_factory
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Test that critical incidents show both Slack channel and Jira ticket messages."""
|
|
197
|
+
# Use factories to create DB objects
|
|
198
|
+
priority = priority_factory(value=1, default=True)
|
|
199
|
+
environment = environment_factory(value="PRD", default=True)
|
|
200
|
+
category = IncidentCategoryFactory()
|
|
201
|
+
|
|
202
|
+
# Create a mock form with cleaned_data
|
|
203
|
+
mock_form = MagicMock(spec=CreateIncidentFormBase)
|
|
204
|
+
mock_form.cleaned_data = {
|
|
205
|
+
"title": "Test critical incident",
|
|
206
|
+
"description": "Testing Slack and Jira messages",
|
|
207
|
+
"priority": priority,
|
|
208
|
+
"incident_category": category,
|
|
209
|
+
"environment": [environment],
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
open_incident_context = build_opening_data(response_type="critical")
|
|
213
|
+
|
|
214
|
+
blocks = OpenModal.get_done_review_blocks(
|
|
215
|
+
open_incident_context,
|
|
216
|
+
user,
|
|
217
|
+
details_form_done=True,
|
|
218
|
+
details_form_class=type(mock_form),
|
|
219
|
+
details_form=mock_form,
|
|
220
|
+
can_submit=True,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Find the SectionBlock containing the message
|
|
224
|
+
section_blocks = [block for block in blocks if isinstance(block, SectionBlock)]
|
|
225
|
+
assert len(section_blocks) >= 1
|
|
226
|
+
|
|
227
|
+
# Get the last SectionBlock which should contain the Slack + Jira message
|
|
228
|
+
message_block = section_blocks[-1].text
|
|
229
|
+
# Extract text string - it could be a string or a MarkdownTextObject
|
|
230
|
+
assert message_block is not None, "SectionBlock text should not be None"
|
|
231
|
+
message_text = message_block.text if hasattr(message_block, "text") else str(message_block)
|
|
232
|
+
|
|
233
|
+
# Verify both Slack and Jira messages are present
|
|
234
|
+
assert ":slack:" in message_text, "Slack channel message should be present for critical incidents"
|
|
235
|
+
assert "A dedicated Slack channel will be created" in message_text
|
|
236
|
+
assert ":jira_new:" in message_text
|
|
237
|
+
assert "An associated Jira ticket will also be created" in message_text
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@pytest.mark.django_db
|
|
241
|
+
def test_get_done_review_blocks_normal_includes_only_jira_message(
|
|
242
|
+
user: User, priority_factory, environment_factory
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Test that normal incidents show only Jira ticket message (no Slack channel)."""
|
|
245
|
+
# Use factories to create DB objects
|
|
246
|
+
priority = priority_factory(value=4, default=False)
|
|
247
|
+
environment = environment_factory(value="PRD", default=True)
|
|
248
|
+
category = IncidentCategoryFactory()
|
|
249
|
+
|
|
250
|
+
# Create a mock form with cleaned_data
|
|
251
|
+
mock_form = MagicMock(spec=CreateIncidentFormBase)
|
|
252
|
+
mock_form.cleaned_data = {
|
|
253
|
+
"title": "Test normal incident",
|
|
254
|
+
"description": "Testing Jira-only message",
|
|
255
|
+
"priority": priority,
|
|
256
|
+
"incident_category": category,
|
|
257
|
+
"environment": [environment],
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
open_incident_context = build_opening_data(response_type="normal")
|
|
261
|
+
|
|
262
|
+
blocks = OpenModal.get_done_review_blocks(
|
|
263
|
+
open_incident_context,
|
|
264
|
+
user,
|
|
265
|
+
details_form_done=True,
|
|
266
|
+
details_form_class=type(mock_form),
|
|
267
|
+
details_form=mock_form,
|
|
268
|
+
can_submit=True,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Find the SectionBlock containing the message
|
|
272
|
+
section_blocks = [block for block in blocks if isinstance(block, SectionBlock)]
|
|
273
|
+
assert len(section_blocks) >= 1
|
|
274
|
+
|
|
275
|
+
# Get the last SectionBlock which should contain only the Jira message
|
|
276
|
+
message_block = section_blocks[-1].text
|
|
277
|
+
# Extract text string - it could be a string or a MarkdownTextObject
|
|
278
|
+
assert message_block is not None, "SectionBlock text should not be None"
|
|
279
|
+
message_text = message_block.text if hasattr(message_block, "text") else str(message_block)
|
|
280
|
+
|
|
281
|
+
# Verify only Jira message is present (no Slack channel mention)
|
|
282
|
+
assert ":slack:" not in message_text, "Slack channel message should NOT be present for normal incidents"
|
|
283
|
+
assert "Slack channel" not in message_text
|
|
284
|
+
assert ":jira_new:" in message_text
|
|
285
|
+
assert "A Jira ticket will be created" in message_text
|