firefighter-incident 0.0.22__py3-none-any.whl → 0.0.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firefighter/_version.py +2 -2
- firefighter/api/serializers.py +18 -0
- firefighter/api/views/incidents.py +3 -0
- firefighter/confluence/models.py +66 -6
- firefighter/confluence/signals/incident_updated.py +8 -26
- firefighter/firefighter/settings/components/jira_app.py +33 -0
- firefighter/incidents/admin.py +3 -0
- firefighter/incidents/models/impact.py +3 -5
- firefighter/incidents/models/incident.py +24 -9
- firefighter/incidents/views/views.py +2 -0
- firefighter/jira_app/admin.py +15 -1
- firefighter/jira_app/apps.py +3 -0
- firefighter/jira_app/client.py +151 -3
- firefighter/jira_app/management/__init__.py +1 -0
- firefighter/jira_app/management/commands/__init__.py +1 -0
- firefighter/jira_app/migrations/0002_add_jira_postmortem_model.py +71 -0
- firefighter/jira_app/models.py +50 -0
- firefighter/jira_app/service_postmortem.py +292 -0
- firefighter/jira_app/signals/__init__.py +10 -0
- firefighter/jira_app/signals/incident_key_events_updated.py +88 -0
- firefighter/jira_app/signals/postmortem_created.py +155 -0
- firefighter/jira_app/templates/jira/postmortem/impact.txt +12 -0
- firefighter/jira_app/templates/jira/postmortem/incident_summary.txt +17 -0
- firefighter/jira_app/templates/jira/postmortem/mitigation_actions.txt +9 -0
- firefighter/jira_app/templates/jira/postmortem/root_causes.txt +12 -0
- firefighter/jira_app/templates/jira/postmortem/timeline.txt +7 -0
- firefighter/raid/signals/incident_updated.py +31 -11
- firefighter/slack/messages/slack_messages.py +39 -3
- firefighter/slack/signals/postmortem_created.py +51 -3
- firefighter/slack/views/modals/closure_reason.py +15 -0
- firefighter/slack/views/modals/key_event_message.py +9 -0
- firefighter/slack/views/modals/postmortem.py +32 -40
- firefighter/slack/views/modals/update_status.py +7 -1
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/RECORD +50 -31
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/WHEEL +1 -1
- firefighter_tests/test_api/test_renderer.py +41 -0
- firefighter_tests/test_incidents/test_models/test_incident_model.py +29 -0
- firefighter_tests/test_jira_app/test_incident_key_events_sync.py +112 -0
- firefighter_tests/test_jira_app/test_models.py +138 -0
- firefighter_tests/test_jira_app/test_postmortem_issue_link.py +201 -0
- firefighter_tests/test_jira_app/test_postmortem_service.py +416 -0
- firefighter_tests/test_jira_app/test_timeline_template.py +135 -0
- firefighter_tests/test_raid/test_raid_signals.py +50 -8
- firefighter_tests/test_slack/messages/test_slack_messages.py +112 -23
- firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +18 -2
- firefighter_tests/test_slack/views/modals/test_key_event_message.py +30 -0
- firefighter_tests/test_slack/views/modals/test_update_status.py +161 -129
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Tests for Jira post-mortem issue link creation with robust error handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
from jira import exceptions as jira_exceptions
|
|
8
|
+
|
|
9
|
+
from firefighter.jira_app.client import JiraClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestPostmortemIssueLink:
|
|
13
|
+
"""Test robust issue link creation between incident and post-mortem."""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
@patch("firefighter.jira_app.client.JIRA")
|
|
17
|
+
def test_create_issue_link_success_first_try(mock_jira_class: MagicMock) -> None:
|
|
18
|
+
"""Test that issue link is created successfully on first try with 'Relates' type."""
|
|
19
|
+
# Setup mock
|
|
20
|
+
mock_jira_instance = MagicMock()
|
|
21
|
+
mock_jira_class.return_value = mock_jira_instance
|
|
22
|
+
|
|
23
|
+
# Mock issue validation - both issues exist
|
|
24
|
+
mock_parent_issue = MagicMock()
|
|
25
|
+
mock_postmortem_issue = MagicMock()
|
|
26
|
+
mock_jira_instance.issue.side_effect = [
|
|
27
|
+
mock_parent_issue,
|
|
28
|
+
mock_postmortem_issue,
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Mock successful link creation
|
|
32
|
+
mock_jira_instance.create_issue_link.return_value = None
|
|
33
|
+
|
|
34
|
+
# Create client and call the method
|
|
35
|
+
client = JiraClient()
|
|
36
|
+
client._create_issue_link_safe(
|
|
37
|
+
parent_issue_key="INCIDENT-123", postmortem_issue_key="PM-456"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Verify link was created with 'Relates' type
|
|
41
|
+
mock_jira_instance.create_issue_link.assert_called_once_with(
|
|
42
|
+
type="Relates",
|
|
43
|
+
inwardIssue="INCIDENT-123",
|
|
44
|
+
outwardIssue="PM-456",
|
|
45
|
+
comment={"body": "Post-mortem PM-456 created for incident INCIDENT-123"},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
@patch("firefighter.jira_app.client.JIRA")
|
|
50
|
+
def test_create_issue_link_fallback_to_blocks(mock_jira_class: MagicMock) -> None:
|
|
51
|
+
"""Test that issue link falls back to 'Blocks' type when 'Relates' fails."""
|
|
52
|
+
# Setup mock
|
|
53
|
+
mock_jira_instance = MagicMock()
|
|
54
|
+
mock_jira_class.return_value = mock_jira_instance
|
|
55
|
+
|
|
56
|
+
# Mock issue validation - both issues exist (called multiple times)
|
|
57
|
+
mock_issue = MagicMock()
|
|
58
|
+
mock_jira_instance.issue.return_value = mock_issue
|
|
59
|
+
|
|
60
|
+
# Mock link creation: first attempt fails, second succeeds
|
|
61
|
+
mock_jira_instance.create_issue_link.side_effect = [
|
|
62
|
+
jira_exceptions.JIRAError(
|
|
63
|
+
status_code=400, text="Link type 'Relates' not found"
|
|
64
|
+
),
|
|
65
|
+
None, # Second attempt succeeds
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# Create client and call the method
|
|
69
|
+
client = JiraClient()
|
|
70
|
+
client._create_issue_link_safe(
|
|
71
|
+
parent_issue_key="INCIDENT-123", postmortem_issue_key="PM-456"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Verify it tried 'Relates' first, then 'Blocks'
|
|
75
|
+
assert mock_jira_instance.create_issue_link.call_count == 2
|
|
76
|
+
first_call = mock_jira_instance.create_issue_link.call_args_list[0]
|
|
77
|
+
second_call = mock_jira_instance.create_issue_link.call_args_list[1]
|
|
78
|
+
assert first_call.kwargs["type"] == "Relates"
|
|
79
|
+
assert second_call.kwargs["type"] == "Blocks"
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
@patch("firefighter.jira_app.client.JIRA")
|
|
83
|
+
def test_create_issue_link_all_types_fail(mock_jira_class: MagicMock) -> None:
|
|
84
|
+
"""Test that method handles gracefully when all link types fail."""
|
|
85
|
+
# Setup mock
|
|
86
|
+
mock_jira_instance = MagicMock()
|
|
87
|
+
mock_jira_class.return_value = mock_jira_instance
|
|
88
|
+
|
|
89
|
+
# Mock issue validation - both issues exist
|
|
90
|
+
mock_issue = MagicMock()
|
|
91
|
+
mock_jira_instance.issue.return_value = mock_issue
|
|
92
|
+
|
|
93
|
+
# Mock link creation: all attempts fail
|
|
94
|
+
mock_jira_instance.create_issue_link.side_effect = jira_exceptions.JIRAError(
|
|
95
|
+
status_code=400, text="Link type not found"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Create client and call the method - should not raise exception
|
|
99
|
+
client = JiraClient()
|
|
100
|
+
client._create_issue_link_safe(
|
|
101
|
+
parent_issue_key="INCIDENT-123", postmortem_issue_key="PM-456"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Verify it tried all 3 link types
|
|
105
|
+
assert mock_jira_instance.create_issue_link.call_count == 3
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
@patch("firefighter.jira_app.client.JIRA")
|
|
109
|
+
def test_create_issue_link_parent_not_found(mock_jira_class: MagicMock) -> None:
|
|
110
|
+
"""Test that method handles gracefully when parent issue doesn't exist."""
|
|
111
|
+
# Setup mock
|
|
112
|
+
mock_jira_instance = MagicMock()
|
|
113
|
+
mock_jira_class.return_value = mock_jira_instance
|
|
114
|
+
|
|
115
|
+
# Mock issue validation: parent issue not found
|
|
116
|
+
mock_jira_instance.issue.side_effect = jira_exceptions.JIRAError(
|
|
117
|
+
status_code=404, text="Issue not found"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Create client and call the method - should not raise exception
|
|
121
|
+
client = JiraClient()
|
|
122
|
+
client._create_issue_link_safe(
|
|
123
|
+
parent_issue_key="INCIDENT-999", postmortem_issue_key="PM-456"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Verify link creation was not attempted
|
|
127
|
+
mock_jira_instance.create_issue_link.assert_not_called()
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
@patch("firefighter.jira_app.client.JIRA")
|
|
131
|
+
def test_create_postmortem_issue_with_link(mock_jira_class: MagicMock) -> None:
|
|
132
|
+
"""Test that post-mortem issue is created and linked successfully."""
|
|
133
|
+
# Setup mock
|
|
134
|
+
mock_jira_instance = MagicMock()
|
|
135
|
+
mock_jira_class.return_value = mock_jira_instance
|
|
136
|
+
|
|
137
|
+
# Mock issue creation
|
|
138
|
+
mock_created_issue = MagicMock()
|
|
139
|
+
mock_created_issue.key = "PM-789"
|
|
140
|
+
mock_created_issue.id = "12345"
|
|
141
|
+
mock_jira_instance.create_issue.return_value = mock_created_issue
|
|
142
|
+
|
|
143
|
+
# Mock issue validation
|
|
144
|
+
mock_issue = MagicMock()
|
|
145
|
+
mock_jira_instance.issue.return_value = mock_issue
|
|
146
|
+
|
|
147
|
+
# Mock link creation
|
|
148
|
+
mock_jira_instance.create_issue_link.return_value = None
|
|
149
|
+
|
|
150
|
+
# Create client and call the method
|
|
151
|
+
client = JiraClient()
|
|
152
|
+
result = client.create_postmortem_issue(
|
|
153
|
+
project_key="PM",
|
|
154
|
+
issue_type="Post-mortem",
|
|
155
|
+
fields={"summary": "Test post-mortem"},
|
|
156
|
+
parent_issue_key="INCIDENT-123",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Verify issue was created
|
|
160
|
+
mock_jira_instance.create_issue.assert_called_once()
|
|
161
|
+
assert result == {"key": "PM-789", "id": "12345"}
|
|
162
|
+
|
|
163
|
+
# Verify link was created
|
|
164
|
+
mock_jira_instance.create_issue_link.assert_called_once()
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
@patch("firefighter.jira_app.client.JIRA")
|
|
168
|
+
def test_create_postmortem_issue_link_fails_but_issue_created(
|
|
169
|
+
mock_jira_class: MagicMock,
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Test that post-mortem issue is still created even if linking fails."""
|
|
172
|
+
# Setup mock
|
|
173
|
+
mock_jira_instance = MagicMock()
|
|
174
|
+
mock_jira_class.return_value = mock_jira_instance
|
|
175
|
+
|
|
176
|
+
# Mock issue creation
|
|
177
|
+
mock_created_issue = MagicMock()
|
|
178
|
+
mock_created_issue.key = "PM-789"
|
|
179
|
+
mock_created_issue.id = "12345"
|
|
180
|
+
mock_jira_instance.create_issue.return_value = mock_created_issue
|
|
181
|
+
|
|
182
|
+
# Mock issue validation - parent doesn't exist
|
|
183
|
+
mock_jira_instance.issue.side_effect = jira_exceptions.JIRAError(
|
|
184
|
+
status_code=404, text="Issue not found"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Create client and call the method
|
|
188
|
+
client = JiraClient()
|
|
189
|
+
result = client.create_postmortem_issue(
|
|
190
|
+
project_key="PM",
|
|
191
|
+
issue_type="Post-mortem",
|
|
192
|
+
fields={"summary": "Test post-mortem"},
|
|
193
|
+
parent_issue_key="INCIDENT-999",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Verify issue was created successfully despite link failure
|
|
197
|
+
mock_jira_instance.create_issue.assert_called_once()
|
|
198
|
+
assert result == {"key": "PM-789", "id": "12345"}
|
|
199
|
+
|
|
200
|
+
# Verify link creation was attempted but failed gracefully
|
|
201
|
+
mock_jira_instance.create_issue_link.assert_not_called()
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Tests for Jira post-mortem service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from django.utils import timezone
|
|
11
|
+
|
|
12
|
+
from firefighter.incidents.enums import IncidentStatus
|
|
13
|
+
from firefighter.incidents.factories import IncidentFactory, UserFactory
|
|
14
|
+
from firefighter.incidents.models.incident_membership import IncidentRole
|
|
15
|
+
from firefighter.incidents.models.incident_role_type import IncidentRoleType
|
|
16
|
+
from firefighter.incidents.models.incident_update import IncidentUpdate
|
|
17
|
+
from firefighter.jira_app.client import JiraUser
|
|
18
|
+
from firefighter.jira_app.models import JiraUser as JiraUserDB
|
|
19
|
+
from firefighter.jira_app.service_postmortem import JiraPostMortemService
|
|
20
|
+
from firefighter.raid.models import JiraTicket
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from firefighter.incidents.models.incident import Incident
|
|
24
|
+
from firefighter.incidents.models.user import User
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.django_db
|
|
28
|
+
class TestJiraPostMortemService:
|
|
29
|
+
"""Test Jira post-mortem service."""
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def test_incident_summary_excludes_status_and_created() -> None:
|
|
33
|
+
"""Test that incident summary does not include Status and Created fields."""
|
|
34
|
+
# Create a user
|
|
35
|
+
user: User = UserFactory.create()
|
|
36
|
+
|
|
37
|
+
# Create an incident with some updates
|
|
38
|
+
incident: Incident = IncidentFactory.create(
|
|
39
|
+
_status=IncidentStatus.POST_MORTEM,
|
|
40
|
+
created_by=user,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Generate the incident summary
|
|
44
|
+
service = JiraPostMortemService()
|
|
45
|
+
fields = service._generate_issue_fields(incident)
|
|
46
|
+
|
|
47
|
+
incident_summary = fields[service.field_ids["incident_summary"]]
|
|
48
|
+
|
|
49
|
+
# Verify that Status and Created are NOT in the summary
|
|
50
|
+
assert "Status:" not in incident_summary
|
|
51
|
+
assert "Created:" not in incident_summary
|
|
52
|
+
|
|
53
|
+
# Verify that required fields ARE present
|
|
54
|
+
assert "Incident Summary" in incident_summary
|
|
55
|
+
assert "Incident:" in incident_summary
|
|
56
|
+
assert "Priority:" in incident_summary
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def test_timeline_includes_status_changes() -> None:
|
|
60
|
+
"""Test that timeline includes incident status changes."""
|
|
61
|
+
# Create a user
|
|
62
|
+
user: User = UserFactory.create()
|
|
63
|
+
|
|
64
|
+
# Create an incident
|
|
65
|
+
incident: Incident = IncidentFactory.create(
|
|
66
|
+
_status=IncidentStatus.OPEN,
|
|
67
|
+
created_by=user,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Create status updates
|
|
71
|
+
now = timezone.now()
|
|
72
|
+
|
|
73
|
+
# Status change to INVESTIGATING
|
|
74
|
+
IncidentUpdate.objects.create(
|
|
75
|
+
incident=incident,
|
|
76
|
+
status=IncidentStatus.INVESTIGATING,
|
|
77
|
+
event_ts=now,
|
|
78
|
+
created_by=user,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Status change to MITIGATING
|
|
82
|
+
IncidentUpdate.objects.create(
|
|
83
|
+
incident=incident,
|
|
84
|
+
status=IncidentStatus.MITIGATING,
|
|
85
|
+
event_ts=now,
|
|
86
|
+
created_by=user,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Status change to MITIGATED
|
|
90
|
+
IncidentUpdate.objects.create(
|
|
91
|
+
incident=incident,
|
|
92
|
+
status=IncidentStatus.MITIGATED,
|
|
93
|
+
event_ts=now,
|
|
94
|
+
created_by=user,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Generate the timeline
|
|
98
|
+
service = JiraPostMortemService()
|
|
99
|
+
fields = service._generate_issue_fields(incident)
|
|
100
|
+
|
|
101
|
+
timeline = fields[service.field_ids["timeline"]]
|
|
102
|
+
|
|
103
|
+
# Verify that status changes are in the timeline
|
|
104
|
+
assert "Status changed to: Investigating" in timeline
|
|
105
|
+
assert "Status changed to: Mitigating" in timeline
|
|
106
|
+
assert "Status changed to: Mitigated" in timeline
|
|
107
|
+
|
|
108
|
+
# Verify the initial creation event is present
|
|
109
|
+
assert "Incident created" in timeline
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def test_generate_issue_fields_sets_due_date() -> None:
|
|
113
|
+
user: User = UserFactory.create()
|
|
114
|
+
incident: Incident = IncidentFactory.create(
|
|
115
|
+
_status=IncidentStatus.OPEN,
|
|
116
|
+
created_by=user,
|
|
117
|
+
)
|
|
118
|
+
incident.created_at = datetime(2024, 1, 1, tzinfo=UTC)
|
|
119
|
+
incident.save(update_fields=["created_at"])
|
|
120
|
+
|
|
121
|
+
service = JiraPostMortemService()
|
|
122
|
+
fields = service._generate_issue_fields(incident)
|
|
123
|
+
|
|
124
|
+
expected_due = (
|
|
125
|
+
service._add_business_days(incident.created_at, 40).date().isoformat()
|
|
126
|
+
)
|
|
127
|
+
assert fields["duedate"] == expected_due
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
@patch("firefighter.jira_app.service_postmortem.JiraClient")
|
|
131
|
+
def test_create_postmortem_prefetches_updates(
|
|
132
|
+
mock_jira_client: MagicMock,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Test that creating post-mortem prefetches incident updates."""
|
|
135
|
+
# Mock Jira client responses
|
|
136
|
+
mock_client_instance = MagicMock()
|
|
137
|
+
mock_client_instance.create_postmortem_issue.return_value = {
|
|
138
|
+
"id": "12345",
|
|
139
|
+
"key": "TEST-123",
|
|
140
|
+
}
|
|
141
|
+
mock_jira_client.return_value = mock_client_instance
|
|
142
|
+
|
|
143
|
+
# Create a user
|
|
144
|
+
user: User = UserFactory.create()
|
|
145
|
+
|
|
146
|
+
# Create an incident with status updates
|
|
147
|
+
incident: Incident = IncidentFactory.create(
|
|
148
|
+
_status=IncidentStatus.POST_MORTEM,
|
|
149
|
+
created_by=user,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
now = timezone.now()
|
|
153
|
+
IncidentUpdate.objects.create(
|
|
154
|
+
incident=incident,
|
|
155
|
+
status=IncidentStatus.INVESTIGATING,
|
|
156
|
+
event_ts=now,
|
|
157
|
+
created_by=user,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Create post-mortem
|
|
161
|
+
service = JiraPostMortemService()
|
|
162
|
+
jira_pm = service.create_postmortem_for_incident(incident, created_by=user)
|
|
163
|
+
|
|
164
|
+
# Verify post-mortem was created
|
|
165
|
+
assert jira_pm is not None
|
|
166
|
+
assert jira_pm.jira_issue_key == "TEST-123"
|
|
167
|
+
assert jira_pm.incident == incident
|
|
168
|
+
|
|
169
|
+
# Verify Jira client was called with correct fields
|
|
170
|
+
mock_client_instance.create_postmortem_issue.assert_called_once()
|
|
171
|
+
call_kwargs = mock_client_instance.create_postmortem_issue.call_args.kwargs
|
|
172
|
+
assert "fields" in call_kwargs
|
|
173
|
+
|
|
174
|
+
# Verify timeline contains status change
|
|
175
|
+
timeline = call_kwargs["fields"][service.field_ids["timeline"]]
|
|
176
|
+
assert "Status changed to: Investigating" in timeline
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
@patch("firefighter.jira_app.service_postmortem.JiraClient")
|
|
180
|
+
def test_create_postmortem_handles_assignment_failure_gracefully(
|
|
181
|
+
mock_jira_client: MagicMock,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Test that post-mortem creation succeeds even if assignment fails."""
|
|
184
|
+
# Mock Jira client responses
|
|
185
|
+
mock_client_instance = MagicMock()
|
|
186
|
+
mock_client_instance.create_postmortem_issue.return_value = {
|
|
187
|
+
"id": "12345",
|
|
188
|
+
"key": "TEST-123",
|
|
189
|
+
}
|
|
190
|
+
# Mock assignment failure - return False instead of raising exception
|
|
191
|
+
mock_client_instance.assign_issue.return_value = False
|
|
192
|
+
mock_jira_client.return_value = mock_client_instance
|
|
193
|
+
|
|
194
|
+
# Create a user
|
|
195
|
+
user: User = UserFactory.create()
|
|
196
|
+
|
|
197
|
+
# Create an incident
|
|
198
|
+
incident: Incident = IncidentFactory.create(
|
|
199
|
+
_status=IncidentStatus.POST_MORTEM,
|
|
200
|
+
created_by=user,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Create post-mortem
|
|
204
|
+
service = JiraPostMortemService()
|
|
205
|
+
jira_pm = service.create_postmortem_for_incident(incident, created_by=user)
|
|
206
|
+
|
|
207
|
+
# Verify post-mortem was created successfully despite assignment failure
|
|
208
|
+
assert jira_pm is not None
|
|
209
|
+
assert jira_pm.jira_issue_key == "TEST-123"
|
|
210
|
+
assert jira_pm.incident == incident
|
|
211
|
+
|
|
212
|
+
# Verify Jira client create was called
|
|
213
|
+
mock_client_instance.create_postmortem_issue.assert_called_once()
|
|
214
|
+
|
|
215
|
+
# Verify assignment was not attempted (no commander role)
|
|
216
|
+
# or if attempted, it returned False without raising exception
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
@pytest.mark.django_db
|
|
220
|
+
def test_assigns_commander_without_jira_user() -> None:
|
|
221
|
+
"""Commander without Jira user should trigger lookup and assignment."""
|
|
222
|
+
service = JiraPostMortemService()
|
|
223
|
+
mock_client = MagicMock()
|
|
224
|
+
mock_client.create_postmortem_issue.return_value = {"id": "1", "key": "INC-1"}
|
|
225
|
+
mock_client.assign_issue.return_value = True
|
|
226
|
+
mock_client.get_jira_user_from_user.return_value = JiraUser(
|
|
227
|
+
id="acct-123", user=None
|
|
228
|
+
)
|
|
229
|
+
service.client = mock_client
|
|
230
|
+
|
|
231
|
+
user = UserFactory()
|
|
232
|
+
incident = IncidentFactory.create(
|
|
233
|
+
_status=IncidentStatus.POST_MORTEM,
|
|
234
|
+
created_by=user,
|
|
235
|
+
)
|
|
236
|
+
role_type, _ = IncidentRoleType.objects.get_or_create(
|
|
237
|
+
slug="commander",
|
|
238
|
+
defaults={
|
|
239
|
+
"name": "Commander",
|
|
240
|
+
"summary": "cmd",
|
|
241
|
+
"description": "Commander role",
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
IncidentRole.objects.create(incident=incident, user=user, role_type=role_type)
|
|
245
|
+
|
|
246
|
+
service.create_postmortem_for_incident(incident, created_by=user)
|
|
247
|
+
|
|
248
|
+
mock_client.get_jira_user_from_user.assert_called_once_with(user)
|
|
249
|
+
mock_client.assign_issue.assert_called_once_with(
|
|
250
|
+
issue_key="INC-1", account_id="acct-123"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
@staticmethod
|
|
254
|
+
def test_replicate_custom_fields_all_present() -> None:
|
|
255
|
+
"""Test that all custom fields are replicated when present."""
|
|
256
|
+
user: User = UserFactory.create()
|
|
257
|
+
incident: Incident = IncidentFactory.create(
|
|
258
|
+
_status=IncidentStatus.POST_MORTEM,
|
|
259
|
+
created_by=user,
|
|
260
|
+
custom_fields={
|
|
261
|
+
"zendesk_ticket_id": "12345",
|
|
262
|
+
"zoho_desk_ticket_id": "67890",
|
|
263
|
+
"seller_contract_id": "98765",
|
|
264
|
+
"platform": "platform-FR",
|
|
265
|
+
"environments": ["PRD", "STG"],
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Create a Jira user for reporter
|
|
270
|
+
jira_user = JiraUserDB.objects.create(
|
|
271
|
+
id="test-jira-user-id",
|
|
272
|
+
user=user,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Create a Jira ticket with business_impact
|
|
276
|
+
JiraTicket.objects.create(
|
|
277
|
+
id=12345,
|
|
278
|
+
incident=incident,
|
|
279
|
+
key="TEST-123",
|
|
280
|
+
reporter=jira_user,
|
|
281
|
+
business_impact="High",
|
|
282
|
+
)
|
|
283
|
+
incident.refresh_from_db()
|
|
284
|
+
|
|
285
|
+
service = JiraPostMortemService()
|
|
286
|
+
fields = service._generate_issue_fields(incident)
|
|
287
|
+
|
|
288
|
+
# Verify Priority is replicated
|
|
289
|
+
assert "customfield_11064" in fields
|
|
290
|
+
assert fields["customfield_11064"] == {"value": str(incident.priority.value)}
|
|
291
|
+
|
|
292
|
+
# Verify Environments are replicated
|
|
293
|
+
assert "customfield_11049" in fields
|
|
294
|
+
assert fields["customfield_11049"] == [{"value": "PRD"}, {"value": "STG"}]
|
|
295
|
+
|
|
296
|
+
# Verify Zendesk ticket is replicated
|
|
297
|
+
assert "customfield_10895" in fields
|
|
298
|
+
assert fields["customfield_10895"] == "12345"
|
|
299
|
+
|
|
300
|
+
# Verify Zoho desk ticket is replicated
|
|
301
|
+
assert "customfield_10896" in fields
|
|
302
|
+
assert fields["customfield_10896"] == "67890"
|
|
303
|
+
|
|
304
|
+
# Verify Seller Contract ID is replicated
|
|
305
|
+
assert "customfield_10908" in fields
|
|
306
|
+
assert fields["customfield_10908"] == "98765"
|
|
307
|
+
|
|
308
|
+
# Verify Platform is replicated (without "platform-" prefix)
|
|
309
|
+
assert "customfield_10201" in fields
|
|
310
|
+
assert fields["customfield_10201"] == {"value": "FR"}
|
|
311
|
+
|
|
312
|
+
# Verify Business Impact is replicated
|
|
313
|
+
assert "customfield_10936" in fields
|
|
314
|
+
assert fields["customfield_10936"] == {"value": "High"}
|
|
315
|
+
|
|
316
|
+
@staticmethod
|
|
317
|
+
def test_replicate_custom_fields_empty_not_sent() -> None:
|
|
318
|
+
"""Test that empty custom fields are not sent to Jira."""
|
|
319
|
+
user: User = UserFactory.create()
|
|
320
|
+
incident: Incident = IncidentFactory.create(
|
|
321
|
+
_status=IncidentStatus.POST_MORTEM,
|
|
322
|
+
created_by=user,
|
|
323
|
+
custom_fields={}, # No custom fields
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
service = JiraPostMortemService()
|
|
327
|
+
fields = service._generate_issue_fields(incident)
|
|
328
|
+
|
|
329
|
+
# Priority should always be present
|
|
330
|
+
assert "customfield_11064" in fields
|
|
331
|
+
|
|
332
|
+
# Other fields should not be present when empty
|
|
333
|
+
assert "customfield_11049" not in fields # Environments
|
|
334
|
+
assert "customfield_10895" not in fields # Zendesk
|
|
335
|
+
assert "customfield_10896" not in fields # Zoho
|
|
336
|
+
assert "customfield_10908" not in fields # Seller
|
|
337
|
+
assert "customfield_10201" not in fields # Platform
|
|
338
|
+
assert "customfield_10936" not in fields # Business Impact (no jira_ticket)
|
|
339
|
+
|
|
340
|
+
@staticmethod
|
|
341
|
+
def test_replicate_custom_fields_partial() -> None:
|
|
342
|
+
"""Test that only present custom fields are replicated."""
|
|
343
|
+
user: User = UserFactory.create()
|
|
344
|
+
incident: Incident = IncidentFactory.create(
|
|
345
|
+
_status=IncidentStatus.POST_MORTEM,
|
|
346
|
+
created_by=user,
|
|
347
|
+
custom_fields={
|
|
348
|
+
"zendesk_ticket_id": "12345",
|
|
349
|
+
"environments": ["PRD"],
|
|
350
|
+
# seller_contract_id and zoho_desk_ticket_id are missing
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
service = JiraPostMortemService()
|
|
355
|
+
fields = service._generate_issue_fields(incident)
|
|
356
|
+
|
|
357
|
+
# Present fields should be replicated
|
|
358
|
+
assert "customfield_10895" in fields # Zendesk
|
|
359
|
+
assert fields["customfield_10895"] == "12345"
|
|
360
|
+
assert "customfield_11049" in fields # Environments
|
|
361
|
+
assert fields["customfield_11049"] == [{"value": "PRD"}]
|
|
362
|
+
|
|
363
|
+
# Missing fields should not be present
|
|
364
|
+
assert "customfield_10896" not in fields # Zoho
|
|
365
|
+
assert "customfield_10908" not in fields # Seller
|
|
366
|
+
assert "customfield_10201" not in fields # Platform
|
|
367
|
+
|
|
368
|
+
@staticmethod
|
|
369
|
+
def test_business_impact_not_replicated_when_na() -> None:
|
|
370
|
+
"""Test that business_impact is not replicated when it's 'N/A' or empty."""
|
|
371
|
+
user: User = UserFactory.create()
|
|
372
|
+
incident: Incident = IncidentFactory.create(
|
|
373
|
+
_status=IncidentStatus.POST_MORTEM,
|
|
374
|
+
created_by=user,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Create a Jira user for reporter
|
|
378
|
+
jira_user = JiraUserDB.objects.create(
|
|
379
|
+
id="test-jira-user-id-2",
|
|
380
|
+
user=user,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Create a Jira ticket with business_impact = "N/A"
|
|
384
|
+
JiraTicket.objects.create(
|
|
385
|
+
id=12346,
|
|
386
|
+
incident=incident,
|
|
387
|
+
key="TEST-123",
|
|
388
|
+
reporter=jira_user,
|
|
389
|
+
business_impact="N/A",
|
|
390
|
+
)
|
|
391
|
+
incident.refresh_from_db()
|
|
392
|
+
|
|
393
|
+
service = JiraPostMortemService()
|
|
394
|
+
fields = service._generate_issue_fields(incident)
|
|
395
|
+
|
|
396
|
+
# Business Impact should not be present when "N/A"
|
|
397
|
+
assert "customfield_10936" not in fields
|
|
398
|
+
|
|
399
|
+
@staticmethod
|
|
400
|
+
def test_platform_prefix_removal() -> None:
|
|
401
|
+
"""Test that 'platform-' prefix is removed from platform value."""
|
|
402
|
+
user: User = UserFactory.create()
|
|
403
|
+
incident: Incident = IncidentFactory.create(
|
|
404
|
+
_status=IncidentStatus.POST_MORTEM,
|
|
405
|
+
created_by=user,
|
|
406
|
+
custom_fields={
|
|
407
|
+
"platform": "platform-DE",
|
|
408
|
+
},
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
service = JiraPostMortemService()
|
|
412
|
+
fields = service._generate_issue_fields(incident)
|
|
413
|
+
|
|
414
|
+
# Verify platform prefix is removed
|
|
415
|
+
assert "customfield_10201" in fields
|
|
416
|
+
assert fields["customfield_10201"] == {"value": "DE"}
|