firefighter-incident 0.0.13__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 (136) hide show
  1. firefighter/_version.py +16 -3
  2. firefighter/api/serializers.py +17 -8
  3. firefighter/api/urls.py +8 -1
  4. firefighter/api/views/_base.py +1 -1
  5. firefighter/api/views/components.py +5 -5
  6. firefighter/api/views/incidents.py +9 -9
  7. firefighter/confluence/signals/incident_updated.py +2 -2
  8. firefighter/firefighter/settings/components/raid.py +3 -0
  9. firefighter/incidents/admin.py +24 -24
  10. firefighter/incidents/enums.py +22 -2
  11. firefighter/incidents/factories.py +14 -5
  12. firefighter/incidents/forms/close_incident.py +4 -4
  13. firefighter/incidents/forms/closure_reason.py +45 -0
  14. firefighter/incidents/forms/create_incident.py +4 -4
  15. firefighter/incidents/forms/unified_incident.py +406 -0
  16. firefighter/incidents/forms/update_status.py +91 -5
  17. firefighter/incidents/menus.py +2 -2
  18. firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +7 -5
  19. firefighter/incidents/migrations/0009_update_sla.py +7 -5
  20. firefighter/incidents/migrations/0020_create_incident_category_model.py +64 -0
  21. firefighter/incidents/migrations/0021_copy_component_data_to_incident_category.py +57 -0
  22. firefighter/incidents/migrations/0022_add_incident_category_fields.py +34 -0
  23. firefighter/incidents/migrations/0023_populate_incident_category_references.py +57 -0
  24. firefighter/incidents/migrations/0024_remove_component_fields_and_model.py +26 -0
  25. firefighter/incidents/migrations/0025_make_incident_category_required.py +24 -0
  26. firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py +39 -0
  27. firefighter/incidents/migrations/0027_add_closure_fields.py +40 -0
  28. firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
  29. firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
  30. firefighter/incidents/models/__init__.py +1 -1
  31. firefighter/incidents/models/group.py +1 -1
  32. firefighter/incidents/models/incident.py +47 -20
  33. firefighter/incidents/models/{component.py → incident_category.py} +30 -29
  34. firefighter/incidents/models/incident_update.py +3 -3
  35. firefighter/incidents/static/css/main.min.css +1 -1
  36. firefighter/incidents/tables.py +9 -9
  37. firefighter/incidents/templates/layouts/partials/incident_card.html +1 -1
  38. firefighter/incidents/templates/layouts/partials/incident_timeline.html +2 -2
  39. firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
  40. firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html} +13 -13
  41. firefighter/incidents/templates/pages/{component_list.html → incident_category_list.html} +2 -2
  42. firefighter/incidents/templates/pages/incident_detail.html +3 -3
  43. firefighter/incidents/urls.py +6 -6
  44. firefighter/incidents/views/components/details.py +9 -9
  45. firefighter/incidents/views/components/list.py +9 -9
  46. firefighter/incidents/views/reports.py +5 -5
  47. firefighter/incidents/views/users/details.py +2 -2
  48. firefighter/incidents/views/views.py +7 -7
  49. firefighter/jira_app/client.py +1 -1
  50. firefighter/logging/custom_json_formatter.py +2 -1
  51. firefighter/pagerduty/tasks/trigger_oncall.py +1 -1
  52. firefighter/raid/admin.py +0 -11
  53. firefighter/raid/apps.py +9 -26
  54. firefighter/raid/client.py +5 -5
  55. firefighter/raid/forms.py +84 -213
  56. firefighter/raid/migrations/0003_delete_raidarea.py +16 -0
  57. firefighter/raid/models.py +2 -21
  58. firefighter/raid/serializers.py +5 -4
  59. firefighter/raid/service.py +29 -27
  60. firefighter/raid/signals/incident_created.py +42 -15
  61. firefighter/raid/signals/incident_updated.py +3 -2
  62. firefighter/raid/utils.py +1 -1
  63. firefighter/raid/views/__init__.py +1 -1
  64. firefighter/slack/admin.py +8 -8
  65. firefighter/slack/management/commands/switch_test_users.py +272 -0
  66. firefighter/slack/messages/slack_messages.py +24 -9
  67. firefighter/slack/migrations/0005_add_incident_categories_fields.py +33 -0
  68. firefighter/slack/migrations/0006_copy_components_to_incident_categories.py +57 -0
  69. firefighter/slack/migrations/0007_remove_components_fields.py +22 -0
  70. firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py +33 -0
  71. firefighter/slack/models/conversation.py +3 -3
  72. firefighter/slack/models/incident_channel.py +1 -1
  73. firefighter/slack/models/user.py +1 -1
  74. firefighter/slack/models/user_group.py +3 -3
  75. firefighter/slack/rules.py +2 -2
  76. firefighter/slack/signals/create_incident_conversation.py +6 -0
  77. firefighter/slack/signals/get_users.py +2 -2
  78. firefighter/slack/signals/incident_updated.py +8 -2
  79. firefighter/slack/utils.py +2 -2
  80. firefighter/slack/views/events/home.py +2 -2
  81. firefighter/slack/views/modals/__init__.py +4 -0
  82. firefighter/slack/views/modals/base_modal/form_utils.py +78 -0
  83. firefighter/slack/views/modals/close.py +18 -5
  84. firefighter/slack/views/modals/closure_reason.py +193 -0
  85. firefighter/slack/views/modals/open.py +83 -12
  86. firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
  87. firefighter/slack/views/modals/opening/details/unified.py +203 -0
  88. firefighter/slack/views/modals/opening/select_impact.py +5 -2
  89. firefighter/slack/views/modals/opening/set_details.py +3 -2
  90. firefighter/slack/views/modals/postmortem.py +10 -2
  91. firefighter/slack/views/modals/update_status.py +32 -6
  92. firefighter/slack/views/modals/utils.py +51 -0
  93. firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
  94. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/METADATA +2 -2
  95. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/RECORD +133 -88
  96. firefighter_tests/conftest.py +4 -5
  97. firefighter_tests/test_api/test_api_landbot.py +1 -1
  98. firefighter_tests/test_firefighter/test_sso.py +146 -0
  99. firefighter_tests/test_incidents/test_enums.py +100 -0
  100. firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
  101. firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
  102. firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
  103. firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
  104. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
  105. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
  106. firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
  107. firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
  108. firefighter_tests/test_incidents/test_incident_urls.py +3 -3
  109. firefighter_tests/test_incidents/test_models/test_incident_category.py +165 -0
  110. firefighter_tests/test_incidents/test_models/test_incident_model.py +70 -2
  111. firefighter_tests/test_raid/conftest.py +154 -0
  112. firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
  113. firefighter_tests/test_raid/test_priority_mapping.py +267 -0
  114. firefighter_tests/test_raid/test_raid_client.py +580 -0
  115. firefighter_tests/test_raid/test_raid_forms.py +552 -0
  116. firefighter_tests/test_raid/test_raid_models.py +185 -0
  117. firefighter_tests/test_raid/test_raid_serializers.py +507 -0
  118. firefighter_tests/test_raid/test_raid_service.py +442 -0
  119. firefighter_tests/test_raid/test_raid_signals.py +187 -0
  120. firefighter_tests/test_raid/test_raid_views.py +196 -0
  121. firefighter_tests/test_slack/messages/__init__.py +0 -0
  122. firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
  123. firefighter_tests/test_slack/views/modals/conftest.py +140 -0
  124. firefighter_tests/test_slack/views/modals/test_close.py +71 -9
  125. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
  126. firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
  127. firefighter_tests/test_slack/views/modals/test_open.py +146 -2
  128. firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
  129. firefighter_tests/test_slack/views/modals/test_update_status.py +331 -7
  130. firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
  131. firefighter/raid/views/open_normal.py +0 -139
  132. firefighter/slack/views/modals/opening/details/critical.py +0 -88
  133. firefighter_fixtures/raid/area.json +0 -1
  134. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/WHEEL +0 -0
  135. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/entry_points.txt +0 -0
  136. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/licenses/LICENSE +0 -0
@@ -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
@@ -0,0 +1,421 @@
1
+ """Tests for unified incident opening modal."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+
6
+ import pytest
7
+
8
+ from firefighter.incidents.models.impact import LevelChoices
9
+ from firefighter.slack.views.modals.base_modal.form_utils import SlackForm
10
+ from firefighter.slack.views.modals.opening.details.unified import (
11
+ OpeningUnifiedModal,
12
+ UnifiedIncidentFormSlack,
13
+ )
14
+
15
+
16
+ @pytest.mark.django_db
17
+ class TestUnifiedIncidentFormSlack:
18
+ """Test Slack-specific version of unified form."""
19
+
20
+ def test_form_initializes_with_impacts_and_response_type(
21
+ self, priority_factory, impact_level_factory
22
+ ):
23
+ """Form should properly receive impacts_data and response_type."""
24
+ priority_factory(value=1, default=True)
25
+ customer_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Customers")
26
+
27
+ impacts_data = {"customers_impact": customer_impact}
28
+
29
+ form = UnifiedIncidentFormSlack(
30
+ impacts_data=impacts_data,
31
+ response_type="critical",
32
+ )
33
+
34
+ # Should configure field visibility
35
+ assert "zendesk_ticket_id" in form.fields
36
+ assert "suggested_team_routing" not in form.fields
37
+
38
+ def test_slack_blocks_generation_with_multiple_choice_fields(
39
+ self, priority_factory, environment_factory
40
+ ):
41
+ """Slack blocks should generate correctly with multiple choice fields."""
42
+ priority_factory(value=1, default=True)
43
+ environment_factory(value="PRD", default=True)
44
+ environment_factory(value="STG", default=False)
45
+
46
+ # Use SlackForm wrapper to generate blocks
47
+ slack_form = SlackForm(UnifiedIncidentFormSlack)(
48
+ impacts_data={},
49
+ response_type="critical",
50
+ )
51
+
52
+ # Should generate blocks without errors
53
+ blocks = slack_form.slack_blocks()
54
+ assert len(blocks) > 0
55
+
56
+ # Find environment block (multi-select)
57
+ env_blocks = [b for b in blocks if hasattr(b, "block_id") and b.block_id == "environment"]
58
+ assert len(env_blocks) == 1
59
+
60
+ env_block = env_blocks[0]
61
+ assert env_block.element.type == "multi_static_select"
62
+
63
+ def test_priority_field_is_hidden(self, priority_factory):
64
+ """Priority field should not appear in Slack blocks."""
65
+ priority_factory(value=1, default=True)
66
+
67
+ # Use SlackForm wrapper to generate blocks
68
+ slack_form = SlackForm(UnifiedIncidentFormSlack)(
69
+ impacts_data={},
70
+ response_type="critical",
71
+ )
72
+
73
+ blocks = slack_form.slack_blocks()
74
+
75
+ # Priority field should not generate a block (it's hidden)
76
+ priority_blocks = [
77
+ b for b in blocks if hasattr(b, "block_id") and b.block_id == "priority"
78
+ ]
79
+ assert len(priority_blocks) == 0
80
+
81
+
82
+ @pytest.mark.django_db
83
+ class TestOpeningUnifiedModal:
84
+ """Test the unified opening modal."""
85
+
86
+ def test_build_modal_fn_passes_context_to_form(
87
+ self, priority_factory, impact_level_factory
88
+ ):
89
+ """build_modal_fn should pass impacts and response_type to form."""
90
+ priority_factory(value=1, default=True)
91
+ customer_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Customers")
92
+
93
+ modal = OpeningUnifiedModal()
94
+
95
+ open_incident_context = {
96
+ "response_type": "critical",
97
+ "impact_form_data": {"customers_impact": customer_impact},
98
+ "details_form_data": {},
99
+ }
100
+
101
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
102
+
103
+ # Should build view without errors
104
+ assert view is not None
105
+ assert view.type == "modal"
106
+ assert len(view.blocks) > 0
107
+
108
+ # Check private_metadata contains our context
109
+ metadata = json.loads(view.private_metadata)
110
+ assert metadata["response_type"] == "critical"
111
+ assert "impact_form_data" in metadata
112
+
113
+ def test_build_modal_fn_critical_incident_hides_feature_team(
114
+ self, priority_factory
115
+ ):
116
+ """Critical incident should not show feature team field."""
117
+ priority_factory(value=1, default=True)
118
+
119
+ modal = OpeningUnifiedModal()
120
+
121
+ open_incident_context = {
122
+ "response_type": "critical",
123
+ "impact_form_data": {},
124
+ "details_form_data": {},
125
+ }
126
+
127
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
128
+
129
+ # Check that feature team field is not in blocks
130
+ feature_team_blocks = [
131
+ b
132
+ for b in view.blocks
133
+ if hasattr(b, "block_id") and b.block_id == "suggested_team_routing"
134
+ ]
135
+ assert len(feature_team_blocks) == 0
136
+
137
+ def test_build_modal_fn_normal_incident_shows_feature_team(
138
+ self, priority_factory
139
+ ):
140
+ """Normal incident (P4-P5) should show feature team field."""
141
+ priority_factory(value=4, default=True)
142
+
143
+ modal = OpeningUnifiedModal()
144
+
145
+ open_incident_context = {
146
+ "response_type": "normal",
147
+ "impact_form_data": {},
148
+ "details_form_data": {},
149
+ }
150
+
151
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
152
+
153
+ # Check that feature team field IS in blocks
154
+ feature_team_blocks = [
155
+ b
156
+ for b in view.blocks
157
+ if hasattr(b, "block_id") and b.block_id == "suggested_team_routing"
158
+ ]
159
+ assert len(feature_team_blocks) == 1
160
+
161
+ def test_build_modal_fn_customer_impact_shows_zendesk(
162
+ self, priority_factory, impact_level_factory
163
+ ):
164
+ """Customer impact should show Zendesk field."""
165
+ priority_factory(value=1, default=True)
166
+ customer_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Customers")
167
+
168
+ modal = OpeningUnifiedModal()
169
+
170
+ open_incident_context = {
171
+ "response_type": "critical",
172
+ "impact_form_data": {"customers_impact": customer_impact},
173
+ "details_form_data": {},
174
+ }
175
+
176
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
177
+
178
+ # Check that zendesk field IS in blocks
179
+ zendesk_blocks = [
180
+ b
181
+ for b in view.blocks
182
+ if hasattr(b, "block_id") and b.block_id == "zendesk_ticket_id"
183
+ ]
184
+ assert len(zendesk_blocks) == 1
185
+
186
+ def test_build_modal_fn_seller_impact_shows_seller_fields(
187
+ self, priority_factory, impact_level_factory
188
+ ):
189
+ """Seller impact should show seller-specific fields."""
190
+ priority_factory(value=1, default=True)
191
+ seller_impact = impact_level_factory(value=LevelChoices.LOW, impact__name="Sellers")
192
+
193
+ modal = OpeningUnifiedModal()
194
+
195
+ open_incident_context = {
196
+ "response_type": "critical",
197
+ "impact_form_data": {"sellers_impact": seller_impact},
198
+ "details_form_data": {},
199
+ }
200
+
201
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
202
+
203
+ # Check seller fields are in blocks
204
+ seller_contract_blocks = [
205
+ b
206
+ for b in view.blocks
207
+ if hasattr(b, "block_id") and b.block_id == "seller_contract_id"
208
+ ]
209
+ assert len(seller_contract_blocks) == 1
210
+
211
+ zoho_blocks = [
212
+ b
213
+ for b in view.blocks
214
+ if hasattr(b, "block_id") and b.block_id == "zoho_desk_ticket_id"
215
+ ]
216
+ assert len(zoho_blocks) == 1
217
+
218
+ def test_build_modal_fn_business_impact_only_hides_customer_seller_fields(
219
+ self, priority_factory, impact_level_factory
220
+ ):
221
+ """Business impact only should not show customer/seller fields."""
222
+ priority_factory(value=1, default=True)
223
+ business_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Business")
224
+
225
+ modal = OpeningUnifiedModal()
226
+
227
+ open_incident_context = {
228
+ "response_type": "critical",
229
+ "impact_form_data": {"business_impact": business_impact},
230
+ "details_form_data": {},
231
+ }
232
+
233
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
234
+
235
+ # Should NOT show customer or seller fields
236
+ zendesk_blocks = [
237
+ b
238
+ for b in view.blocks
239
+ if hasattr(b, "block_id") and b.block_id == "zendesk_ticket_id"
240
+ ]
241
+ assert len(zendesk_blocks) == 0
242
+
243
+ seller_blocks = [
244
+ b
245
+ for b in view.blocks
246
+ if hasattr(b, "block_id") and b.block_id == "seller_contract_id"
247
+ ]
248
+ assert len(seller_blocks) == 0
249
+
250
+ def test_build_modal_fn_employee_impact_only_hides_customer_seller_fields(
251
+ self, priority_factory, impact_level_factory
252
+ ):
253
+ """Employee impact only should not show customer/seller fields."""
254
+ priority_factory(value=1, default=True)
255
+ employee_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Employees")
256
+
257
+ modal = OpeningUnifiedModal()
258
+
259
+ open_incident_context = {
260
+ "response_type": "critical",
261
+ "impact_form_data": {"employees_impact": employee_impact},
262
+ "details_form_data": {},
263
+ }
264
+
265
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
266
+
267
+ # Should NOT show customer or seller fields
268
+ zendesk_blocks = [
269
+ b
270
+ for b in view.blocks
271
+ if hasattr(b, "block_id") and b.block_id == "zendesk_ticket_id"
272
+ ]
273
+ assert len(zendesk_blocks) == 0
274
+
275
+ seller_blocks = [
276
+ b
277
+ for b in view.blocks
278
+ if hasattr(b, "block_id") and b.block_id == "seller_contract_id"
279
+ ]
280
+ assert len(seller_blocks) == 0
281
+
282
+ def test_build_modal_fn_all_impacts_shows_all_fields(
283
+ self, priority_factory, impact_level_factory
284
+ ):
285
+ """All impact types should show all specific fields."""
286
+ priority_factory(value=1, default=True)
287
+ customer_impact = impact_level_factory(value=LevelChoices.LOW, impact__name="Customers")
288
+ seller_impact = impact_level_factory(value=LevelChoices.LOW, impact__name="Sellers")
289
+ employee_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Employees")
290
+ business_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Business")
291
+
292
+ modal = OpeningUnifiedModal()
293
+
294
+ open_incident_context = {
295
+ "response_type": "critical",
296
+ "impact_form_data": {
297
+ "customers_impact": customer_impact,
298
+ "sellers_impact": seller_impact,
299
+ "employees_impact": employee_impact,
300
+ "business_impact": business_impact,
301
+ },
302
+ "details_form_data": {},
303
+ }
304
+
305
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
306
+
307
+ # Should show both customer and seller fields
308
+ zendesk_blocks = [
309
+ b
310
+ for b in view.blocks
311
+ if hasattr(b, "block_id") and b.block_id == "zendesk_ticket_id"
312
+ ]
313
+ assert len(zendesk_blocks) == 1
314
+
315
+ seller_blocks = [
316
+ b
317
+ for b in view.blocks
318
+ if hasattr(b, "block_id") and b.block_id == "seller_contract_id"
319
+ ]
320
+ assert len(seller_blocks) == 1
321
+
322
+ def test_build_modal_fn_none_impacts_hides_all_fields(
323
+ self, priority_factory, impact_level_factory
324
+ ):
325
+ """All impacts at NONE level should not show any specific fields."""
326
+ priority_factory(value=1, default=True)
327
+ customer_impact = impact_level_factory(value=LevelChoices.NONE, impact__name="Customers")
328
+ seller_impact = impact_level_factory(value=LevelChoices.NONE, impact__name="Sellers")
329
+
330
+ modal = OpeningUnifiedModal()
331
+
332
+ open_incident_context = {
333
+ "response_type": "critical",
334
+ "impact_form_data": {
335
+ "customers_impact": customer_impact,
336
+ "sellers_impact": seller_impact,
337
+ },
338
+ "details_form_data": {},
339
+ }
340
+
341
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
342
+
343
+ # Should NOT show any specific fields
344
+ zendesk_blocks = [
345
+ b
346
+ for b in view.blocks
347
+ if hasattr(b, "block_id") and b.block_id == "zendesk_ticket_id"
348
+ ]
349
+ assert len(zendesk_blocks) == 0
350
+
351
+ seller_blocks = [
352
+ b
353
+ for b in view.blocks
354
+ if hasattr(b, "block_id") and b.block_id == "seller_contract_id"
355
+ ]
356
+ assert len(seller_blocks) == 0
357
+
358
+ def test_build_modal_fn_normal_with_customer_shows_team_and_zendesk(
359
+ self, priority_factory, impact_level_factory
360
+ ):
361
+ """P4-P5 with customer impact should show both feature team and Zendesk."""
362
+ priority_factory(value=4, default=True)
363
+ customer_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Customers")
364
+
365
+ modal = OpeningUnifiedModal()
366
+
367
+ open_incident_context = {
368
+ "response_type": "normal",
369
+ "impact_form_data": {"customers_impact": customer_impact},
370
+ "details_form_data": {},
371
+ }
372
+
373
+ view = modal.build_modal_fn(open_incident_context=open_incident_context)
374
+
375
+ # Should show both fields
376
+ team_blocks = [
377
+ b
378
+ for b in view.blocks
379
+ if hasattr(b, "block_id") and b.block_id == "suggested_team_routing"
380
+ ]
381
+ assert len(team_blocks) == 1
382
+
383
+ zendesk_blocks = [
384
+ b
385
+ for b in view.blocks
386
+ if hasattr(b, "block_id") and b.block_id == "zendesk_ticket_id"
387
+ ]
388
+ assert len(zendesk_blocks) == 1
389
+
390
+ def test_handle_modal_fn_preserves_custom_fields_in_validation(
391
+ self, priority_factory, impact_level_factory
392
+ ):
393
+ """Form validation during submission should preserve custom fields based on impact context."""
394
+ # Setup
395
+ priority = priority_factory(value=1, default=True)
396
+ customer_impact = impact_level_factory(value=LevelChoices.HIGH, impact__name="Customers")
397
+
398
+ modal = OpeningUnifiedModal()
399
+
400
+ # Simulate private_metadata as it would be in the modal submission
401
+ private_metadata = {
402
+ "response_type": "critical",
403
+ "impact_form_data": {"customers_impact": str(customer_impact.id)},
404
+ "details_form_data": {"priority": str(priority.id)},
405
+ }
406
+
407
+ # This is what handle_modal_fn now does (with our fix)
408
+ # It passes open_incident_context to the form initialization
409
+ slack_form = modal.get_form_class()(
410
+ data={}, # Empty data for this test - we just want to verify field visibility
411
+ open_incident_context=private_metadata,
412
+ )
413
+
414
+ form = slack_form.form
415
+
416
+ # Verify that zendesk_ticket_id field exists in the form
417
+ # This proves the context was passed correctly and field visibility was configured
418
+ assert "zendesk_ticket_id" in form.fields, "zendesk_ticket_id should be present in form fields when customer impact is selected"
419
+
420
+ # Verify that suggested_team_routing is NOT in the form (critical incident)
421
+ assert "suggested_team_routing" not in form.fields, "suggested_team_routing should not be present for critical incidents"