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.
Files changed (64) 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 +60 -13
  29. firefighter/slack/views/modals/opening/details/unified.py +203 -0
  30. firefighter/slack/views/modals/opening/select_impact.py +1 -1
  31. firefighter/slack/views/modals/opening/set_details.py +3 -2
  32. firefighter/slack/views/modals/postmortem.py +10 -2
  33. firefighter/slack/views/modals/update_status.py +28 -2
  34. firefighter/slack/views/modals/utils.py +51 -0
  35. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/METADATA +1 -1
  36. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/RECORD +62 -38
  37. firefighter_tests/test_incidents/test_enums.py +100 -0
  38. firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
  39. firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
  40. firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
  41. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
  42. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
  43. firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
  44. firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
  45. firefighter_tests/test_incidents/test_models/test_incident_model.py +68 -0
  46. firefighter_tests/test_raid/conftest.py +154 -0
  47. firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
  48. firefighter_tests/test_raid/test_raid_forms.py +10 -253
  49. firefighter_tests/test_raid/test_raid_signals.py +187 -0
  50. firefighter_tests/test_slack/messages/__init__.py +0 -0
  51. firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
  52. firefighter_tests/test_slack/views/modals/conftest.py +140 -0
  53. firefighter_tests/test_slack/views/modals/test_close.py +65 -3
  54. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
  55. firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
  56. firefighter_tests/test_slack/views/modals/test_open.py +146 -2
  57. firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
  58. firefighter_tests/test_slack/views/modals/test_update_status.py +327 -3
  59. firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
  60. firefighter/raid/views/open_normal.py +0 -139
  61. firefighter/slack/views/modals/opening/details/critical.py +0 -88
  62. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/WHEEL +0 -0
  63. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/entry_points.txt +0 -0
  64. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,154 @@
1
+ """Pytest fixtures for RAID tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import factory
6
+ import pytest
7
+ from factory.django import DjangoModelFactory
8
+
9
+ from firefighter.incidents.factories import (
10
+ IncidentCategoryFactory,
11
+ IncidentFactory,
12
+ UserFactory,
13
+ )
14
+ from firefighter.incidents.models import Environment, Priority
15
+
16
+
17
+ class JiraUserFactoryClass(DjangoModelFactory):
18
+ """Factory for JiraUser model."""
19
+
20
+ class Meta:
21
+ model = "jira_app.JiraUser"
22
+ django_get_or_create = ("id",)
23
+
24
+ id = factory.Sequence(lambda n: f"jira-user-{n}")
25
+ user = factory.SubFactory(UserFactory)
26
+
27
+
28
+ class JiraTicketFactory(DjangoModelFactory):
29
+ """Factory for JiraTicket model."""
30
+
31
+ class Meta:
32
+ model = "raid.JiraTicket"
33
+
34
+ id = factory.Sequence(lambda n: 10000 + n)
35
+ key = factory.Sequence(lambda n: f"INC-{n}")
36
+ summary = "Test Jira Ticket"
37
+ description = "Test description for Jira ticket"
38
+ reporter = factory.SubFactory(JiraUserFactoryClass)
39
+ incident = None
40
+
41
+
42
+ @pytest.fixture
43
+ def user_factory():
44
+ """Pytest fixture for UserFactory."""
45
+ return UserFactory
46
+
47
+
48
+ @pytest.fixture
49
+ def incident_factory():
50
+ """Pytest fixture for IncidentFactory."""
51
+ return IncidentFactory
52
+
53
+
54
+ @pytest.fixture
55
+ def jira_user_factory():
56
+ """Pytest fixture for JiraUserFactory."""
57
+ return JiraUserFactoryClass
58
+
59
+
60
+ @pytest.fixture
61
+ def jira_ticket_factory():
62
+ """Pytest fixture for JiraTicketFactory."""
63
+ return JiraTicketFactory
64
+
65
+
66
+ @pytest.fixture
67
+ def priority_factory(db):
68
+ """Factory to create Priority instances."""
69
+
70
+ def _create(**kwargs):
71
+ value = kwargs.get("value", 1)
72
+ name = kwargs.get("name", f"P{value}")
73
+ set_as_default = kwargs.get("default", False)
74
+
75
+ # If default=True, clear any other defaults first
76
+ if set_as_default:
77
+ Priority.objects.filter(default=True).update(default=False)
78
+
79
+ defaults = {
80
+ "emoji": "🔴",
81
+ "order": value,
82
+ "default": set_as_default,
83
+ "enabled_create": True,
84
+ "enabled_update": True,
85
+ "needs_postmortem": value <= 2, # P1-P2 need postmortem
86
+ }
87
+ # Remove name and value from kwargs if present
88
+ kwargs_copy = kwargs.copy()
89
+ kwargs_copy.pop("name", None)
90
+ kwargs_copy.pop("value", None)
91
+ defaults.update(kwargs_copy)
92
+
93
+ priority, created = Priority.objects.get_or_create(
94
+ name=name,
95
+ value=value,
96
+ defaults=defaults,
97
+ )
98
+
99
+ # If already exists and we want it as default, just set that
100
+ if not created and set_as_default:
101
+ priority.default = True
102
+ priority.save(update_fields=["default"])
103
+
104
+ return priority
105
+
106
+ return _create
107
+
108
+
109
+ @pytest.fixture
110
+ def environment_factory(db):
111
+ """Factory to create Environment instances."""
112
+
113
+ def _create(**kwargs):
114
+ value = kwargs.get("value", "TST")
115
+ set_as_default = kwargs.get("default", False)
116
+
117
+ # If default=True, clear any other defaults first
118
+ if set_as_default:
119
+ Environment.objects.filter(default=True).update(default=False)
120
+
121
+ defaults = {
122
+ "description": f"Environment {value}",
123
+ "order": 1,
124
+ "default": set_as_default,
125
+ }
126
+ # Remove value and default from kwargs if present
127
+ kwargs_copy = kwargs.copy()
128
+ kwargs_copy.pop("value", None)
129
+ kwargs_copy.pop("default", None)
130
+ defaults.update(kwargs_copy)
131
+
132
+ environment, created = Environment.objects.get_or_create(
133
+ value=value,
134
+ defaults=defaults,
135
+ )
136
+
137
+ # If already exists and we want it as default, just set that
138
+ if not created and set_as_default:
139
+ environment.default = True
140
+ environment.save(update_fields=["default"])
141
+
142
+ return environment
143
+
144
+ return _create
145
+
146
+
147
+ @pytest.fixture
148
+ def incident_category_factory(db):
149
+ """Factory to create IncidentCategory instances."""
150
+
151
+ def _create(**kwargs):
152
+ return IncidentCategoryFactory(**kwargs)
153
+
154
+ return _create
@@ -0,0 +1,372 @@
1
+ """Tests for P1-P3 (critical) incident Jira ticket creation with all custom fields.
2
+
3
+ This test suite verifies that P1-P3 incidents properly pass all custom fields
4
+ to Jira when creating tickets via the incident_channel_done signal, including:
5
+ - environments (customfield_11049)
6
+ - platform (customfield_10201)
7
+ - business_impact (customfield_10936)
8
+ - customer/seller specific fields from jira_extra_fields
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from unittest.mock import MagicMock, PropertyMock, patch
13
+
14
+ import pytest
15
+
16
+ from firefighter.incidents.forms.unified_incident import UnifiedIncidentForm
17
+ from firefighter.incidents.models.impact import ImpactLevel, ImpactType, LevelChoices
18
+ from firefighter.incidents.signals import create_incident_conversation
19
+ from firefighter.jira_app.client import client
20
+ from firefighter.jira_app.models import JiraUser
21
+ from firefighter.raid.signals.incident_created import create_ticket
22
+
23
+
24
+ @pytest.mark.django_db
25
+ class TestP1P2P3JiraTicketFields:
26
+ """Test P1-P3 incident Jira ticket creation includes all custom fields."""
27
+
28
+ def test_p1_with_customer_impact_creates_jira_with_all_fields(
29
+ self,
30
+ priority_factory,
31
+ environment_factory,
32
+ incident_category_factory,
33
+ user_factory,
34
+ ):
35
+ """P1 customer impact should create Jira with zendesk, environments, platform, business_impact."""
36
+ # Setup P1 priority
37
+ p1_priority = priority_factory(value=1, name="P1", default=True)
38
+ env_prd = environment_factory(value="PRD", default=True)
39
+ env_stg = environment_factory(value="STG", default=False)
40
+ category = incident_category_factory()
41
+ user = user_factory()
42
+
43
+ # Get customer impact
44
+ customers_impact_type = ImpactType.objects.get(value="customers_impact")
45
+ customer_impact = ImpactLevel.objects.get(
46
+ impact_type=customers_impact_type,
47
+ value=LevelChoices.HIGHEST.value
48
+ )
49
+
50
+ # Form data
51
+ form_data = {
52
+ "title": "P1 Critical customer issue",
53
+ "description": "Test P1 critical incident",
54
+ "incident_category": category.id,
55
+ "environment": [env_prd.id, env_stg.id], # Multiple environments!
56
+ "platform": ["platform-FR", "platform-DE"], # Multiple platforms!
57
+ "priority": p1_priority.id,
58
+ "zendesk_ticket_id": "ZD-CRITICAL-123",
59
+ }
60
+
61
+ impacts_data = {
62
+ "customers_impact": customer_impact, # Pass object for get_business_impact()
63
+ }
64
+
65
+ form = UnifiedIncidentForm(form_data)
66
+ assert form.is_valid(), f"Form should be valid. Errors: {form.errors}"
67
+
68
+ # Setup mock channel
69
+ mock_channel = MagicMock()
70
+ mock_channel.channel_id = "C123456"
71
+
72
+ # Mock the signal send to intercept kwargs and directly call create_ticket handler
73
+ def mock_signal_send(sender, incident, **kwargs):
74
+ # Skip calling the real signal handler (which would call Slack API)
75
+ # Instead, directly call the JIRA ticket creation handler
76
+ create_ticket(
77
+ sender="test",
78
+ incident=incident,
79
+ channel=mock_channel,
80
+ jira_extra_fields=kwargs.get("jira_extra_fields", {}),
81
+ impacts_data=kwargs.get("impacts_data", {}),
82
+ )
83
+
84
+ # Mock Jira client property at the CLASS level to prevent real connection
85
+ mock_jira_client = MagicMock()
86
+
87
+ with (
88
+ patch.object(create_incident_conversation, "send", side_effect=mock_signal_send),
89
+ patch.object(type(client), "jira", new_callable=PropertyMock, return_value=mock_jira_client),
90
+ patch("firefighter.raid.signals.incident_created.client.create_issue") as mock_jira_create,
91
+ patch("firefighter.raid.signals.incident_created.client.get_jira_user_from_jira_id") as mock_get_default_jira_user,
92
+ patch("firefighter.raid.signals.incident_created.get_jira_user_from_user") as mock_get_jira_user,
93
+ patch("firefighter.raid.forms.get_business_impact") as mock_get_business_impact,
94
+ patch("firefighter.incidents.forms.unified_incident.SelectImpactForm.save"),
95
+ patch("firefighter.raid.signals.incident_created.JiraTicket.objects.create"),
96
+ ):
97
+ # Mock Jira ticket creation (format from _jira_object)
98
+ mock_jira_create.return_value = {
99
+ "id": 99999,
100
+ "key": "P1-TEST-123",
101
+ "project_key": "P1",
102
+ "assignee_id": None,
103
+ "reporter_id": "test_account",
104
+ "description": "Test",
105
+ "summary": "Test",
106
+ "issue_type": "Incident",
107
+ "business_impact": "",
108
+ }
109
+ mock_jira_user = JiraUser(id="test_account")
110
+ mock_get_jira_user.return_value = mock_jira_user
111
+ mock_default_jira_user = JiraUser(id="default_account")
112
+ mock_get_default_jira_user.return_value = mock_default_jira_user
113
+ mock_get_business_impact.return_value = "High"
114
+
115
+ # Trigger the P1-P3 workflow
116
+ form.trigger_incident_workflow(
117
+ creator=user,
118
+ impacts_data=impacts_data,
119
+ response_type="critical",
120
+ )
121
+
122
+ # Verify Jira create_issue was called
123
+ assert mock_jira_create.called, "Jira create_issue should have been called"
124
+
125
+ # Get the call arguments
126
+ call_kwargs = mock_jira_create.call_args.kwargs
127
+
128
+ # ✅ CRITICAL ASSERTIONS - These will FAIL initially
129
+ assert "environments" in call_kwargs, "environments should be passed to Jira for P1-P3"
130
+ # P1-P3 use first environment only (from non-deterministic QuerySet order)
131
+ assert len(call_kwargs["environments"]) == 1, "Should pass exactly one environment"
132
+ assert call_kwargs["environments"][0] in {"PRD", "STG"}, "Should pass one of the form environments"
133
+
134
+ assert "platform" in call_kwargs, "platform should be passed to Jira for P1-P3"
135
+ assert call_kwargs["platform"] == "platform-FR", "Should pass first platform value"
136
+
137
+ assert "business_impact" in call_kwargs, "business_impact should be passed to Jira for P1-P3"
138
+ assert call_kwargs["business_impact"] is not None, "Business impact should be computed from customer impact"
139
+
140
+ # Verify zendesk is passed via jira_extra_fields
141
+ assert "zendesk_ticket_id" in call_kwargs
142
+ assert call_kwargs["zendesk_ticket_id"] == "ZD-CRITICAL-123"
143
+
144
+ def test_p2_with_seller_impact_creates_jira_with_all_fields(
145
+ self,
146
+ priority_factory,
147
+ environment_factory,
148
+ incident_category_factory,
149
+ user_factory,
150
+ ):
151
+ """P2 seller impact should create Jira with seller fields + environments."""
152
+ p2_priority = priority_factory(value=2, name="P2", default=False)
153
+ env_prd = environment_factory(value="PRD", default=True)
154
+ category = incident_category_factory()
155
+ user = user_factory()
156
+
157
+ # Get seller impact
158
+ sellers_impact_type = ImpactType.objects.get(value="sellers_impact")
159
+ seller_impact = ImpactLevel.objects.get(
160
+ impact_type=sellers_impact_type,
161
+ value=LevelChoices.HIGH.value
162
+ )
163
+
164
+ form_data = {
165
+ "title": "P2 Critical seller issue",
166
+ "description": "Test P2 seller incident",
167
+ "incident_category": category.id,
168
+ "environment": [env_prd.id],
169
+ "platform": ["platform-FR"],
170
+ "priority": p2_priority.id,
171
+ "seller_contract_id": "SC-CRITICAL-999",
172
+ "zoho_desk_ticket_id": "ZOHO-CRITICAL-888",
173
+ "is_key_account": True,
174
+ "is_seller_in_golden_list": True,
175
+ }
176
+
177
+ impacts_data = {
178
+ "sellers_impact": seller_impact, # Pass object for get_business_impact()
179
+ }
180
+
181
+ form = UnifiedIncidentForm(form_data)
182
+ assert form.is_valid(), f"Form should be valid. Errors: {form.errors}"
183
+
184
+ # Setup mock channel
185
+ mock_channel = MagicMock()
186
+ mock_channel.channel_id = "C789012"
187
+
188
+ # Mock the signal send to intercept kwargs and directly call create_ticket handler
189
+ def mock_signal_send(sender, incident, **kwargs):
190
+ # Skip calling the real signal handler (which would call Slack API)
191
+ # Instead, directly call the JIRA ticket creation handler
192
+ create_ticket(
193
+ sender="test",
194
+ incident=incident,
195
+ channel=mock_channel,
196
+ jira_extra_fields=kwargs.get("jira_extra_fields", {}),
197
+ impacts_data=kwargs.get("impacts_data", {}),
198
+ )
199
+
200
+ # Mock Jira client property at the CLASS level to prevent real connection
201
+ mock_jira_client = MagicMock()
202
+
203
+ with (
204
+ patch.object(create_incident_conversation, "send", side_effect=mock_signal_send),
205
+ patch.object(type(client), "jira", new_callable=PropertyMock, return_value=mock_jira_client),
206
+ patch("firefighter.raid.signals.incident_created.client.create_issue") as mock_jira_create,
207
+ patch("firefighter.raid.signals.incident_created.client.get_jira_user_from_jira_id") as mock_get_default_jira_user,
208
+ patch("firefighter.raid.signals.incident_created.get_jira_user_from_user") as mock_get_jira_user,
209
+ patch("firefighter.raid.forms.get_business_impact") as mock_get_business_impact,
210
+ patch("firefighter.incidents.forms.unified_incident.SelectImpactForm.save"),
211
+ patch("firefighter.raid.signals.incident_created.JiraTicket.objects.create"),
212
+ ):
213
+ mock_jira_create.return_value = {
214
+ "id": 88888,
215
+ "key": "P2-SELLER-456",
216
+ "project_key": "P2",
217
+ "assignee_id": None,
218
+ "reporter_id": "test_account",
219
+ "description": "Test",
220
+ "summary": "Test",
221
+ "issue_type": "Incident",
222
+ "business_impact": "",
223
+ }
224
+ mock_jira_user = JiraUser(id="test_account")
225
+ mock_get_jira_user.return_value = mock_jira_user
226
+ mock_default_jira_user = JiraUser(id="default_account")
227
+ mock_get_default_jira_user.return_value = mock_default_jira_user
228
+ mock_get_business_impact.return_value = "High"
229
+
230
+ form.trigger_incident_workflow(
231
+ creator=user,
232
+ impacts_data=impacts_data,
233
+ response_type="critical",
234
+ )
235
+
236
+ call_kwargs = mock_jira_create.call_args.kwargs
237
+
238
+ # ✅ CRITICAL: environments must be passed with exact value
239
+ assert "environments" in call_kwargs, "environments should be passed to Jira for P2"
240
+ assert call_kwargs["environments"] == ["PRD"], "Should pass environment value"
241
+
242
+ # ✅ Verify platform and business_impact with exact values
243
+ assert "platform" in call_kwargs, "platform should be passed to Jira for P2"
244
+ assert call_kwargs["platform"] == "platform-FR", "Should pass platform value"
245
+ assert "business_impact" in call_kwargs, "business_impact should be passed to Jira for P2"
246
+ assert call_kwargs["business_impact"] is not None, "Business impact should be computed"
247
+
248
+ # ✅ Verify seller-specific fields
249
+ assert "seller_contract_id" in call_kwargs
250
+ assert call_kwargs["seller_contract_id"] == "SC-CRITICAL-999"
251
+ assert "zoho_desk_ticket_id" in call_kwargs
252
+ assert call_kwargs["zoho_desk_ticket_id"] == "ZOHO-CRITICAL-888"
253
+ assert "is_key_account" in call_kwargs
254
+ assert call_kwargs["is_key_account"] is True
255
+ assert "is_seller_in_golden_list" in call_kwargs
256
+ assert call_kwargs["is_seller_in_golden_list"] is True
257
+
258
+ def test_p3_with_both_impacts_creates_jira_with_all_fields(
259
+ self,
260
+ priority_factory,
261
+ environment_factory,
262
+ incident_category_factory,
263
+ user_factory,
264
+ ):
265
+ """P3 with both customer and seller impact should pass all fields to Jira."""
266
+ p3_priority = priority_factory(value=3, name="P3", default=False)
267
+ env_prd = environment_factory(value="PRD", default=True)
268
+ env_stg = environment_factory(value="STG", default=False)
269
+ category = incident_category_factory()
270
+ user = user_factory()
271
+
272
+ # Get both impacts
273
+ customers_impact_type = ImpactType.objects.get(value="customers_impact")
274
+ customer_impact = ImpactLevel.objects.get(
275
+ impact_type=customers_impact_type,
276
+ value=LevelChoices.MEDIUM.value
277
+ )
278
+ sellers_impact_type = ImpactType.objects.get(value="sellers_impact")
279
+ seller_impact = ImpactLevel.objects.get(
280
+ impact_type=sellers_impact_type,
281
+ value=LevelChoices.LOW.value
282
+ )
283
+
284
+ form_data = {
285
+ "title": "P3 Combined customer and seller",
286
+ "description": "Test P3 with multiple impacts",
287
+ "incident_category": category.id,
288
+ "environment": [env_prd.id, env_stg.id],
289
+ "platform": ["platform-All"],
290
+ "priority": p3_priority.id,
291
+ "zendesk_ticket_id": "ZD-P3-111",
292
+ "seller_contract_id": "SC-P3-222",
293
+ "zoho_desk_ticket_id": "ZOHO-P3-333",
294
+ "is_key_account": False,
295
+ "is_seller_in_golden_list": False,
296
+ }
297
+
298
+ impacts_data = {
299
+ "customers_impact": customer_impact, # Pass object for get_business_impact()
300
+ "sellers_impact": seller_impact, # Pass object for get_business_impact()
301
+ }
302
+
303
+ form = UnifiedIncidentForm(form_data)
304
+ assert form.is_valid(), f"Form should be valid. Errors: {form.errors}"
305
+
306
+ # Setup mock channel
307
+ mock_channel = MagicMock()
308
+ mock_channel.channel_id = "C111222"
309
+
310
+ # Mock the signal send to intercept kwargs and directly call create_ticket handler
311
+ def mock_signal_send(sender, incident, **kwargs):
312
+ # Skip calling the real signal handler (which would call Slack API)
313
+ # Instead, directly call the JIRA ticket creation handler
314
+ create_ticket(
315
+ sender="test",
316
+ incident=incident,
317
+ channel=mock_channel,
318
+ jira_extra_fields=kwargs.get("jira_extra_fields", {}),
319
+ impacts_data=kwargs.get("impacts_data", {}),
320
+ )
321
+
322
+ # Mock Jira client property at the CLASS level to prevent real connection
323
+ mock_jira_client = MagicMock()
324
+
325
+ with (
326
+ patch.object(create_incident_conversation, "send", side_effect=mock_signal_send),
327
+ patch.object(type(client), "jira", new_callable=PropertyMock, return_value=mock_jira_client),
328
+ patch("firefighter.raid.signals.incident_created.client.create_issue") as mock_jira_create,
329
+ patch("firefighter.raid.signals.incident_created.client.get_jira_user_from_jira_id") as mock_get_default_jira_user,
330
+ patch("firefighter.raid.signals.incident_created.get_jira_user_from_user") as mock_get_jira_user,
331
+ patch("firefighter.raid.forms.get_business_impact") as mock_get_business_impact,
332
+ patch("firefighter.incidents.forms.unified_incident.SelectImpactForm.save"),
333
+ patch("firefighter.raid.signals.incident_created.JiraTicket.objects.create"),
334
+ ):
335
+ mock_jira_create.return_value = {
336
+ "id": 77777,
337
+ "key": "P3-COMBO-789",
338
+ "project_key": "P3",
339
+ "assignee_id": None,
340
+ "reporter_id": "test_account",
341
+ "description": "Test",
342
+ "summary": "Test",
343
+ "issue_type": "Incident",
344
+ "business_impact": "",
345
+ }
346
+ mock_jira_user = JiraUser(id="test_account")
347
+ mock_get_jira_user.return_value = mock_jira_user
348
+ mock_default_jira_user = JiraUser(id="default_account")
349
+ mock_get_default_jira_user.return_value = mock_default_jira_user
350
+ mock_get_business_impact.return_value = "High"
351
+
352
+ form.trigger_incident_workflow(
353
+ creator=user,
354
+ impacts_data=impacts_data,
355
+ response_type="critical",
356
+ )
357
+
358
+ call_kwargs = mock_jira_create.call_args.kwargs
359
+
360
+ # ✅ All fields should be present with exact values
361
+ assert "environments" in call_kwargs, "environments should be passed to Jira for P3"
362
+ # P1-P3 use first environment only (from non-deterministic QuerySet order)
363
+ assert len(call_kwargs["environments"]) == 1, "Should pass exactly one environment"
364
+ assert call_kwargs["environments"][0] in {"PRD", "STG"}, "Should pass one of the form environments"
365
+ assert "platform" in call_kwargs, "platform should be passed to Jira for P3"
366
+ assert call_kwargs["platform"] == "platform-All", "Should pass first platform value"
367
+ assert "business_impact" in call_kwargs, "business_impact should be passed to Jira for P3"
368
+ assert call_kwargs["business_impact"] is not None, "Business impact should be computed"
369
+ assert "zendesk_ticket_id" in call_kwargs
370
+ assert call_kwargs["zendesk_ticket_id"] == "ZD-P3-111"
371
+ assert "seller_contract_id" in call_kwargs
372
+ assert call_kwargs["seller_contract_id"] == "SC-P3-222"