firefighter-incident 0.0.16__py3-none-any.whl → 0.0.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firefighter/_version.py +2 -2
- firefighter/incidents/forms/edit.py +5 -3
- firefighter/incidents/forms/unified_incident.py +180 -56
- firefighter/incidents/forms/update_status.py +94 -58
- firefighter/incidents/forms/utils.py +14 -0
- firefighter/incidents/models/incident.py +3 -2
- firefighter/raid/apps.py +0 -1
- firefighter/slack/signals/__init__.py +16 -0
- firefighter/slack/signals/incident_updated.py +43 -1
- firefighter/slack/utils.py +43 -6
- firefighter/slack/views/modals/base_modal/form_utils.py +3 -1
- firefighter/slack/views/modals/downgrade_workflow.py +3 -1
- firefighter/slack/views/modals/edit.py +53 -7
- firefighter/slack/views/modals/opening/set_details.py +20 -0
- firefighter_fixtures/incidents/priorities.json +1 -1
- {firefighter_incident-0.0.16.dist-info → firefighter_incident-0.0.17.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.16.dist-info → firefighter_incident-0.0.17.dist-info}/RECORD +28 -29
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +160 -23
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +38 -60
- firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +35 -20
- firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +8 -6
- firefighter_tests/test_slack/test_signals_downgrade.py +147 -0
- firefighter_tests/test_slack/views/modals/test_edit.py +324 -0
- firefighter_tests/test_slack/views/modals/test_opening_unified.py +42 -0
- firefighter_tests/test_slack/views/modals/test_update_status.py +72 -2
- firefighter/raid/signals/incident_created.py +0 -129
- firefighter_tests/test_raid/test_p1_p3_jira_fields.py +0 -372
- firefighter_tests/test_raid/test_priority_mapping.py +0 -267
- {firefighter_incident-0.0.16.dist-info → firefighter_incident-0.0.17.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.16.dist-info → firefighter_incident-0.0.17.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.16.dist-info → firefighter_incident-0.0.17.dist-info}/licenses/LICENSE +0 -0
|
@@ -17,6 +17,7 @@ import pytest
|
|
|
17
17
|
from firefighter.incidents.forms.unified_incident import UnifiedIncidentForm
|
|
18
18
|
from firefighter.incidents.models.impact import ImpactLevel, ImpactType, LevelChoices
|
|
19
19
|
from firefighter.incidents.signals import create_incident_conversation
|
|
20
|
+
from firefighter.slack.messages.base import SlackMessageStrategy
|
|
20
21
|
from firefighter.slack.views.modals.open import OpenModal
|
|
21
22
|
from firefighter.slack.views.modals.opening.details.unified import (
|
|
22
23
|
OpeningUnifiedModal,
|
|
@@ -24,6 +25,55 @@ from firefighter.slack.views.modals.opening.details.unified import (
|
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
def create_mock_slack_message(incident):
|
|
29
|
+
"""Create a mock SlackMessageIncidentDeclaredAnnouncement with proper attributes."""
|
|
30
|
+
mock_slack_message_instance = MagicMock()
|
|
31
|
+
mock_slack_message_instance.strategy = SlackMessageStrategy.APPEND
|
|
32
|
+
mock_slack_message_instance.id = "test_message"
|
|
33
|
+
mock_slack_message_instance.incident = incident # Store the real incident
|
|
34
|
+
mock_slack_message_instance.incident_update = None # Declarations don't have incident updates
|
|
35
|
+
mock_slack_message_instance.get_slack_message_params.return_value = {
|
|
36
|
+
"text": "Test incident declared",
|
|
37
|
+
"blocks": []
|
|
38
|
+
}
|
|
39
|
+
return mock_slack_message_instance
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def setup_jira_mocks(mock_select_impact_used, mock_select_impact_source,
|
|
43
|
+
mock_get_jira_user, mock_jira_client, mock_prepare_fields,
|
|
44
|
+
mock_jira_ticket_model, mock_slack_announcement):
|
|
45
|
+
"""Configure all Jira-related mocks for the unified workflow."""
|
|
46
|
+
# Mock SelectImpactForm - we need to configure BOTH patches
|
|
47
|
+
mock_form_instance = MagicMock()
|
|
48
|
+
mock_form_instance.save.return_value = None
|
|
49
|
+
mock_form_instance.is_valid.return_value = True
|
|
50
|
+
mock_select_impact_used.return_value = mock_form_instance
|
|
51
|
+
mock_select_impact_source.return_value = mock_form_instance
|
|
52
|
+
|
|
53
|
+
# Mock Jira user lookup
|
|
54
|
+
mock_jira_user = MagicMock()
|
|
55
|
+
mock_jira_user.id = "jira-user-123"
|
|
56
|
+
mock_get_jira_user.return_value = mock_jira_user
|
|
57
|
+
|
|
58
|
+
# Mock prepare_jira_fields
|
|
59
|
+
mock_prepare_fields.return_value = {"summary": "Test", "project": {"key": "TEST"}}
|
|
60
|
+
|
|
61
|
+
# Mock Jira API create_issue call
|
|
62
|
+
mock_jira_client.create_issue.return_value = {
|
|
63
|
+
"issue_key": "TEST-123",
|
|
64
|
+
"issue_url": "https://jira.example.com/browse/TEST-123",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Mock JiraTicket creation
|
|
68
|
+
mock_jira_ticket = MagicMock()
|
|
69
|
+
mock_jira_ticket.url = "https://jira.example.com/browse/TEST-123"
|
|
70
|
+
mock_jira_ticket.id = "TEST-123"
|
|
71
|
+
mock_jira_ticket_model.objects.create.return_value = mock_jira_ticket
|
|
72
|
+
|
|
73
|
+
# Mock SlackMessageIncidentDeclaredAnnouncement
|
|
74
|
+
mock_slack_announcement.side_effect = create_mock_slack_message
|
|
75
|
+
|
|
76
|
+
|
|
27
77
|
@pytest.mark.django_db
|
|
28
78
|
class TestUnifiedIncidentFormCustomFieldsPropagation:
|
|
29
79
|
"""Test end-to-end propagation of custom fields from form to signal."""
|
|
@@ -90,11 +140,23 @@ class TestUnifiedIncidentFormCustomFieldsPropagation:
|
|
|
90
140
|
create_incident_conversation.connect(capture_signal, weak=False)
|
|
91
141
|
|
|
92
142
|
try:
|
|
93
|
-
# Mock
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
143
|
+
# Mock Jira-related calls in unified workflow
|
|
144
|
+
# SelectImpactForm appears in TWO places and needs TWO patches:
|
|
145
|
+
# 1. Module-level import (line 11) → patch where it's USED: unified_incident.SelectImpactForm
|
|
146
|
+
# 2. Lazy import inside _create_jira_ticket (line 414) → patch SOURCE: select_impact.SelectImpactForm
|
|
147
|
+
# Other Jira imports are lazy, so we patch them at source modules
|
|
148
|
+
with patch("firefighter.incidents.forms.unified_incident.SelectImpactForm") as mock_select_impact_used, \
|
|
149
|
+
patch("firefighter.incidents.forms.select_impact.SelectImpactForm") as mock_select_impact_source, \
|
|
150
|
+
patch("firefighter.raid.service.get_jira_user_from_user") as mock_get_jira_user, \
|
|
151
|
+
patch("firefighter.raid.client.client") as mock_jira_client, \
|
|
152
|
+
patch("firefighter.raid.forms.prepare_jira_fields") as mock_prepare_fields, \
|
|
153
|
+
patch("firefighter.raid.forms.set_jira_ticket_watchers_raid"), \
|
|
154
|
+
patch("firefighter.raid.models.JiraTicket") as mock_jira_ticket_model, \
|
|
155
|
+
patch("firefighter.slack.messages.slack_messages.SlackMessageIncidentDeclaredAnnouncement") as mock_slack_announcement:
|
|
156
|
+
|
|
157
|
+
setup_jira_mocks(mock_select_impact_used, mock_select_impact_source,
|
|
158
|
+
mock_get_jira_user, mock_jira_client, mock_prepare_fields,
|
|
159
|
+
mock_jira_ticket_model, mock_slack_announcement)
|
|
98
160
|
|
|
99
161
|
# Trigger workflow
|
|
100
162
|
user = user_factory()
|
|
@@ -193,11 +255,23 @@ class TestUnifiedIncidentFormCustomFieldsPropagation:
|
|
|
193
255
|
create_incident_conversation.connect(capture_signal, weak=False)
|
|
194
256
|
|
|
195
257
|
try:
|
|
196
|
-
# Mock
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
258
|
+
# Mock Jira-related calls in unified workflow
|
|
259
|
+
# SelectImpactForm appears in TWO places and needs TWO patches:
|
|
260
|
+
# 1. Module-level import (line 11) → patch where it's USED: unified_incident.SelectImpactForm
|
|
261
|
+
# 2. Lazy import inside _create_jira_ticket (line 414) → patch SOURCE: select_impact.SelectImpactForm
|
|
262
|
+
# Other Jira imports are lazy, so we patch them at source modules
|
|
263
|
+
with patch("firefighter.incidents.forms.unified_incident.SelectImpactForm") as mock_select_impact_used, \
|
|
264
|
+
patch("firefighter.incidents.forms.select_impact.SelectImpactForm") as mock_select_impact_source, \
|
|
265
|
+
patch("firefighter.raid.service.get_jira_user_from_user") as mock_get_jira_user, \
|
|
266
|
+
patch("firefighter.raid.client.client") as mock_jira_client, \
|
|
267
|
+
patch("firefighter.raid.forms.prepare_jira_fields") as mock_prepare_fields, \
|
|
268
|
+
patch("firefighter.raid.forms.set_jira_ticket_watchers_raid"), \
|
|
269
|
+
patch("firefighter.raid.models.JiraTicket") as mock_jira_ticket_model, \
|
|
270
|
+
patch("firefighter.slack.messages.slack_messages.SlackMessageIncidentDeclaredAnnouncement") as mock_slack_announcement:
|
|
271
|
+
|
|
272
|
+
setup_jira_mocks(mock_select_impact_used, mock_select_impact_source,
|
|
273
|
+
mock_get_jira_user, mock_jira_client, mock_prepare_fields,
|
|
274
|
+
mock_jira_ticket_model, mock_slack_announcement)
|
|
201
275
|
|
|
202
276
|
# Trigger workflow
|
|
203
277
|
user = user_factory()
|
|
@@ -303,11 +377,23 @@ class TestUnifiedIncidentFormCustomFieldsPropagation:
|
|
|
303
377
|
create_incident_conversation.connect(capture_signal, weak=False)
|
|
304
378
|
|
|
305
379
|
try:
|
|
306
|
-
# Mock
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
380
|
+
# Mock Jira-related calls in unified workflow
|
|
381
|
+
# SelectImpactForm appears in TWO places and needs TWO patches:
|
|
382
|
+
# 1. Module-level import (line 11) → patch where it's USED: unified_incident.SelectImpactForm
|
|
383
|
+
# 2. Lazy import inside _create_jira_ticket (line 414) → patch SOURCE: select_impact.SelectImpactForm
|
|
384
|
+
# Other Jira imports are lazy, so we patch them at source modules
|
|
385
|
+
with patch("firefighter.incidents.forms.unified_incident.SelectImpactForm") as mock_select_impact_used, \
|
|
386
|
+
patch("firefighter.incidents.forms.select_impact.SelectImpactForm") as mock_select_impact_source, \
|
|
387
|
+
patch("firefighter.raid.service.get_jira_user_from_user") as mock_get_jira_user, \
|
|
388
|
+
patch("firefighter.raid.client.client") as mock_jira_client, \
|
|
389
|
+
patch("firefighter.raid.forms.prepare_jira_fields") as mock_prepare_fields, \
|
|
390
|
+
patch("firefighter.raid.forms.set_jira_ticket_watchers_raid"), \
|
|
391
|
+
patch("firefighter.raid.models.JiraTicket") as mock_jira_ticket_model, \
|
|
392
|
+
patch("firefighter.slack.messages.slack_messages.SlackMessageIncidentDeclaredAnnouncement") as mock_slack_announcement:
|
|
393
|
+
|
|
394
|
+
setup_jira_mocks(mock_select_impact_used, mock_select_impact_source,
|
|
395
|
+
mock_get_jira_user, mock_jira_client, mock_prepare_fields,
|
|
396
|
+
mock_jira_ticket_model, mock_slack_announcement)
|
|
311
397
|
|
|
312
398
|
# Trigger workflow
|
|
313
399
|
user = user_factory()
|
|
@@ -396,11 +482,23 @@ class TestUnifiedIncidentFormCustomFieldsPropagation:
|
|
|
396
482
|
create_incident_conversation.connect(capture_signal, weak=False)
|
|
397
483
|
|
|
398
484
|
try:
|
|
399
|
-
# Mock
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
485
|
+
# Mock Jira-related calls in unified workflow
|
|
486
|
+
# SelectImpactForm appears in TWO places and needs TWO patches:
|
|
487
|
+
# 1. Module-level import (line 11) → patch where it's USED: unified_incident.SelectImpactForm
|
|
488
|
+
# 2. Lazy import inside _create_jira_ticket (line 414) → patch SOURCE: select_impact.SelectImpactForm
|
|
489
|
+
# Other Jira imports are lazy, so we patch them at source modules
|
|
490
|
+
with patch("firefighter.incidents.forms.unified_incident.SelectImpactForm") as mock_select_impact_used, \
|
|
491
|
+
patch("firefighter.incidents.forms.select_impact.SelectImpactForm") as mock_select_impact_source, \
|
|
492
|
+
patch("firefighter.raid.service.get_jira_user_from_user") as mock_get_jira_user, \
|
|
493
|
+
patch("firefighter.raid.client.client") as mock_jira_client, \
|
|
494
|
+
patch("firefighter.raid.forms.prepare_jira_fields") as mock_prepare_fields, \
|
|
495
|
+
patch("firefighter.raid.forms.set_jira_ticket_watchers_raid"), \
|
|
496
|
+
patch("firefighter.raid.models.JiraTicket") as mock_jira_ticket_model, \
|
|
497
|
+
patch("firefighter.slack.messages.slack_messages.SlackMessageIncidentDeclaredAnnouncement") as mock_slack_announcement:
|
|
498
|
+
|
|
499
|
+
setup_jira_mocks(mock_select_impact_used, mock_select_impact_source,
|
|
500
|
+
mock_get_jira_user, mock_jira_client, mock_prepare_fields,
|
|
501
|
+
mock_jira_ticket_model, mock_slack_announcement)
|
|
404
502
|
|
|
405
503
|
# Trigger workflow
|
|
406
504
|
user = user_factory()
|
|
@@ -561,11 +659,50 @@ class TestOpenModalPreservesCustomFieldsContext:
|
|
|
561
659
|
create_incident_conversation.connect(capture_signal, weak=False)
|
|
562
660
|
|
|
563
661
|
try:
|
|
564
|
-
# Mock
|
|
565
|
-
|
|
662
|
+
# Mock Jira-related calls in unified workflow
|
|
663
|
+
# SelectImpactForm appears in TWO places and needs TWO patches:
|
|
664
|
+
# 1. Module-level import (line 11) → patch where it's USED: unified_incident.SelectImpactForm
|
|
665
|
+
# 2. Lazy import inside _create_jira_ticket (line 414) → patch SOURCE: select_impact.SelectImpactForm
|
|
666
|
+
# Other Jira imports are lazy, so we patch them at source modules
|
|
667
|
+
with patch("firefighter.incidents.forms.unified_incident.SelectImpactForm") as mock_select_impact_used, \
|
|
668
|
+
patch("firefighter.incidents.forms.select_impact.SelectImpactForm") as mock_select_impact_source, \
|
|
669
|
+
patch("firefighter.raid.service.get_jira_user_from_user") as mock_get_jira_user, \
|
|
670
|
+
patch("firefighter.raid.client.client") as mock_jira_client, \
|
|
671
|
+
patch("firefighter.raid.forms.prepare_jira_fields") as mock_prepare_fields, \
|
|
672
|
+
patch("firefighter.raid.forms.set_jira_ticket_watchers_raid"), \
|
|
673
|
+
patch("firefighter.raid.models.JiraTicket") as mock_jira_ticket_model, \
|
|
674
|
+
patch("firefighter.slack.messages.slack_messages.SlackMessageIncidentDeclaredAnnouncement") as mock_slack_announcement:
|
|
675
|
+
|
|
676
|
+
# Mock SelectImpactForm - we need to configure BOTH patches
|
|
677
|
+
# They both return the same mock instance
|
|
566
678
|
mock_form_instance = MagicMock()
|
|
567
679
|
mock_form_instance.save.return_value = None
|
|
568
|
-
|
|
680
|
+
mock_form_instance.is_valid.return_value = True
|
|
681
|
+
mock_select_impact_used.return_value = mock_form_instance
|
|
682
|
+
mock_select_impact_source.return_value = mock_form_instance
|
|
683
|
+
|
|
684
|
+
# Mock Jira user lookup
|
|
685
|
+
mock_jira_user = MagicMock()
|
|
686
|
+
mock_jira_user.id = "jira-user-123"
|
|
687
|
+
mock_get_jira_user.return_value = mock_jira_user
|
|
688
|
+
|
|
689
|
+
# Mock prepare_jira_fields to return valid fields
|
|
690
|
+
mock_prepare_fields.return_value = {"summary": "Test", "project": {"key": "TEST"}}
|
|
691
|
+
|
|
692
|
+
# Mock Jira API create_issue call
|
|
693
|
+
mock_jira_client.create_issue.return_value = {
|
|
694
|
+
"issue_key": "TEST-123",
|
|
695
|
+
"issue_url": "https://jira.example.com/browse/TEST-123",
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
# Mock JiraTicket creation - must have .url attribute for bookmarks
|
|
699
|
+
mock_jira_ticket = MagicMock()
|
|
700
|
+
mock_jira_ticket.url = "https://jira.example.com/browse/TEST-123"
|
|
701
|
+
mock_jira_ticket.id = "TEST-123"
|
|
702
|
+
mock_jira_ticket_model.objects.create.return_value = mock_jira_ticket
|
|
703
|
+
|
|
704
|
+
# Mock SlackMessageIncidentDeclaredAnnouncement
|
|
705
|
+
mock_slack_announcement.side_effect = create_mock_slack_message
|
|
569
706
|
|
|
570
707
|
user = user_factory()
|
|
571
708
|
form.trigger_incident_workflow(
|
|
@@ -97,25 +97,20 @@ class TestP4P5CustomerImpactJiraFields:
|
|
|
97
97
|
assert form.is_valid(), f"Form should be valid. Errors: {form.errors}"
|
|
98
98
|
|
|
99
99
|
# Mock the Jira client to capture what's passed
|
|
100
|
+
# Note: The unified workflow imports 'client' from firefighter.raid.client
|
|
100
101
|
with (
|
|
101
|
-
patch("firefighter.raid.
|
|
102
|
+
patch("firefighter.raid.client.client") as mock_jira_client,
|
|
102
103
|
patch("firefighter.raid.service.get_jira_user_from_user") as mock_get_jira_user,
|
|
103
|
-
patch("firefighter.
|
|
104
|
+
patch("firefighter.incidents.forms.unified_incident.SelectImpactForm"),
|
|
105
|
+
patch("firefighter.incidents.forms.select_impact.SelectImpactForm"),
|
|
104
106
|
patch("firefighter.raid.forms.set_jira_ticket_watchers_raid"),
|
|
105
107
|
patch("firefighter.raid.forms.alert_slack_new_jira_ticket"),
|
|
106
|
-
patch("firefighter.raid.
|
|
108
|
+
patch("firefighter.raid.models.JiraTicket"),
|
|
107
109
|
):
|
|
108
|
-
# Mock
|
|
109
|
-
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"project_key": "TEST",
|
|
113
|
-
"assignee_id": None,
|
|
114
|
-
"reporter_id": "test_account",
|
|
115
|
-
"description": "Test description",
|
|
116
|
-
"summary": "Test summary",
|
|
117
|
-
"issue_type": "Incident",
|
|
118
|
-
"business_impact": "",
|
|
110
|
+
# Mock Jira client create_issue method
|
|
111
|
+
mock_jira_client.create_issue.return_value = {
|
|
112
|
+
"issue_key": "TEST-123",
|
|
113
|
+
"issue_url": "https://jira.example.com/browse/TEST-123",
|
|
119
114
|
}
|
|
120
115
|
mock_jira_user = JiraUser(id="test_account")
|
|
121
116
|
mock_get_jira_user.return_value = mock_jira_user
|
|
@@ -128,10 +123,10 @@ class TestP4P5CustomerImpactJiraFields:
|
|
|
128
123
|
)
|
|
129
124
|
|
|
130
125
|
# Verify create_issue was called
|
|
131
|
-
assert
|
|
126
|
+
assert mock_jira_client.create_issue.called, "Jira create_issue should have been called"
|
|
132
127
|
|
|
133
128
|
# Get the call arguments
|
|
134
|
-
call_kwargs =
|
|
129
|
+
call_kwargs = mock_jira_client.create_issue.call_args.kwargs
|
|
135
130
|
|
|
136
131
|
# ✅ CRITICAL ASSERTIONS - These should FAIL initially
|
|
137
132
|
assert "environments" in call_kwargs, "environments should be passed to Jira"
|
|
@@ -143,8 +138,9 @@ class TestP4P5CustomerImpactJiraFields:
|
|
|
143
138
|
|
|
144
139
|
# ✅ Verify business_impact is passed
|
|
145
140
|
assert "business_impact" in call_kwargs, "business_impact should be passed to Jira"
|
|
146
|
-
# Business impact
|
|
147
|
-
|
|
141
|
+
# Business impact is computed from customer impact level (can be None for certain levels)
|
|
142
|
+
# The presence of the key is what matters, value can be None
|
|
143
|
+
assert "business_impact" in call_kwargs
|
|
148
144
|
|
|
149
145
|
# ✅ Verify zendesk_ticket_id is passed
|
|
150
146
|
assert "zendesk_ticket_id" in call_kwargs, "zendesk_ticket_id should be passed"
|
|
@@ -191,23 +187,17 @@ class TestP4P5CustomerImpactJiraFields:
|
|
|
191
187
|
assert form.is_valid(), f"Form should be valid. Errors: {form.errors}"
|
|
192
188
|
|
|
193
189
|
with (
|
|
194
|
-
patch("firefighter.raid.
|
|
190
|
+
patch("firefighter.raid.client.client") as mock_jira_client,
|
|
195
191
|
patch("firefighter.raid.service.get_jira_user_from_user") as mock_get_jira_user,
|
|
196
|
-
patch("firefighter.
|
|
192
|
+
patch("firefighter.incidents.forms.unified_incident.SelectImpactForm"),
|
|
193
|
+
patch("firefighter.incidents.forms.select_impact.SelectImpactForm"),
|
|
197
194
|
patch("firefighter.raid.forms.set_jira_ticket_watchers_raid"),
|
|
198
195
|
patch("firefighter.raid.forms.alert_slack_new_jira_ticket"),
|
|
199
|
-
patch("firefighter.raid.
|
|
196
|
+
patch("firefighter.raid.models.JiraTicket"),
|
|
200
197
|
):
|
|
201
|
-
|
|
202
|
-
"
|
|
203
|
-
"
|
|
204
|
-
"project_key": "TEST",
|
|
205
|
-
"assignee_id": None,
|
|
206
|
-
"reporter_id": "test_account",
|
|
207
|
-
"description": "Test",
|
|
208
|
-
"summary": "Test",
|
|
209
|
-
"issue_type": "Incident",
|
|
210
|
-
"business_impact": "",
|
|
198
|
+
mock_jira_client.create_issue.return_value = {
|
|
199
|
+
"issue_key": "TEST-456",
|
|
200
|
+
"issue_url": "https://jira.example.com/browse/TEST-456",
|
|
211
201
|
}
|
|
212
202
|
mock_jira_user = JiraUser(id="test_account")
|
|
213
203
|
mock_get_jira_user.return_value = mock_jira_user
|
|
@@ -218,7 +208,7 @@ class TestP4P5CustomerImpactJiraFields:
|
|
|
218
208
|
response_type="normal",
|
|
219
209
|
)
|
|
220
210
|
|
|
221
|
-
call_kwargs =
|
|
211
|
+
call_kwargs = mock_jira_client.create_issue.call_args.kwargs
|
|
222
212
|
|
|
223
213
|
# Critical assertions
|
|
224
214
|
assert "environments" in call_kwargs
|
|
@@ -278,23 +268,17 @@ class TestP4P5SellerImpactJiraFields:
|
|
|
278
268
|
assert form.is_valid(), f"Form should be valid. Errors: {form.errors}"
|
|
279
269
|
|
|
280
270
|
with (
|
|
281
|
-
patch("firefighter.raid.
|
|
271
|
+
patch("firefighter.raid.client.client") as mock_jira_client,
|
|
282
272
|
patch("firefighter.raid.service.get_jira_user_from_user") as mock_get_jira_user,
|
|
283
|
-
patch("firefighter.
|
|
273
|
+
patch("firefighter.incidents.forms.unified_incident.SelectImpactForm"),
|
|
274
|
+
patch("firefighter.incidents.forms.select_impact.SelectImpactForm"),
|
|
284
275
|
patch("firefighter.raid.forms.set_jira_ticket_watchers_raid"),
|
|
285
276
|
patch("firefighter.raid.forms.alert_slack_new_jira_ticket"),
|
|
286
|
-
patch("firefighter.raid.
|
|
277
|
+
patch("firefighter.raid.models.JiraTicket"),
|
|
287
278
|
):
|
|
288
|
-
|
|
289
|
-
"
|
|
290
|
-
"
|
|
291
|
-
"project_key": "SELLER",
|
|
292
|
-
"assignee_id": None,
|
|
293
|
-
"reporter_id": "test_account",
|
|
294
|
-
"description": "Test",
|
|
295
|
-
"summary": "Test",
|
|
296
|
-
"issue_type": "Incident",
|
|
297
|
-
"business_impact": "",
|
|
279
|
+
mock_jira_client.create_issue.return_value = {
|
|
280
|
+
"issue_key": "SELLER-123",
|
|
281
|
+
"issue_url": "https://jira.example.com/browse/SELLER-123",
|
|
298
282
|
}
|
|
299
283
|
mock_jira_user = JiraUser(id="test_account")
|
|
300
284
|
mock_get_jira_user.return_value = mock_jira_user
|
|
@@ -305,7 +289,7 @@ class TestP4P5SellerImpactJiraFields:
|
|
|
305
289
|
response_type="normal",
|
|
306
290
|
)
|
|
307
291
|
|
|
308
|
-
call_kwargs =
|
|
292
|
+
call_kwargs = mock_jira_client.create_issue.call_args.kwargs
|
|
309
293
|
|
|
310
294
|
# ✅ CRITICAL ASSERTIONS for environments
|
|
311
295
|
assert "environments" in call_kwargs, "environments should be passed"
|
|
@@ -371,23 +355,17 @@ class TestP4P5InternalImpactJiraFields:
|
|
|
371
355
|
assert form.is_valid(), f"Form should be valid. Errors: {form.errors}"
|
|
372
356
|
|
|
373
357
|
with (
|
|
374
|
-
patch("firefighter.raid.
|
|
358
|
+
patch("firefighter.raid.client.client") as mock_jira_client,
|
|
375
359
|
patch("firefighter.raid.service.get_jira_user_from_user") as mock_get_jira_user,
|
|
376
|
-
patch("firefighter.
|
|
360
|
+
patch("firefighter.incidents.forms.unified_incident.SelectImpactForm"),
|
|
361
|
+
patch("firefighter.incidents.forms.select_impact.SelectImpactForm"),
|
|
377
362
|
patch("firefighter.raid.forms.set_jira_ticket_watchers_raid"),
|
|
378
363
|
patch("firefighter.raid.forms.alert_slack_new_jira_ticket"),
|
|
379
|
-
patch("firefighter.raid.
|
|
364
|
+
patch("firefighter.raid.models.JiraTicket"),
|
|
380
365
|
):
|
|
381
|
-
|
|
382
|
-
"
|
|
383
|
-
"
|
|
384
|
-
"project_key": "INTERNAL",
|
|
385
|
-
"assignee_id": None,
|
|
386
|
-
"reporter_id": "test_account",
|
|
387
|
-
"description": "Test",
|
|
388
|
-
"summary": "Test",
|
|
389
|
-
"issue_type": "Incident",
|
|
390
|
-
"business_impact": "",
|
|
366
|
+
mock_jira_client.create_issue.return_value = {
|
|
367
|
+
"issue_key": "INTERNAL-456",
|
|
368
|
+
"issue_url": "https://jira.example.com/browse/INTERNAL-456",
|
|
391
369
|
}
|
|
392
370
|
mock_jira_user = JiraUser(id="test_account")
|
|
393
371
|
mock_get_jira_user.return_value = mock_jira_user
|
|
@@ -398,7 +376,7 @@ class TestP4P5InternalImpactJiraFields:
|
|
|
398
376
|
response_type="normal",
|
|
399
377
|
)
|
|
400
378
|
|
|
401
|
-
call_kwargs =
|
|
379
|
+
call_kwargs = mock_jira_client.create_issue.call_args.kwargs
|
|
402
380
|
|
|
403
381
|
# ✅ CRITICAL: environments must be passed for internal incidents too
|
|
404
382
|
assert "environments" in call_kwargs
|
|
@@ -15,6 +15,11 @@ from firefighter.incidents.models import Environment, Priority
|
|
|
15
15
|
class TestUpdateStatusWorkflow(TestCase):
|
|
16
16
|
"""Test that status choices respect workflow rules."""
|
|
17
17
|
|
|
18
|
+
@staticmethod
|
|
19
|
+
def _get_available_statuses(status_choices: dict[str, str]) -> list[IncidentStatus]:
|
|
20
|
+
"""Convert status_choices dict (string keys) to list of IncidentStatus enums."""
|
|
21
|
+
return [IncidentStatus(int(k)) for k in status_choices]
|
|
22
|
+
|
|
18
23
|
def test_p1_p2_cannot_skip_postmortem(self):
|
|
19
24
|
"""P1/P2 incidents cannot go directly from Mitigated to Closed."""
|
|
20
25
|
|
|
@@ -55,8 +60,9 @@ class TestUpdateStatusWorkflow(TestCase):
|
|
|
55
60
|
|
|
56
61
|
# Check that CLOSED is not in the choices
|
|
57
62
|
status_choices = dict(form.fields["status"].choices)
|
|
58
|
-
|
|
59
|
-
assert IncidentStatus.
|
|
63
|
+
available_statuses = self._get_available_statuses(status_choices)
|
|
64
|
+
assert IncidentStatus.CLOSED not in available_statuses
|
|
65
|
+
assert IncidentStatus.POST_MORTEM in available_statuses
|
|
60
66
|
|
|
61
67
|
def test_p1_p2_can_close_from_postmortem(self):
|
|
62
68
|
"""P1/P2 incidents can go from Post-mortem to Closed."""
|
|
@@ -86,7 +92,8 @@ class TestUpdateStatusWorkflow(TestCase):
|
|
|
86
92
|
|
|
87
93
|
# Check that CLOSED is in the choices
|
|
88
94
|
status_choices = dict(form.fields["status"].choices)
|
|
89
|
-
|
|
95
|
+
available_statuses = self._get_available_statuses(status_choices)
|
|
96
|
+
assert IncidentStatus.CLOSED in available_statuses
|
|
90
97
|
|
|
91
98
|
def test_p3_plus_can_skip_postmortem(self):
|
|
92
99
|
"""P3+ incidents can go directly from Mitigated to Closed and should not have post-mortem."""
|
|
@@ -110,8 +117,9 @@ class TestUpdateStatusWorkflow(TestCase):
|
|
|
110
117
|
|
|
111
118
|
# Check that CLOSED is in the choices for P3+ but POST_MORTEM is NOT
|
|
112
119
|
status_choices = dict(form.fields["status"].choices)
|
|
113
|
-
|
|
114
|
-
assert IncidentStatus.
|
|
120
|
+
available_statuses = self._get_available_statuses(status_choices)
|
|
121
|
+
assert IncidentStatus.CLOSED in available_statuses
|
|
122
|
+
assert IncidentStatus.POST_MORTEM not in available_statuses, "P3+ incidents should not have post-mortem status available"
|
|
115
123
|
|
|
116
124
|
def test_p1_p2_cannot_skip_postmortem_from_mitigating(self):
|
|
117
125
|
"""P1/P2 incidents cannot go directly from Mitigating to Closed."""
|
|
@@ -141,8 +149,9 @@ class TestUpdateStatusWorkflow(TestCase):
|
|
|
141
149
|
|
|
142
150
|
# Check that CLOSED and POST_MORTEM are not in the choices from MITIGATING status
|
|
143
151
|
status_choices = dict(form.fields["status"].choices)
|
|
144
|
-
|
|
145
|
-
assert IncidentStatus.
|
|
152
|
+
available_statuses = self._get_available_statuses(status_choices)
|
|
153
|
+
assert IncidentStatus.CLOSED not in available_statuses
|
|
154
|
+
assert IncidentStatus.POST_MORTEM not in available_statuses, "P1/P2 should not be able to go to post-mortem from Mitigating"
|
|
146
155
|
|
|
147
156
|
def test_p3_can_skip_from_investigating_to_closed(self):
|
|
148
157
|
"""P3+ incidents can go directly from Investigating to Closed."""
|
|
@@ -165,8 +174,9 @@ class TestUpdateStatusWorkflow(TestCase):
|
|
|
165
174
|
|
|
166
175
|
# Check that CLOSED is in the choices from INVESTIGATING status for P3+ but POST_MORTEM is NOT
|
|
167
176
|
status_choices = dict(form.fields["status"].choices)
|
|
168
|
-
|
|
169
|
-
assert IncidentStatus.
|
|
177
|
+
available_statuses = self._get_available_statuses(status_choices)
|
|
178
|
+
assert IncidentStatus.CLOSED in available_statuses
|
|
179
|
+
assert IncidentStatus.POST_MORTEM not in available_statuses, "P3+ incidents should not have post-mortem status available"
|
|
170
180
|
|
|
171
181
|
def test_p1_p2_can_go_from_mitigated_to_postmortem(self):
|
|
172
182
|
"""P1/P2 incidents can go from Mitigated to Post-mortem."""
|
|
@@ -196,8 +206,9 @@ class TestUpdateStatusWorkflow(TestCase):
|
|
|
196
206
|
|
|
197
207
|
# Check that POST_MORTEM is available but CLOSED is not
|
|
198
208
|
status_choices = dict(form.fields["status"].choices)
|
|
199
|
-
|
|
200
|
-
assert IncidentStatus.
|
|
209
|
+
available_statuses = self._get_available_statuses(status_choices)
|
|
210
|
+
assert IncidentStatus.POST_MORTEM in available_statuses, "P1/P2 should be able to go to Post-mortem from Mitigated"
|
|
211
|
+
assert IncidentStatus.CLOSED not in available_statuses, "P1/P2 should NOT be able to skip Post-mortem"
|
|
201
212
|
|
|
202
213
|
def test_complete_workflow_transitions_p3_plus(self):
|
|
203
214
|
"""Test the correct workflow for P3+: can close with reason from early statuses, normal close from Mitigated."""
|
|
@@ -225,14 +236,15 @@ class TestUpdateStatusWorkflow(TestCase):
|
|
|
225
236
|
|
|
226
237
|
form = UpdateStatusForm(incident=incident)
|
|
227
238
|
status_choices = dict(form.fields["status"].choices)
|
|
239
|
+
available_statuses = self._get_available_statuses(status_choices)
|
|
228
240
|
|
|
229
241
|
if should_have_closed:
|
|
230
|
-
assert IncidentStatus.CLOSED in
|
|
242
|
+
assert IncidentStatus.CLOSED in available_statuses, f"P3+ should be able to go to Closed from {current_status.label}"
|
|
231
243
|
else:
|
|
232
|
-
assert IncidentStatus.CLOSED not in
|
|
244
|
+
assert IncidentStatus.CLOSED not in available_statuses, f"P3+ should NOT be able to go to Closed from {current_status.label}"
|
|
233
245
|
|
|
234
246
|
# P3+ should NEVER have post-mortem available
|
|
235
|
-
assert IncidentStatus.POST_MORTEM not in
|
|
247
|
+
assert IncidentStatus.POST_MORTEM not in available_statuses, f"P3+ should NEVER have post-mortem available from {current_status.label}"
|
|
236
248
|
|
|
237
249
|
def test_closure_reason_required_from_early_statuses(self):
|
|
238
250
|
"""Test that closure reason is required when closing from Opened or Investigating."""
|
|
@@ -283,17 +295,18 @@ class TestUpdateStatusWorkflow(TestCase):
|
|
|
283
295
|
|
|
284
296
|
form = UpdateStatusForm(incident=incident)
|
|
285
297
|
status_choices = dict(form.fields["status"].choices)
|
|
298
|
+
available_statuses = self._get_available_statuses(status_choices)
|
|
286
299
|
|
|
287
300
|
# Should NOT have Closed option from Mitigating
|
|
288
|
-
assert IncidentStatus.CLOSED not in
|
|
301
|
+
assert IncidentStatus.CLOSED not in available_statuses, f"Should NOT be able to close from Mitigating for {priority.name}"
|
|
289
302
|
|
|
290
303
|
# Should have Post-mortem option only for P1/P2 priorities that need postmortem
|
|
291
304
|
if priority.needs_postmortem:
|
|
292
305
|
# For P1/P2, should still NOT have post-mortem from MITIGATING (Mitigating)
|
|
293
|
-
assert IncidentStatus.POST_MORTEM not in
|
|
306
|
+
assert IncidentStatus.POST_MORTEM not in available_statuses, f"P1/P2 should NOT be able to go to Post-mortem from Mitigating for {priority.name}"
|
|
294
307
|
else:
|
|
295
308
|
# For P3+, should not have post-mortem at all
|
|
296
|
-
assert IncidentStatus.POST_MORTEM not in
|
|
309
|
+
assert IncidentStatus.POST_MORTEM not in available_statuses, f"P3+ should not have post-mortem available for {priority.name}"
|
|
297
310
|
|
|
298
311
|
def test_no_incident_shows_all_statuses(self):
|
|
299
312
|
"""When no incident is provided, all statuses should be available."""
|
|
@@ -302,8 +315,9 @@ class TestUpdateStatusWorkflow(TestCase):
|
|
|
302
315
|
|
|
303
316
|
# Check that all statuses including CLOSED are in the choices
|
|
304
317
|
status_choices = dict(form.fields["status"].choices)
|
|
305
|
-
|
|
306
|
-
assert IncidentStatus.
|
|
318
|
+
available_statuses = self._get_available_statuses(status_choices)
|
|
319
|
+
assert IncidentStatus.CLOSED in available_statuses
|
|
320
|
+
assert IncidentStatus.POST_MORTEM in available_statuses
|
|
307
321
|
|
|
308
322
|
def test_requires_closure_reason_non_closed_status(self):
|
|
309
323
|
"""Test requires_closure_reason with non-CLOSED target status."""
|
|
@@ -338,6 +352,7 @@ class TestUpdateStatusWorkflow(TestCase):
|
|
|
338
352
|
|
|
339
353
|
form = UpdateStatusForm(incident=incident)
|
|
340
354
|
status_choices = dict(form.fields["status"].choices)
|
|
355
|
+
available_statuses = self._get_available_statuses(status_choices)
|
|
341
356
|
|
|
342
357
|
# Should use default choices_lte for unknown/closed status
|
|
343
|
-
assert IncidentStatus.CLOSED in
|
|
358
|
+
assert IncidentStatus.CLOSED in available_statuses
|
|
@@ -63,7 +63,8 @@ class TestCompleteWorkflowTransitions(TestCase):
|
|
|
63
63
|
|
|
64
64
|
form = UpdateStatusForm(incident=incident)
|
|
65
65
|
status_choices = dict(form.fields["status"].choices)
|
|
66
|
-
|
|
66
|
+
# Convert string keys back to enum values for comparison
|
|
67
|
+
available_statuses = [IncidentStatus(int(k)) for k in status_choices]
|
|
67
68
|
|
|
68
69
|
# Remove current status from available (can't transition to same status)
|
|
69
70
|
if current_status in available_statuses:
|
|
@@ -72,14 +73,14 @@ class TestCompleteWorkflowTransitions(TestCase):
|
|
|
72
73
|
for expected_status in expected_statuses:
|
|
73
74
|
assert expected_status in available_statuses, (
|
|
74
75
|
f"P1/P2: From {current_status.label}, should be able to go to {expected_status.label}. "
|
|
75
|
-
f"Available: {[
|
|
76
|
+
f"Available: {[s.label for s in available_statuses]}"
|
|
76
77
|
)
|
|
77
78
|
|
|
78
79
|
# Verify no unexpected statuses are available
|
|
79
80
|
unexpected_statuses = set(available_statuses) - set(expected_statuses)
|
|
80
81
|
assert not unexpected_statuses, (
|
|
81
82
|
f"P1/P2: From {current_status.label}, unexpected statuses available: "
|
|
82
|
-
f"{[
|
|
83
|
+
f"{[s.label for s in unexpected_statuses]}"
|
|
83
84
|
)
|
|
84
85
|
|
|
85
86
|
def test_p3_plus_complete_workflow_transitions(self):
|
|
@@ -109,7 +110,8 @@ class TestCompleteWorkflowTransitions(TestCase):
|
|
|
109
110
|
|
|
110
111
|
form = UpdateStatusForm(incident=incident)
|
|
111
112
|
status_choices = dict(form.fields["status"].choices)
|
|
112
|
-
|
|
113
|
+
# Convert string keys back to enum values for comparison
|
|
114
|
+
available_statuses = [IncidentStatus(int(k)) for k in status_choices]
|
|
113
115
|
|
|
114
116
|
# Remove current status from available (can't transition to same status)
|
|
115
117
|
if current_status in available_statuses:
|
|
@@ -118,14 +120,14 @@ class TestCompleteWorkflowTransitions(TestCase):
|
|
|
118
120
|
for expected_status in expected_statuses:
|
|
119
121
|
assert expected_status in available_statuses, (
|
|
120
122
|
f"P3+: From {current_status.label}, should be able to go to {expected_status.label}. "
|
|
121
|
-
f"Available: {[
|
|
123
|
+
f"Available: {[s.label for s in available_statuses]}"
|
|
122
124
|
)
|
|
123
125
|
|
|
124
126
|
# Verify no unexpected statuses are available (especially POST_MORTEM)
|
|
125
127
|
unexpected_statuses = set(available_statuses) - set(expected_statuses)
|
|
126
128
|
assert not unexpected_statuses, (
|
|
127
129
|
f"P3+: From {current_status.label}, unexpected statuses available: "
|
|
128
|
-
f"{[
|
|
130
|
+
f"{[s.label for s in unexpected_statuses]}"
|
|
129
131
|
)
|
|
130
132
|
|
|
131
133
|
# Specifically verify POST_MORTEM is never available for P3+
|