firefighter-incident 0.0.14__py3-none-any.whl → 0.0.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. firefighter/_version.py +2 -2
  2. firefighter/api/serializers.py +9 -0
  3. firefighter/confluence/signals/incident_updated.py +2 -2
  4. firefighter/incidents/enums.py +22 -2
  5. firefighter/incidents/forms/closure_reason.py +45 -0
  6. firefighter/incidents/forms/unified_incident.py +406 -0
  7. firefighter/incidents/forms/update_status.py +87 -1
  8. firefighter/incidents/migrations/0027_add_closure_fields.py +40 -0
  9. firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
  10. firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
  11. firefighter/incidents/models/incident.py +32 -5
  12. firefighter/incidents/static/css/main.min.css +1 -1
  13. firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
  14. firefighter/incidents/views/reports.py +3 -3
  15. firefighter/raid/apps.py +9 -26
  16. firefighter/raid/client.py +2 -2
  17. firefighter/raid/forms.py +75 -238
  18. firefighter/raid/signals/incident_created.py +38 -13
  19. firefighter/raid/signals/incident_updated.py +3 -2
  20. firefighter/slack/messages/slack_messages.py +19 -4
  21. firefighter/slack/rules.py +1 -1
  22. firefighter/slack/signals/create_incident_conversation.py +6 -0
  23. firefighter/slack/signals/incident_updated.py +7 -1
  24. firefighter/slack/views/modals/__init__.py +4 -0
  25. firefighter/slack/views/modals/base_modal/form_utils.py +63 -0
  26. firefighter/slack/views/modals/close.py +15 -2
  27. firefighter/slack/views/modals/closure_reason.py +193 -0
  28. firefighter/slack/views/modals/open.py +59 -12
  29. firefighter/slack/views/modals/opening/details/unified.py +203 -0
  30. firefighter/slack/views/modals/opening/set_details.py +3 -2
  31. firefighter/slack/views/modals/postmortem.py +10 -2
  32. firefighter/slack/views/modals/update_status.py +28 -2
  33. firefighter/slack/views/modals/utils.py +51 -0
  34. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.15.dist-info}/METADATA +1 -1
  35. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.15.dist-info}/RECORD +61 -37
  36. firefighter_tests/test_incidents/test_enums.py +100 -0
  37. firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
  38. firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
  39. firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
  40. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
  41. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
  42. firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
  43. firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
  44. firefighter_tests/test_incidents/test_models/test_incident_model.py +68 -0
  45. firefighter_tests/test_raid/conftest.py +154 -0
  46. firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
  47. firefighter_tests/test_raid/test_raid_forms.py +10 -253
  48. firefighter_tests/test_raid/test_raid_signals.py +187 -0
  49. firefighter_tests/test_slack/messages/__init__.py +0 -0
  50. firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
  51. firefighter_tests/test_slack/views/modals/conftest.py +140 -0
  52. firefighter_tests/test_slack/views/modals/test_close.py +65 -3
  53. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
  54. firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
  55. firefighter_tests/test_slack/views/modals/test_open.py +146 -2
  56. firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
  57. firefighter_tests/test_slack/views/modals/test_update_status.py +327 -3
  58. firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
  59. firefighter/raid/views/open_normal.py +0 -139
  60. firefighter/slack/views/modals/opening/details/critical.py +0 -88
  61. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.15.dist-info}/WHEEL +0 -0
  62. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.15.dist-info}/entry_points.txt +0 -0
  63. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.15.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