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.
- firefighter/_version.py +16 -3
- firefighter/api/serializers.py +17 -8
- firefighter/api/urls.py +8 -1
- firefighter/api/views/_base.py +1 -1
- firefighter/api/views/components.py +5 -5
- firefighter/api/views/incidents.py +9 -9
- firefighter/confluence/signals/incident_updated.py +2 -2
- firefighter/firefighter/settings/components/raid.py +3 -0
- firefighter/incidents/admin.py +24 -24
- firefighter/incidents/enums.py +22 -2
- firefighter/incidents/factories.py +14 -5
- firefighter/incidents/forms/close_incident.py +4 -4
- firefighter/incidents/forms/closure_reason.py +45 -0
- firefighter/incidents/forms/create_incident.py +4 -4
- firefighter/incidents/forms/unified_incident.py +406 -0
- firefighter/incidents/forms/update_status.py +91 -5
- firefighter/incidents/menus.py +2 -2
- firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +7 -5
- firefighter/incidents/migrations/0009_update_sla.py +7 -5
- firefighter/incidents/migrations/0020_create_incident_category_model.py +64 -0
- firefighter/incidents/migrations/0021_copy_component_data_to_incident_category.py +57 -0
- firefighter/incidents/migrations/0022_add_incident_category_fields.py +34 -0
- firefighter/incidents/migrations/0023_populate_incident_category_references.py +57 -0
- firefighter/incidents/migrations/0024_remove_component_fields_and_model.py +26 -0
- firefighter/incidents/migrations/0025_make_incident_category_required.py +24 -0
- firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py +39 -0
- firefighter/incidents/migrations/0027_add_closure_fields.py +40 -0
- firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
- firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
- firefighter/incidents/models/__init__.py +1 -1
- firefighter/incidents/models/group.py +1 -1
- firefighter/incidents/models/incident.py +47 -20
- firefighter/incidents/models/{component.py → incident_category.py} +30 -29
- firefighter/incidents/models/incident_update.py +3 -3
- firefighter/incidents/static/css/main.min.css +1 -1
- firefighter/incidents/tables.py +9 -9
- firefighter/incidents/templates/layouts/partials/incident_card.html +1 -1
- firefighter/incidents/templates/layouts/partials/incident_timeline.html +2 -2
- firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
- firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html} +13 -13
- firefighter/incidents/templates/pages/{component_list.html → incident_category_list.html} +2 -2
- firefighter/incidents/templates/pages/incident_detail.html +3 -3
- firefighter/incidents/urls.py +6 -6
- firefighter/incidents/views/components/details.py +9 -9
- firefighter/incidents/views/components/list.py +9 -9
- firefighter/incidents/views/reports.py +5 -5
- firefighter/incidents/views/users/details.py +2 -2
- firefighter/incidents/views/views.py +7 -7
- firefighter/jira_app/client.py +1 -1
- firefighter/logging/custom_json_formatter.py +2 -1
- firefighter/pagerduty/tasks/trigger_oncall.py +1 -1
- firefighter/raid/admin.py +0 -11
- firefighter/raid/apps.py +9 -26
- firefighter/raid/client.py +5 -5
- firefighter/raid/forms.py +84 -213
- firefighter/raid/migrations/0003_delete_raidarea.py +16 -0
- firefighter/raid/models.py +2 -21
- firefighter/raid/serializers.py +5 -4
- firefighter/raid/service.py +29 -27
- firefighter/raid/signals/incident_created.py +42 -15
- firefighter/raid/signals/incident_updated.py +3 -2
- firefighter/raid/utils.py +1 -1
- firefighter/raid/views/__init__.py +1 -1
- firefighter/slack/admin.py +8 -8
- firefighter/slack/management/commands/switch_test_users.py +272 -0
- firefighter/slack/messages/slack_messages.py +24 -9
- firefighter/slack/migrations/0005_add_incident_categories_fields.py +33 -0
- firefighter/slack/migrations/0006_copy_components_to_incident_categories.py +57 -0
- firefighter/slack/migrations/0007_remove_components_fields.py +22 -0
- firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py +33 -0
- firefighter/slack/models/conversation.py +3 -3
- firefighter/slack/models/incident_channel.py +1 -1
- firefighter/slack/models/user.py +1 -1
- firefighter/slack/models/user_group.py +3 -3
- firefighter/slack/rules.py +2 -2
- firefighter/slack/signals/create_incident_conversation.py +6 -0
- firefighter/slack/signals/get_users.py +2 -2
- firefighter/slack/signals/incident_updated.py +8 -2
- firefighter/slack/utils.py +2 -2
- firefighter/slack/views/events/home.py +2 -2
- firefighter/slack/views/modals/__init__.py +4 -0
- firefighter/slack/views/modals/base_modal/form_utils.py +78 -0
- firefighter/slack/views/modals/close.py +18 -5
- firefighter/slack/views/modals/closure_reason.py +193 -0
- firefighter/slack/views/modals/open.py +83 -12
- firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
- firefighter/slack/views/modals/opening/details/unified.py +203 -0
- firefighter/slack/views/modals/opening/select_impact.py +5 -2
- firefighter/slack/views/modals/opening/set_details.py +3 -2
- firefighter/slack/views/modals/postmortem.py +10 -2
- firefighter/slack/views/modals/update_status.py +32 -6
- firefighter/slack/views/modals/utils.py +51 -0
- firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/METADATA +2 -2
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/RECORD +133 -88
- firefighter_tests/conftest.py +4 -5
- firefighter_tests/test_api/test_api_landbot.py +1 -1
- firefighter_tests/test_firefighter/test_sso.py +146 -0
- firefighter_tests/test_incidents/test_enums.py +100 -0
- firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
- firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
- firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
- firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
- firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
- firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
- firefighter_tests/test_incidents/test_incident_urls.py +3 -3
- firefighter_tests/test_incidents/test_models/test_incident_category.py +165 -0
- firefighter_tests/test_incidents/test_models/test_incident_model.py +70 -2
- firefighter_tests/test_raid/conftest.py +154 -0
- firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
- firefighter_tests/test_raid/test_priority_mapping.py +267 -0
- firefighter_tests/test_raid/test_raid_client.py +580 -0
- firefighter_tests/test_raid/test_raid_forms.py +552 -0
- firefighter_tests/test_raid/test_raid_models.py +185 -0
- firefighter_tests/test_raid/test_raid_serializers.py +507 -0
- firefighter_tests/test_raid/test_raid_service.py +442 -0
- firefighter_tests/test_raid/test_raid_signals.py +187 -0
- firefighter_tests/test_raid/test_raid_views.py +196 -0
- firefighter_tests/test_slack/messages/__init__.py +0 -0
- firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
- firefighter_tests/test_slack/views/modals/conftest.py +140 -0
- firefighter_tests/test_slack/views/modals/test_close.py +71 -9
- firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
- firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
- firefighter_tests/test_slack/views/modals/test_open.py +146 -2
- firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
- firefighter_tests/test_slack/views/modals/test_update_status.py +331 -7
- firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
- firefighter/raid/views/open_normal.py +0 -139
- firefighter/slack/views/modals/opening/details/critical.py +0 -88
- firefighter_fixtures/raid/area.json +0 -1
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""Tests for raid.service module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import Mock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from firefighter.jira_app.client import JiraAPIError, JiraUserNotFoundError
|
|
10
|
+
from firefighter.raid.service import (
|
|
11
|
+
CustomerIssueData,
|
|
12
|
+
check_issue_id,
|
|
13
|
+
create_issue_customer,
|
|
14
|
+
create_issue_documentation_request,
|
|
15
|
+
create_issue_feature_request,
|
|
16
|
+
create_issue_internal,
|
|
17
|
+
create_issue_seller,
|
|
18
|
+
get_jira_user_from_user,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestCustomerIssueData:
|
|
23
|
+
"""Test the CustomerIssueData dataclass."""
|
|
24
|
+
|
|
25
|
+
def test_customer_issue_data_creation(self):
|
|
26
|
+
"""Test creating CustomerIssueData with all fields."""
|
|
27
|
+
data = CustomerIssueData(
|
|
28
|
+
priority=1,
|
|
29
|
+
labels=["urgent", "customer"],
|
|
30
|
+
platform="web",
|
|
31
|
+
business_impact="high",
|
|
32
|
+
team_to_be_routed="backend-team",
|
|
33
|
+
area="payments",
|
|
34
|
+
zendesk_ticket_id="12345",
|
|
35
|
+
incident_category="payment-issue",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
assert data.priority == 1
|
|
39
|
+
assert data.labels == ["urgent", "customer"]
|
|
40
|
+
assert data.platform == "web"
|
|
41
|
+
assert data.business_impact == "high"
|
|
42
|
+
assert data.team_to_be_routed == "backend-team"
|
|
43
|
+
assert data.area == "payments"
|
|
44
|
+
assert data.zendesk_ticket_id == "12345"
|
|
45
|
+
assert data.incident_category == "payment-issue"
|
|
46
|
+
|
|
47
|
+
def test_customer_issue_data_defaults(self):
|
|
48
|
+
"""Test CustomerIssueData with default values."""
|
|
49
|
+
data = CustomerIssueData(
|
|
50
|
+
priority=None,
|
|
51
|
+
labels=None,
|
|
52
|
+
platform="mobile",
|
|
53
|
+
business_impact=None,
|
|
54
|
+
team_to_be_routed=None,
|
|
55
|
+
area=None,
|
|
56
|
+
zendesk_ticket_id=None,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
assert data.priority is None
|
|
60
|
+
assert data.labels is None
|
|
61
|
+
assert data.platform == "mobile"
|
|
62
|
+
assert data.business_impact is None
|
|
63
|
+
assert data.team_to_be_routed is None
|
|
64
|
+
assert data.area is None
|
|
65
|
+
assert data.zendesk_ticket_id is None
|
|
66
|
+
assert data.incident_category is None # Default value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.django_db
|
|
70
|
+
class TestGetJiraUserFromUser:
|
|
71
|
+
"""Test get_jira_user_from_user function."""
|
|
72
|
+
|
|
73
|
+
@patch("firefighter.raid.service.jira_client")
|
|
74
|
+
def test_get_jira_user_success(self, mock_jira_client, admin_user):
|
|
75
|
+
"""Test successful get_jira_user_from_user."""
|
|
76
|
+
mock_jira_user = Mock()
|
|
77
|
+
mock_jira_user.id = "test_jira_id"
|
|
78
|
+
mock_jira_client.get_jira_user_from_user.return_value = mock_jira_user
|
|
79
|
+
|
|
80
|
+
result = get_jira_user_from_user(admin_user)
|
|
81
|
+
|
|
82
|
+
mock_jira_client.get_jira_user_from_user.assert_called_once_with(admin_user)
|
|
83
|
+
assert result == mock_jira_user
|
|
84
|
+
|
|
85
|
+
@patch("firefighter.raid.service.jira_client")
|
|
86
|
+
def test_get_jira_user_fallback_to_default(self, mock_jira_client, admin_user):
|
|
87
|
+
"""Test fallback to default user when jira_client fails."""
|
|
88
|
+
|
|
89
|
+
# Make the first call fail
|
|
90
|
+
mock_jira_client.get_jira_user_from_user.side_effect = JiraAPIError("API error")
|
|
91
|
+
|
|
92
|
+
# Mock the fallback call to succeed
|
|
93
|
+
mock_fallback_user = Mock()
|
|
94
|
+
mock_fallback_user.id = "fallback_id"
|
|
95
|
+
mock_jira_client.get_jira_user_from_jira_id.return_value = mock_fallback_user
|
|
96
|
+
|
|
97
|
+
result = get_jira_user_from_user(admin_user)
|
|
98
|
+
|
|
99
|
+
# Should call the fallback method
|
|
100
|
+
mock_jira_client.get_jira_user_from_jira_id.assert_called_once()
|
|
101
|
+
assert result == mock_fallback_user
|
|
102
|
+
|
|
103
|
+
@patch("firefighter.raid.service.jira_client")
|
|
104
|
+
@patch("firefighter.raid.service.JiraUser")
|
|
105
|
+
def test_get_jira_user_fallback_to_db(self, mock_jira_user_model, mock_jira_client, admin_user):
|
|
106
|
+
"""Test fallback to database when both API calls fail."""
|
|
107
|
+
|
|
108
|
+
# Make both API calls fail
|
|
109
|
+
mock_jira_client.get_jira_user_from_user.side_effect = JiraAPIError("API error")
|
|
110
|
+
mock_jira_client.get_jira_user_from_jira_id.side_effect = JiraUserNotFoundError("User not found")
|
|
111
|
+
|
|
112
|
+
# Mock database fallback
|
|
113
|
+
mock_db_user = Mock()
|
|
114
|
+
mock_db_user.id = "db_user_id"
|
|
115
|
+
mock_jira_user_model.objects.get.return_value = mock_db_user
|
|
116
|
+
|
|
117
|
+
result = get_jira_user_from_user(admin_user)
|
|
118
|
+
|
|
119
|
+
# Should try both API calls then fallback to DB
|
|
120
|
+
mock_jira_client.get_jira_user_from_user.assert_called_once_with(admin_user)
|
|
121
|
+
mock_jira_client.get_jira_user_from_jira_id.assert_called_once()
|
|
122
|
+
mock_jira_user_model.objects.get.assert_called_once()
|
|
123
|
+
assert result == mock_db_user
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestCheckIssueId:
|
|
127
|
+
"""Test check_issue_id function."""
|
|
128
|
+
|
|
129
|
+
def test_check_issue_id_with_valid_issue(self):
|
|
130
|
+
"""Test check_issue_id with valid issue object."""
|
|
131
|
+
mock_issue = {"id": "TICKET-123"}
|
|
132
|
+
|
|
133
|
+
result = check_issue_id(mock_issue, "Test Title", "test_reporter")
|
|
134
|
+
assert result == "TICKET-123"
|
|
135
|
+
|
|
136
|
+
def test_check_issue_id_with_none_issue(self):
|
|
137
|
+
"""Test check_issue_id with None issue should raise AttributeError."""
|
|
138
|
+
# The actual code doesn't check for None, so it raises AttributeError
|
|
139
|
+
with pytest.raises(AttributeError):
|
|
140
|
+
check_issue_id(None, "Test Title", "test_reporter")
|
|
141
|
+
|
|
142
|
+
def test_check_issue_id_with_issue_without_id(self):
|
|
143
|
+
"""Test check_issue_id with issue object without id."""
|
|
144
|
+
mock_issue = {"id": None}
|
|
145
|
+
|
|
146
|
+
with pytest.raises(JiraAPIError):
|
|
147
|
+
check_issue_id(mock_issue, "Test Title", "test_reporter")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.django_db
|
|
151
|
+
class TestCreateIssueFunctions:
|
|
152
|
+
"""Test the create_issue_* functions."""
|
|
153
|
+
|
|
154
|
+
@patch("firefighter.raid.service.jira_client")
|
|
155
|
+
def test_create_issue_customer(self, mock_jira_client):
|
|
156
|
+
"""Test create_issue_customer function."""
|
|
157
|
+
# Setup mock
|
|
158
|
+
mock_issue = Mock()
|
|
159
|
+
mock_issue.id = "CUST-123"
|
|
160
|
+
mock_jira_client.create_issue.return_value = mock_issue
|
|
161
|
+
|
|
162
|
+
# Create test data
|
|
163
|
+
issue_data = CustomerIssueData(
|
|
164
|
+
priority=1,
|
|
165
|
+
labels=["customer"],
|
|
166
|
+
platform="web",
|
|
167
|
+
business_impact="high",
|
|
168
|
+
team_to_be_routed="support-team",
|
|
169
|
+
area="billing",
|
|
170
|
+
zendesk_ticket_id="ZD-456",
|
|
171
|
+
incident_category="billing-issue",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Call function
|
|
175
|
+
result = create_issue_customer(
|
|
176
|
+
title="Customer Issue",
|
|
177
|
+
description="Customer is experiencing billing problems",
|
|
178
|
+
reporter="reporter_id",
|
|
179
|
+
issue_data=issue_data,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Verify jira_client.create_issue was called with correct parameters
|
|
183
|
+
mock_jira_client.create_issue.assert_called_once_with(
|
|
184
|
+
issuetype="Incident",
|
|
185
|
+
summary="Customer Issue",
|
|
186
|
+
description="Customer is experiencing billing problems",
|
|
187
|
+
assignee=None,
|
|
188
|
+
reporter="reporter_id",
|
|
189
|
+
priority=1,
|
|
190
|
+
labels=["customer"],
|
|
191
|
+
platform="web",
|
|
192
|
+
business_impact="high",
|
|
193
|
+
suggested_team_routing="support-team",
|
|
194
|
+
zendesk_ticket_id="ZD-456",
|
|
195
|
+
incident_category="billing-issue",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
assert result == mock_issue
|
|
199
|
+
|
|
200
|
+
@patch("firefighter.raid.service.jira_client")
|
|
201
|
+
def test_create_issue_feature_request(self, mock_jira_client):
|
|
202
|
+
"""Test create_issue_feature_request function."""
|
|
203
|
+
mock_issue = {"id": "FEAT-123"}
|
|
204
|
+
mock_jira_client.create_issue.return_value = mock_issue
|
|
205
|
+
|
|
206
|
+
result = create_issue_feature_request(
|
|
207
|
+
title="New Feature",
|
|
208
|
+
description="Feature description",
|
|
209
|
+
reporter="reporter_id",
|
|
210
|
+
priority=2,
|
|
211
|
+
labels=["enhancement"],
|
|
212
|
+
platform="mobile",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
mock_jira_client.create_issue.assert_called_once()
|
|
216
|
+
call_args = mock_jira_client.create_issue.call_args
|
|
217
|
+
assert call_args[1]["issuetype"] == "Feature Request"
|
|
218
|
+
assert call_args[1]["summary"] == "New Feature"
|
|
219
|
+
assert call_args[1]["description"] == "Feature description"
|
|
220
|
+
assert call_args[1]["reporter"] == "reporter_id"
|
|
221
|
+
assert call_args[1]["priority"] == 2
|
|
222
|
+
|
|
223
|
+
assert result == mock_issue
|
|
224
|
+
|
|
225
|
+
@patch("firefighter.raid.service.jira_client")
|
|
226
|
+
def test_create_issue_feature_request_with_none_labels(self, mock_jira_client):
|
|
227
|
+
"""Test create_issue_feature_request with None labels (covers line 75)."""
|
|
228
|
+
mock_issue = {"id": "FEAT-124"}
|
|
229
|
+
mock_jira_client.create_issue.return_value = mock_issue
|
|
230
|
+
|
|
231
|
+
result = create_issue_feature_request(
|
|
232
|
+
title="Feature with None labels",
|
|
233
|
+
description="Feature description",
|
|
234
|
+
reporter="reporter_id",
|
|
235
|
+
priority=1,
|
|
236
|
+
labels=None, # This will trigger line 75: labels = [""]
|
|
237
|
+
platform="web",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
mock_jira_client.create_issue.assert_called_once()
|
|
241
|
+
call_args = mock_jira_client.create_issue.call_args
|
|
242
|
+
# Should have default labels + feature-request
|
|
243
|
+
expected_labels = ["", "feature-request"]
|
|
244
|
+
assert call_args[1]["labels"] == expected_labels
|
|
245
|
+
assert result == mock_issue
|
|
246
|
+
|
|
247
|
+
@patch("firefighter.raid.service.jira_client")
|
|
248
|
+
def test_create_issue_feature_request_with_existing_label(self, mock_jira_client):
|
|
249
|
+
"""Test create_issue_feature_request when label already exists (covers branch 76->78)."""
|
|
250
|
+
mock_issue = {"id": "FEAT-125"}
|
|
251
|
+
mock_jira_client.create_issue.return_value = mock_issue
|
|
252
|
+
|
|
253
|
+
result = create_issue_feature_request(
|
|
254
|
+
title="Feature with existing label",
|
|
255
|
+
description="Feature description",
|
|
256
|
+
reporter="reporter_id",
|
|
257
|
+
priority=1,
|
|
258
|
+
labels=["custom", "feature-request"], # Already has feature-request
|
|
259
|
+
platform="web",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
mock_jira_client.create_issue.assert_called_once()
|
|
263
|
+
call_args = mock_jira_client.create_issue.call_args
|
|
264
|
+
# Should not duplicate the feature-request label
|
|
265
|
+
expected_labels = ["custom", "feature-request"]
|
|
266
|
+
assert call_args[1]["labels"] == expected_labels
|
|
267
|
+
assert result == mock_issue
|
|
268
|
+
|
|
269
|
+
@patch("firefighter.raid.service.jira_client")
|
|
270
|
+
def test_create_issue_documentation_request(self, mock_jira_client):
|
|
271
|
+
"""Test create_issue_documentation_request function."""
|
|
272
|
+
mock_issue = {"id": "DOC-123"}
|
|
273
|
+
mock_jira_client.create_issue.return_value = mock_issue
|
|
274
|
+
|
|
275
|
+
result = create_issue_documentation_request(
|
|
276
|
+
title="Update Documentation",
|
|
277
|
+
description="Documentation needs updating",
|
|
278
|
+
reporter="reporter_id",
|
|
279
|
+
priority=3,
|
|
280
|
+
labels=["docs"],
|
|
281
|
+
platform="web",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
mock_jira_client.create_issue.assert_called_once()
|
|
285
|
+
call_args = mock_jira_client.create_issue.call_args
|
|
286
|
+
assert call_args[1]["issuetype"] == "Documentation/Process Request"
|
|
287
|
+
assert call_args[1]["summary"] == "Update Documentation"
|
|
288
|
+
|
|
289
|
+
assert result == mock_issue
|
|
290
|
+
|
|
291
|
+
@patch("firefighter.raid.service.jira_client")
|
|
292
|
+
def test_create_issue_documentation_request_with_none_labels(self, mock_jira_client):
|
|
293
|
+
"""Test create_issue_documentation_request with None labels (covers line 111)."""
|
|
294
|
+
mock_issue = {"id": "DOC-124"}
|
|
295
|
+
mock_jira_client.create_issue.return_value = mock_issue
|
|
296
|
+
|
|
297
|
+
result = create_issue_documentation_request(
|
|
298
|
+
title="Doc with None labels",
|
|
299
|
+
description="Documentation description",
|
|
300
|
+
reporter="reporter_id",
|
|
301
|
+
priority=2,
|
|
302
|
+
labels=None, # This will trigger line 111: labels = [""]
|
|
303
|
+
platform="mobile",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
mock_jira_client.create_issue.assert_called_once()
|
|
307
|
+
call_args = mock_jira_client.create_issue.call_args
|
|
308
|
+
# Should have default labels + documentation-request
|
|
309
|
+
expected_labels = ["", "documentation-request"]
|
|
310
|
+
assert call_args[1]["labels"] == expected_labels
|
|
311
|
+
assert result == mock_issue
|
|
312
|
+
|
|
313
|
+
@patch("firefighter.raid.service.jira_client")
|
|
314
|
+
def test_create_issue_documentation_request_with_existing_label(self, mock_jira_client):
|
|
315
|
+
"""Test create_issue_documentation_request when label already exists (covers branch 112->114)."""
|
|
316
|
+
mock_issue = {"id": "DOC-125"}
|
|
317
|
+
mock_jira_client.create_issue.return_value = mock_issue
|
|
318
|
+
|
|
319
|
+
result = create_issue_documentation_request(
|
|
320
|
+
title="Doc with existing label",
|
|
321
|
+
description="Documentation description",
|
|
322
|
+
reporter="reporter_id",
|
|
323
|
+
priority=2,
|
|
324
|
+
labels=["help", "documentation-request"], # Already has documentation-request
|
|
325
|
+
platform="mobile",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
mock_jira_client.create_issue.assert_called_once()
|
|
329
|
+
call_args = mock_jira_client.create_issue.call_args
|
|
330
|
+
# Should not duplicate the documentation-request label
|
|
331
|
+
expected_labels = ["help", "documentation-request"]
|
|
332
|
+
assert call_args[1]["labels"] == expected_labels
|
|
333
|
+
assert result == mock_issue
|
|
334
|
+
|
|
335
|
+
@patch("firefighter.raid.service.jira_client")
|
|
336
|
+
def test_create_issue_internal(self, mock_jira_client):
|
|
337
|
+
"""Test create_issue_internal function."""
|
|
338
|
+
mock_issue = {"id": "INT-123"}
|
|
339
|
+
mock_jira_client.create_issue.return_value = mock_issue
|
|
340
|
+
|
|
341
|
+
result = create_issue_internal(
|
|
342
|
+
title="Internal Issue",
|
|
343
|
+
description="Internal issue description",
|
|
344
|
+
reporter="reporter_id",
|
|
345
|
+
priority=1,
|
|
346
|
+
labels=["internal"],
|
|
347
|
+
platform="api",
|
|
348
|
+
business_impact="critical",
|
|
349
|
+
team_to_be_routed="backend-team",
|
|
350
|
+
incident_category="infrastructure",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
mock_jira_client.create_issue.assert_called_once()
|
|
354
|
+
call_args = mock_jira_client.create_issue.call_args
|
|
355
|
+
assert call_args[1]["issuetype"] == "Incident"
|
|
356
|
+
assert call_args[1]["summary"] == "Internal Issue"
|
|
357
|
+
|
|
358
|
+
assert result == mock_issue
|
|
359
|
+
|
|
360
|
+
@patch("firefighter.raid.service.jira_client")
|
|
361
|
+
def test_create_issue_seller(self, mock_jira_client):
|
|
362
|
+
"""Test create_issue_seller function."""
|
|
363
|
+
mock_issue = {"id": "SELL-123"}
|
|
364
|
+
mock_jira_client.create_issue.return_value = mock_issue
|
|
365
|
+
|
|
366
|
+
result = create_issue_seller(
|
|
367
|
+
title="Seller Issue",
|
|
368
|
+
description="Seller issue description",
|
|
369
|
+
reporter="reporter_id",
|
|
370
|
+
priority=2,
|
|
371
|
+
labels=["seller"],
|
|
372
|
+
platform="web",
|
|
373
|
+
business_impact="medium",
|
|
374
|
+
team_to_be_routed="seller-team",
|
|
375
|
+
incident_category="marketplace",
|
|
376
|
+
seller_contract_id="CONTRACT-123",
|
|
377
|
+
is_key_account=True,
|
|
378
|
+
is_seller_in_golden_list=False,
|
|
379
|
+
zoho_desk_ticket_id="ZOHO-456",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
mock_jira_client.create_issue.assert_called_once()
|
|
383
|
+
call_args = mock_jira_client.create_issue.call_args
|
|
384
|
+
assert call_args[1]["issuetype"] == "Incident"
|
|
385
|
+
assert call_args[1]["summary"] == "Seller Issue"
|
|
386
|
+
|
|
387
|
+
assert result == mock_issue
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@pytest.mark.django_db
|
|
391
|
+
class TestCreateIssueErrorHandling:
|
|
392
|
+
"""Test error handling in create_issue functions."""
|
|
393
|
+
|
|
394
|
+
@patch("firefighter.raid.service.jira_client")
|
|
395
|
+
def test_create_issue_customer_with_jira_client_returning_none(self, mock_jira_client):
|
|
396
|
+
"""Test create_issue_customer when jira_client returns None."""
|
|
397
|
+
mock_jira_client.create_issue.return_value = None
|
|
398
|
+
|
|
399
|
+
issue_data = CustomerIssueData(
|
|
400
|
+
priority=1,
|
|
401
|
+
labels=["test"],
|
|
402
|
+
platform="web",
|
|
403
|
+
business_impact="low",
|
|
404
|
+
team_to_be_routed="test-team",
|
|
405
|
+
area="test",
|
|
406
|
+
zendesk_ticket_id="123",
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
with pytest.raises(AttributeError):
|
|
410
|
+
create_issue_customer(
|
|
411
|
+
title="Test",
|
|
412
|
+
description="Test description",
|
|
413
|
+
reporter="test_reporter",
|
|
414
|
+
issue_data=issue_data,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
@patch("firefighter.raid.service.jira_client")
|
|
418
|
+
def test_create_issue_customer_with_empty_issue_data(self, mock_jira_client):
|
|
419
|
+
"""Test create_issue_customer with minimal issue_data."""
|
|
420
|
+
mock_issue = {"id": "TEST-123"}
|
|
421
|
+
mock_jira_client.create_issue.return_value = mock_issue
|
|
422
|
+
|
|
423
|
+
issue_data = CustomerIssueData(
|
|
424
|
+
priority=None,
|
|
425
|
+
labels=None,
|
|
426
|
+
platform="test",
|
|
427
|
+
business_impact=None,
|
|
428
|
+
team_to_be_routed=None,
|
|
429
|
+
area=None,
|
|
430
|
+
zendesk_ticket_id=None,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
result = create_issue_customer(
|
|
434
|
+
title="Minimal Test",
|
|
435
|
+
description="Minimal test description",
|
|
436
|
+
reporter="test_reporter",
|
|
437
|
+
issue_data=issue_data,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Should still work with None values
|
|
441
|
+
mock_jira_client.create_issue.assert_called_once()
|
|
442
|
+
assert result == mock_issue
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Tests for RAID signal handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import Mock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from firefighter.incidents.enums import IncidentStatus
|
|
10
|
+
from firefighter.incidents.models.incident_update import IncidentUpdate
|
|
11
|
+
from firefighter.raid.signals.incident_updated import (
|
|
12
|
+
incident_updated_close_ticket_when_mitigated_or_postmortem,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.django_db
|
|
17
|
+
class TestIncidentUpdatedCloseJiraTicket:
|
|
18
|
+
"""Test that Jira tickets are closed when incidents reach terminal statuses."""
|
|
19
|
+
|
|
20
|
+
@patch("firefighter.raid.signals.incident_updated.client.close_issue")
|
|
21
|
+
def test_close_jira_ticket_when_status_changes_to_mitigated(
|
|
22
|
+
self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Test that Jira ticket is closed when incident status changes to MITIGATED."""
|
|
25
|
+
user = user_factory()
|
|
26
|
+
incident = incident_factory(created_by=user)
|
|
27
|
+
jira_ticket = jira_ticket_factory(incident=incident)
|
|
28
|
+
incident.jira_ticket = jira_ticket
|
|
29
|
+
|
|
30
|
+
incident_update = IncidentUpdate(
|
|
31
|
+
incident=incident,
|
|
32
|
+
status=IncidentStatus.MITIGATED,
|
|
33
|
+
created_by=user,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Call the signal handler
|
|
37
|
+
incident_updated_close_ticket_when_mitigated_or_postmortem(
|
|
38
|
+
sender="update_status",
|
|
39
|
+
incident=incident,
|
|
40
|
+
incident_update=incident_update,
|
|
41
|
+
updated_fields=["_status"],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Verify close_issue was called
|
|
45
|
+
mock_close_issue.assert_called_once_with(issue_id=jira_ticket.id)
|
|
46
|
+
|
|
47
|
+
@patch("firefighter.raid.signals.incident_updated.client.close_issue")
|
|
48
|
+
def test_close_jira_ticket_when_status_changes_to_postmortem(
|
|
49
|
+
self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Test that Jira ticket is closed when incident status changes to POST_MORTEM."""
|
|
52
|
+
user = user_factory()
|
|
53
|
+
incident = incident_factory(created_by=user)
|
|
54
|
+
jira_ticket = jira_ticket_factory(incident=incident)
|
|
55
|
+
incident.jira_ticket = jira_ticket
|
|
56
|
+
|
|
57
|
+
incident_update = IncidentUpdate(
|
|
58
|
+
incident=incident,
|
|
59
|
+
status=IncidentStatus.POST_MORTEM,
|
|
60
|
+
created_by=user,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Call the signal handler
|
|
64
|
+
incident_updated_close_ticket_when_mitigated_or_postmortem(
|
|
65
|
+
sender="update_status",
|
|
66
|
+
incident=incident,
|
|
67
|
+
incident_update=incident_update,
|
|
68
|
+
updated_fields=["_status"],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Verify close_issue was called
|
|
72
|
+
mock_close_issue.assert_called_once_with(issue_id=jira_ticket.id)
|
|
73
|
+
|
|
74
|
+
@patch("firefighter.raid.signals.incident_updated.client.close_issue")
|
|
75
|
+
def test_close_jira_ticket_when_status_changes_to_closed(
|
|
76
|
+
self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Test that Jira ticket is closed when incident status changes to CLOSED (direct close)."""
|
|
79
|
+
user = user_factory()
|
|
80
|
+
incident = incident_factory(created_by=user)
|
|
81
|
+
jira_ticket = jira_ticket_factory(incident=incident)
|
|
82
|
+
incident.jira_ticket = jira_ticket
|
|
83
|
+
|
|
84
|
+
incident_update = IncidentUpdate(
|
|
85
|
+
incident=incident,
|
|
86
|
+
status=IncidentStatus.CLOSED,
|
|
87
|
+
created_by=user,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Call the signal handler
|
|
91
|
+
incident_updated_close_ticket_when_mitigated_or_postmortem(
|
|
92
|
+
sender="update_status",
|
|
93
|
+
incident=incident,
|
|
94
|
+
incident_update=incident_update,
|
|
95
|
+
updated_fields=["_status"],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Verify close_issue was called
|
|
99
|
+
mock_close_issue.assert_called_once_with(issue_id=jira_ticket.id)
|
|
100
|
+
|
|
101
|
+
@patch("firefighter.raid.signals.incident_updated.client.close_issue")
|
|
102
|
+
def test_do_not_close_jira_ticket_when_status_not_terminal(
|
|
103
|
+
self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Test that Jira ticket is NOT closed for non-terminal statuses."""
|
|
106
|
+
user = user_factory()
|
|
107
|
+
incident = incident_factory(created_by=user)
|
|
108
|
+
jira_ticket = jira_ticket_factory(incident=incident)
|
|
109
|
+
incident.jira_ticket = jira_ticket
|
|
110
|
+
|
|
111
|
+
incident_update = IncidentUpdate(
|
|
112
|
+
incident=incident,
|
|
113
|
+
status=IncidentStatus.INVESTIGATING,
|
|
114
|
+
created_by=user,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Call the signal handler
|
|
118
|
+
incident_updated_close_ticket_when_mitigated_or_postmortem(
|
|
119
|
+
sender="update_status",
|
|
120
|
+
incident=incident,
|
|
121
|
+
incident_update=incident_update,
|
|
122
|
+
updated_fields=["_status"],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Verify close_issue was NOT called
|
|
126
|
+
mock_close_issue.assert_not_called()
|
|
127
|
+
|
|
128
|
+
@patch("firefighter.raid.signals.incident_updated.client.close_issue")
|
|
129
|
+
def test_do_not_close_jira_ticket_when_status_not_updated(
|
|
130
|
+
self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Test that Jira ticket is NOT closed when _status is not in updated_fields."""
|
|
133
|
+
user = user_factory()
|
|
134
|
+
incident = incident_factory(created_by=user)
|
|
135
|
+
jira_ticket = jira_ticket_factory(incident=incident)
|
|
136
|
+
incident.jira_ticket = jira_ticket
|
|
137
|
+
|
|
138
|
+
incident_update = IncidentUpdate(
|
|
139
|
+
incident=incident,
|
|
140
|
+
status=IncidentStatus.CLOSED,
|
|
141
|
+
created_by=user,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Call the signal handler with updated_fields that don't include _status
|
|
145
|
+
incident_updated_close_ticket_when_mitigated_or_postmortem(
|
|
146
|
+
sender="update_status",
|
|
147
|
+
incident=incident,
|
|
148
|
+
incident_update=incident_update,
|
|
149
|
+
updated_fields=["priority_id"], # Not _status
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Verify close_issue was NOT called
|
|
153
|
+
mock_close_issue.assert_not_called()
|
|
154
|
+
|
|
155
|
+
@patch("firefighter.raid.signals.incident_updated.client.close_issue")
|
|
156
|
+
@patch("firefighter.raid.signals.incident_updated.logger")
|
|
157
|
+
def test_do_not_crash_when_jira_ticket_missing(
|
|
158
|
+
self,
|
|
159
|
+
mock_logger: Mock,
|
|
160
|
+
mock_close_issue: Mock,
|
|
161
|
+
incident_factory,
|
|
162
|
+
user_factory,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Test that signal handler handles gracefully when Jira ticket is missing."""
|
|
165
|
+
user = user_factory()
|
|
166
|
+
incident = incident_factory(created_by=user)
|
|
167
|
+
# No jira_ticket attached to incident
|
|
168
|
+
|
|
169
|
+
incident_update = IncidentUpdate(
|
|
170
|
+
incident=incident,
|
|
171
|
+
status=IncidentStatus.CLOSED,
|
|
172
|
+
created_by=user,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Call the signal handler - should not crash
|
|
176
|
+
incident_updated_close_ticket_when_mitigated_or_postmortem(
|
|
177
|
+
sender="update_status",
|
|
178
|
+
incident=incident,
|
|
179
|
+
incident_update=incident_update,
|
|
180
|
+
updated_fields=["_status"],
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Verify close_issue was NOT called
|
|
184
|
+
mock_close_issue.assert_not_called()
|
|
185
|
+
|
|
186
|
+
# Verify a warning was logged
|
|
187
|
+
mock_logger.warning.assert_called_once()
|