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,580 @@
|
|
|
1
|
+
"""Improved tests for raid.client module focusing on coverage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import Mock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from httpx import HTTPError
|
|
9
|
+
from jira.exceptions import JIRAError
|
|
10
|
+
|
|
11
|
+
from firefighter.raid.client import JiraAttachmentError, RaidJiraClient
|
|
12
|
+
from firefighter.raid.models import FeatureTeam
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestJiraAttachmentError:
|
|
16
|
+
"""Test JiraAttachmentError exception."""
|
|
17
|
+
|
|
18
|
+
def test_jira_attachment_error_creation(self):
|
|
19
|
+
"""Test creating JiraAttachmentError."""
|
|
20
|
+
error = JiraAttachmentError("Test error message")
|
|
21
|
+
assert str(error) == "Test error message"
|
|
22
|
+
assert isinstance(error, Exception)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.mark.django_db
|
|
26
|
+
class TestRaidJiraClientBasics:
|
|
27
|
+
"""Test basic RaidJiraClient functionality."""
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def mock_jira_client(self):
|
|
31
|
+
"""Create a minimal mock RaidJiraClient."""
|
|
32
|
+
with patch(
|
|
33
|
+
"firefighter.jira_app.client.JiraClient.__init__", return_value=None
|
|
34
|
+
):
|
|
35
|
+
client = RaidJiraClient()
|
|
36
|
+
client.jira = Mock()
|
|
37
|
+
return client
|
|
38
|
+
|
|
39
|
+
def test_get_projects(self, mock_jira_client):
|
|
40
|
+
"""Test get_projects method."""
|
|
41
|
+
mock_projects = [Mock(key="PROJ1"), Mock(key="PROJ2")]
|
|
42
|
+
mock_jira_client.jira.projects.return_value = mock_projects
|
|
43
|
+
|
|
44
|
+
result = mock_jira_client.get_projects()
|
|
45
|
+
|
|
46
|
+
mock_jira_client.jira.projects.assert_called_once()
|
|
47
|
+
assert result == mock_projects
|
|
48
|
+
|
|
49
|
+
def test_create_issue_basic_success(self, mock_jira_client):
|
|
50
|
+
"""Test basic create_issue success."""
|
|
51
|
+
# Mock a JIRA issue response with .raw attribute
|
|
52
|
+
mock_issue = Mock()
|
|
53
|
+
mock_issue.raw = {
|
|
54
|
+
"id": "12345",
|
|
55
|
+
"key": "TEST-123",
|
|
56
|
+
"fields": {
|
|
57
|
+
"summary": "Test issue",
|
|
58
|
+
"description": "Test description",
|
|
59
|
+
"assignee": {"accountId": "assignee123"},
|
|
60
|
+
"reporter": {"accountId": "reporter123"},
|
|
61
|
+
"issuetype": {"name": "Bug"},
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
mock_jira_client.jira.create_issue.return_value = mock_issue
|
|
65
|
+
|
|
66
|
+
result = mock_jira_client.create_issue(
|
|
67
|
+
issuetype="Bug",
|
|
68
|
+
summary="Test bug",
|
|
69
|
+
description="Bug description",
|
|
70
|
+
assignee=None,
|
|
71
|
+
reporter="test_reporter",
|
|
72
|
+
priority=1,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
mock_jira_client.jira.create_issue.assert_called_once()
|
|
76
|
+
assert result["id"] == 12345
|
|
77
|
+
assert result["key"] == "TEST-123"
|
|
78
|
+
|
|
79
|
+
def test_create_issue_with_valid_business_impact(self, mock_jira_client):
|
|
80
|
+
"""Test create_issue with valid business impact values."""
|
|
81
|
+
mock_issue = Mock()
|
|
82
|
+
mock_issue.raw = {
|
|
83
|
+
"id": "12346",
|
|
84
|
+
"key": "TEST-124",
|
|
85
|
+
"fields": {
|
|
86
|
+
"summary": "Test story",
|
|
87
|
+
"description": "Test description",
|
|
88
|
+
"assignee": {"accountId": "assignee123"},
|
|
89
|
+
"reporter": {"accountId": "reporter123"},
|
|
90
|
+
"issuetype": {"name": "Story"},
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
mock_jira_client.jira.create_issue.return_value = mock_issue
|
|
94
|
+
|
|
95
|
+
# Test each valid business impact value
|
|
96
|
+
for impact in ["Lowest", "Low", "Medium", "High", "Highest"]:
|
|
97
|
+
result = mock_jira_client.create_issue(
|
|
98
|
+
issuetype="Story",
|
|
99
|
+
summary="Test story",
|
|
100
|
+
description="Test description",
|
|
101
|
+
assignee=None,
|
|
102
|
+
reporter="test_reporter",
|
|
103
|
+
priority=2,
|
|
104
|
+
business_impact=impact,
|
|
105
|
+
)
|
|
106
|
+
assert result["id"] == 12346
|
|
107
|
+
|
|
108
|
+
def test_create_issue_with_invalid_business_impact(self, mock_jira_client):
|
|
109
|
+
"""Test create_issue with invalid business impact."""
|
|
110
|
+
with pytest.raises(ValueError, match="Business impact must be"):
|
|
111
|
+
mock_jira_client.create_issue(
|
|
112
|
+
issuetype="Bug",
|
|
113
|
+
summary="Test bug",
|
|
114
|
+
description="Bug description",
|
|
115
|
+
assignee=None,
|
|
116
|
+
reporter="test_reporter",
|
|
117
|
+
priority=1,
|
|
118
|
+
business_impact="Invalid",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def test_create_issue_with_na_business_impact(self, mock_jira_client):
|
|
122
|
+
"""Test create_issue with N/A business impact (should be ignored)."""
|
|
123
|
+
mock_issue = Mock()
|
|
124
|
+
mock_issue.raw = {
|
|
125
|
+
"id": "12347",
|
|
126
|
+
"key": "TEST-125",
|
|
127
|
+
"fields": {
|
|
128
|
+
"summary": "Test task",
|
|
129
|
+
"description": "Task description",
|
|
130
|
+
"assignee": {"accountId": "assignee123"},
|
|
131
|
+
"reporter": {"accountId": "reporter123"},
|
|
132
|
+
"issuetype": {"name": "Task"},
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
mock_jira_client.jira.create_issue.return_value = mock_issue
|
|
136
|
+
|
|
137
|
+
result = mock_jira_client.create_issue(
|
|
138
|
+
issuetype="Task",
|
|
139
|
+
summary="Test task",
|
|
140
|
+
description="Task description",
|
|
141
|
+
assignee=None,
|
|
142
|
+
reporter="test_reporter",
|
|
143
|
+
priority=3,
|
|
144
|
+
business_impact="N/A",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
assert result["id"] == 12347
|
|
148
|
+
|
|
149
|
+
def test_create_issue_with_assignee(self, mock_jira_client):
|
|
150
|
+
"""Test create_issue with assignee."""
|
|
151
|
+
mock_issue = Mock()
|
|
152
|
+
mock_issue.raw = {
|
|
153
|
+
"id": "12348",
|
|
154
|
+
"key": "TEST-126",
|
|
155
|
+
"fields": {
|
|
156
|
+
"summary": "Test task",
|
|
157
|
+
"description": "Task description",
|
|
158
|
+
"assignee": {"accountId": "assignee123"},
|
|
159
|
+
"reporter": {"accountId": "reporter123"},
|
|
160
|
+
"issuetype": {"name": "Task"},
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
mock_jira_client.jira.create_issue.return_value = mock_issue
|
|
164
|
+
|
|
165
|
+
result = mock_jira_client.create_issue(
|
|
166
|
+
issuetype="Task",
|
|
167
|
+
summary="Test task",
|
|
168
|
+
description="Task description",
|
|
169
|
+
assignee="assignee123",
|
|
170
|
+
reporter="test_reporter",
|
|
171
|
+
priority=1,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
assert result["id"] == 12348
|
|
175
|
+
|
|
176
|
+
def test_create_issue_with_none_labels(self, mock_jira_client):
|
|
177
|
+
"""Test create_issue with None labels (should default to empty string)."""
|
|
178
|
+
mock_issue = Mock()
|
|
179
|
+
mock_issue.raw = {
|
|
180
|
+
"id": "12349",
|
|
181
|
+
"key": "TEST-127",
|
|
182
|
+
"fields": {
|
|
183
|
+
"summary": "Test task",
|
|
184
|
+
"description": "Task description",
|
|
185
|
+
"assignee": {"accountId": "assignee123"},
|
|
186
|
+
"reporter": {"accountId": "reporter123"},
|
|
187
|
+
"issuetype": {"name": "Task"},
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
mock_jira_client.jira.create_issue.return_value = mock_issue
|
|
191
|
+
|
|
192
|
+
result = mock_jira_client.create_issue(
|
|
193
|
+
issuetype="Task",
|
|
194
|
+
summary="Test task",
|
|
195
|
+
description="Task description",
|
|
196
|
+
assignee=None,
|
|
197
|
+
reporter="test_reporter",
|
|
198
|
+
priority=None, # Test None priority
|
|
199
|
+
labels=None,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
assert result["id"] == 12349
|
|
203
|
+
|
|
204
|
+
def test_create_issue_with_invalid_priority(self, mock_jira_client):
|
|
205
|
+
"""Test create_issue with invalid priority."""
|
|
206
|
+
with pytest.raises(ValueError, match="Priority must be between 1 and 5"):
|
|
207
|
+
mock_jira_client.create_issue(
|
|
208
|
+
issuetype="Bug",
|
|
209
|
+
summary="Test bug",
|
|
210
|
+
description="Bug description",
|
|
211
|
+
assignee=None,
|
|
212
|
+
reporter="test_reporter",
|
|
213
|
+
priority=6, # Invalid priority
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def test_create_issue_with_all_extra_fields(self, mock_jira_client):
|
|
217
|
+
"""Test create_issue with all extra fields."""
|
|
218
|
+
mock_issue = Mock()
|
|
219
|
+
mock_issue.raw = {
|
|
220
|
+
"id": "12350",
|
|
221
|
+
"key": "TEST-128",
|
|
222
|
+
"fields": {
|
|
223
|
+
"summary": "Test comprehensive",
|
|
224
|
+
"description": "Comprehensive description",
|
|
225
|
+
"assignee": {"accountId": "assignee123"},
|
|
226
|
+
"reporter": {"accountId": "reporter123"},
|
|
227
|
+
"issuetype": {"name": "Story"},
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
mock_jira_client.jira.create_issue.return_value = mock_issue
|
|
231
|
+
|
|
232
|
+
result = mock_jira_client.create_issue(
|
|
233
|
+
issuetype="Story",
|
|
234
|
+
summary="Test comprehensive",
|
|
235
|
+
description="Base description",
|
|
236
|
+
assignee="assignee123",
|
|
237
|
+
reporter="test_reporter",
|
|
238
|
+
priority=2,
|
|
239
|
+
labels=["label1", "label2"],
|
|
240
|
+
zoho_desk_ticket_id="12345",
|
|
241
|
+
zendesk_ticket_id="67890",
|
|
242
|
+
is_seller_in_golden_list=True,
|
|
243
|
+
is_key_account=True,
|
|
244
|
+
seller_contract_id=999,
|
|
245
|
+
suggested_team_routing="TeamA",
|
|
246
|
+
business_impact="High",
|
|
247
|
+
platform="platform-web",
|
|
248
|
+
environments=["production", "staging"],
|
|
249
|
+
incident_category="Performance",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
assert result["id"] == 12350
|
|
253
|
+
|
|
254
|
+
@patch("firefighter.raid.models.FeatureTeam.objects.get")
|
|
255
|
+
def test_create_issue_with_feature_team_routing(
|
|
256
|
+
self, mock_feature_team_get, mock_jira_client
|
|
257
|
+
):
|
|
258
|
+
"""Test create_issue with suggested_team_routing that maps to FeatureTeam."""
|
|
259
|
+
# Mock FeatureTeam
|
|
260
|
+
mock_feature_team = Mock()
|
|
261
|
+
mock_feature_team.jira_project_key = "CUSTOM-PROJ"
|
|
262
|
+
mock_feature_team_get.return_value = mock_feature_team
|
|
263
|
+
|
|
264
|
+
mock_issue = Mock()
|
|
265
|
+
mock_issue.raw = {
|
|
266
|
+
"id": "12351",
|
|
267
|
+
"key": "CUSTOM-129",
|
|
268
|
+
"fields": {
|
|
269
|
+
"summary": "Custom project issue",
|
|
270
|
+
"description": "Custom description",
|
|
271
|
+
"assignee": {"accountId": "assignee123"},
|
|
272
|
+
"reporter": {"accountId": "reporter123"},
|
|
273
|
+
"issuetype": {"name": "Story"},
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
mock_jira_client.jira.create_issue.return_value = mock_issue
|
|
277
|
+
|
|
278
|
+
result = mock_jira_client.create_issue(
|
|
279
|
+
issuetype="Story",
|
|
280
|
+
summary="Custom project issue",
|
|
281
|
+
description="Custom description",
|
|
282
|
+
assignee=None,
|
|
283
|
+
reporter="test_reporter",
|
|
284
|
+
priority=1,
|
|
285
|
+
suggested_team_routing="CustomTeam",
|
|
286
|
+
project=None, # Force the method to look up FeatureTeam
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
mock_feature_team_get.assert_called_once_with(name="CustomTeam")
|
|
290
|
+
assert result["id"] == 12351
|
|
291
|
+
|
|
292
|
+
@patch("firefighter.raid.models.FeatureTeam.objects.get")
|
|
293
|
+
def test_create_issue_with_nonexistent_feature_team(
|
|
294
|
+
self, mock_feature_team_get, mock_jira_client
|
|
295
|
+
):
|
|
296
|
+
"""Test create_issue with suggested_team_routing for nonexistent FeatureTeam."""
|
|
297
|
+
# Mock FeatureTeam.DoesNotExist
|
|
298
|
+
mock_feature_team_get.side_effect = FeatureTeam.DoesNotExist()
|
|
299
|
+
|
|
300
|
+
mock_issue = Mock()
|
|
301
|
+
mock_issue.raw = {
|
|
302
|
+
"id": "12352",
|
|
303
|
+
"key": "DEFAULT-130",
|
|
304
|
+
"fields": {
|
|
305
|
+
"summary": "Default project issue",
|
|
306
|
+
"description": "Default description",
|
|
307
|
+
"assignee": {"accountId": "assignee123"},
|
|
308
|
+
"reporter": {"accountId": "reporter123"},
|
|
309
|
+
"issuetype": {"name": "Bug"},
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
mock_jira_client.jira.create_issue.return_value = mock_issue
|
|
313
|
+
|
|
314
|
+
result = mock_jira_client.create_issue(
|
|
315
|
+
issuetype="Bug",
|
|
316
|
+
summary="Default project issue",
|
|
317
|
+
description="Default description",
|
|
318
|
+
assignee=None,
|
|
319
|
+
reporter="test_reporter",
|
|
320
|
+
priority=1,
|
|
321
|
+
suggested_team_routing="NonexistentTeam",
|
|
322
|
+
project=None, # Force the method to look up FeatureTeam
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
mock_feature_team_get.assert_called_once_with(name="NonexistentTeam")
|
|
326
|
+
assert result["id"] == 12352
|
|
327
|
+
|
|
328
|
+
def test_create_issue_with_explicit_project(self, mock_jira_client):
|
|
329
|
+
"""Test create_issue with explicit project (skips FeatureTeam lookup)."""
|
|
330
|
+
mock_issue = Mock()
|
|
331
|
+
mock_issue.raw = {
|
|
332
|
+
"id": "12353",
|
|
333
|
+
"key": "EXPLICIT-131",
|
|
334
|
+
"fields": {
|
|
335
|
+
"summary": "Explicit project issue",
|
|
336
|
+
"description": "Explicit description",
|
|
337
|
+
"assignee": {"accountId": "assignee123"},
|
|
338
|
+
"reporter": {"accountId": "reporter123"},
|
|
339
|
+
"issuetype": {"name": "Task"},
|
|
340
|
+
},
|
|
341
|
+
}
|
|
342
|
+
mock_jira_client.jira.create_issue.return_value = mock_issue
|
|
343
|
+
|
|
344
|
+
result = mock_jira_client.create_issue(
|
|
345
|
+
issuetype="Task",
|
|
346
|
+
summary="Explicit project issue",
|
|
347
|
+
description="Explicit description",
|
|
348
|
+
assignee=None,
|
|
349
|
+
reporter="test_reporter",
|
|
350
|
+
priority=1,
|
|
351
|
+
suggested_team_routing="SomeTeam",
|
|
352
|
+
project="EXPLICIT-PROJ", # Explicit project bypasses FeatureTeam lookup
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
assert result["id"] == 12353
|
|
356
|
+
|
|
357
|
+
def test_create_issue_with_jira_error(self, mock_jira_client):
|
|
358
|
+
"""Test create_issue when JIRA raises an error."""
|
|
359
|
+
mock_jira_client.jira.create_issue.side_effect = JIRAError("JIRA error")
|
|
360
|
+
|
|
361
|
+
with pytest.raises(JIRAError):
|
|
362
|
+
mock_jira_client.create_issue(
|
|
363
|
+
issuetype="Bug",
|
|
364
|
+
summary="Failed bug",
|
|
365
|
+
description="Bug description",
|
|
366
|
+
assignee=None,
|
|
367
|
+
reporter="test_reporter",
|
|
368
|
+
priority=1,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def test_jira_object_static_method(self):
|
|
372
|
+
"""Test _jira_object static method."""
|
|
373
|
+
test_issue = {
|
|
374
|
+
"id": "12345",
|
|
375
|
+
"key": "TEST-123",
|
|
376
|
+
"fields": {
|
|
377
|
+
"summary": "Test issue",
|
|
378
|
+
"description": "Test description",
|
|
379
|
+
"assignee": {"accountId": "assignee123"},
|
|
380
|
+
"reporter": {"accountId": "reporter123"},
|
|
381
|
+
"issuetype": {"name": "Bug"},
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
result = RaidJiraClient._jira_object(test_issue)
|
|
386
|
+
|
|
387
|
+
assert result["id"] == 12345
|
|
388
|
+
assert result["key"] == "TEST-123"
|
|
389
|
+
assert result["summary"] == "Test issue"
|
|
390
|
+
assert result["description"] == "Test description"
|
|
391
|
+
|
|
392
|
+
def test_jira_object_invalid_type(self):
|
|
393
|
+
"""Test _jira_object with invalid input type."""
|
|
394
|
+
with pytest.raises(AttributeError):
|
|
395
|
+
RaidJiraClient._jira_object("invalid_input")
|
|
396
|
+
|
|
397
|
+
def test_jira_object_missing_id(self):
|
|
398
|
+
"""Test _jira_object with missing ID."""
|
|
399
|
+
test_issue = {
|
|
400
|
+
"key": "TEST-123",
|
|
401
|
+
"fields": {
|
|
402
|
+
"summary": "Test issue",
|
|
403
|
+
"description": "Test description",
|
|
404
|
+
"assignee": {"accountId": "assignee123"},
|
|
405
|
+
"reporter": {"accountId": "reporter123"},
|
|
406
|
+
"issuetype": {"name": "Bug"},
|
|
407
|
+
},
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
with pytest.raises(TypeError, match="Jira ID not found"):
|
|
411
|
+
RaidJiraClient._jira_object(test_issue)
|
|
412
|
+
|
|
413
|
+
def test_jira_object_missing_required_fields(self):
|
|
414
|
+
"""Test _jira_object with missing required fields."""
|
|
415
|
+
test_issue = {
|
|
416
|
+
"id": "12345",
|
|
417
|
+
"key": "TEST-123",
|
|
418
|
+
"fields": {
|
|
419
|
+
"summary": "Test issue",
|
|
420
|
+
"description": None, # Missing description
|
|
421
|
+
"assignee": {"accountId": "assignee123"},
|
|
422
|
+
"reporter": {"accountId": "reporter123"},
|
|
423
|
+
"issuetype": {"name": "Bug"},
|
|
424
|
+
},
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
with pytest.raises(TypeError, match="Jira object has wrong type"):
|
|
428
|
+
RaidJiraClient._jira_object(test_issue)
|
|
429
|
+
|
|
430
|
+
def test_jira_object_missing_key(self):
|
|
431
|
+
"""Test _jira_object with missing key."""
|
|
432
|
+
test_issue = {
|
|
433
|
+
"id": "12345",
|
|
434
|
+
"key": None,
|
|
435
|
+
"fields": {
|
|
436
|
+
"summary": "Test issue",
|
|
437
|
+
"description": "Test description",
|
|
438
|
+
"assignee": {"accountId": "assignee123"},
|
|
439
|
+
"reporter": {"accountId": "reporter123"},
|
|
440
|
+
"issuetype": {"name": "Bug"},
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
with pytest.raises(TypeError, match="Jira key is None"):
|
|
445
|
+
RaidJiraClient._jira_object(test_issue)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@pytest.mark.django_db
|
|
449
|
+
class TestRaidJiraClientAttachments:
|
|
450
|
+
"""Test attachment functionality."""
|
|
451
|
+
|
|
452
|
+
@patch("firefighter.raid.client.HttpClient")
|
|
453
|
+
@patch("firefighter.raid.client.client")
|
|
454
|
+
def test_add_attachments_success(self, mock_client, mock_http_client_class):
|
|
455
|
+
"""Test successful attachment addition."""
|
|
456
|
+
# Setup HTTP client mock
|
|
457
|
+
mock_http_client = Mock()
|
|
458
|
+
mock_http_client_class.return_value = mock_http_client
|
|
459
|
+
|
|
460
|
+
mock_response = Mock()
|
|
461
|
+
mock_response.content = b"fake file content"
|
|
462
|
+
mock_response.headers = {"content-type": "image/png"}
|
|
463
|
+
mock_http_client.get.return_value = mock_response
|
|
464
|
+
|
|
465
|
+
# Setup JIRA mock
|
|
466
|
+
mock_client.jira.add_attachment.return_value = Mock()
|
|
467
|
+
|
|
468
|
+
# Test the method
|
|
469
|
+
RaidJiraClient.add_attachments_to_issue(
|
|
470
|
+
"TEST-123", ["https://example.com/image.png"]
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
mock_http_client.get.assert_called_once_with("https://example.com/image.png")
|
|
474
|
+
mock_client.jira.add_attachment.assert_called_once()
|
|
475
|
+
|
|
476
|
+
@patch("firefighter.raid.client.HttpClient")
|
|
477
|
+
def test_add_attachments_http_error(self, mock_http_client_class):
|
|
478
|
+
"""Test attachment with HTTP error."""
|
|
479
|
+
mock_http_client = Mock()
|
|
480
|
+
mock_http_client_class.return_value = mock_http_client
|
|
481
|
+
mock_http_client.get.side_effect = HTTPError("Network error")
|
|
482
|
+
|
|
483
|
+
with pytest.raises(
|
|
484
|
+
JiraAttachmentError, match="Error while adding attachment to issue"
|
|
485
|
+
):
|
|
486
|
+
RaidJiraClient.add_attachments_to_issue(
|
|
487
|
+
"TEST-123", ["https://bad-url.com/file.png"]
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
@patch("firefighter.raid.client.HttpClient")
|
|
491
|
+
@patch("firefighter.raid.client.client")
|
|
492
|
+
def test_add_attachments_jira_error(self, mock_client, mock_http_client_class):
|
|
493
|
+
"""Test attachment with JIRA error."""
|
|
494
|
+
# Setup HTTP client mock
|
|
495
|
+
mock_http_client = Mock()
|
|
496
|
+
mock_http_client_class.return_value = mock_http_client
|
|
497
|
+
|
|
498
|
+
mock_response = Mock()
|
|
499
|
+
mock_response.content = b"file content"
|
|
500
|
+
mock_response.headers = {"content-type": "text/plain"}
|
|
501
|
+
mock_http_client.get.return_value = mock_response
|
|
502
|
+
|
|
503
|
+
# Setup JIRA to fail
|
|
504
|
+
mock_client.jira.add_attachment.side_effect = JIRAError(
|
|
505
|
+
"JIRA attachment failed"
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
with pytest.raises(
|
|
509
|
+
JiraAttachmentError, match="Error while adding attachment to issue"
|
|
510
|
+
):
|
|
511
|
+
RaidJiraClient.add_attachments_to_issue(
|
|
512
|
+
"TEST-123", ["https://example.com/file.txt"]
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
@pytest.mark.django_db
|
|
517
|
+
class TestRaidJiraClientWorkflow:
|
|
518
|
+
"""Test workflow and configuration methods."""
|
|
519
|
+
|
|
520
|
+
@pytest.fixture
|
|
521
|
+
def workflow_client(self):
|
|
522
|
+
"""Create client for workflow testing."""
|
|
523
|
+
with patch(
|
|
524
|
+
"firefighter.jira_app.client.JiraClient.__init__", return_value=None
|
|
525
|
+
):
|
|
526
|
+
client = RaidJiraClient()
|
|
527
|
+
client.jira = Mock()
|
|
528
|
+
return client
|
|
529
|
+
|
|
530
|
+
def test_close_issue(self, workflow_client):
|
|
531
|
+
"""Test close_issue method."""
|
|
532
|
+
mock_result = Mock()
|
|
533
|
+
|
|
534
|
+
# Mock the workflow method
|
|
535
|
+
workflow_client.transition_issue_auto = Mock(return_value=mock_result)
|
|
536
|
+
|
|
537
|
+
result = workflow_client.close_issue("WORKFLOW-123")
|
|
538
|
+
|
|
539
|
+
workflow_client.transition_issue_auto.assert_called_once_with(
|
|
540
|
+
"WORKFLOW-123", "Closed", "Incident workflow - v2023.03.13"
|
|
541
|
+
)
|
|
542
|
+
assert result == mock_result
|
|
543
|
+
|
|
544
|
+
def test_get_project_config_workflow(self, workflow_client):
|
|
545
|
+
"""Test _get_project_config_workflow method."""
|
|
546
|
+
# Mock the base method
|
|
547
|
+
workflow_client._get_project_config_workflow_base = Mock(
|
|
548
|
+
return_value={"workflows": [{"name": "test_workflow"}]}
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
result = workflow_client._get_project_config_workflow("TEST")
|
|
552
|
+
|
|
553
|
+
workflow_client._get_project_config_workflow_base.assert_called_once_with(
|
|
554
|
+
"TEST", "Incident workflow - v2023.03.13"
|
|
555
|
+
)
|
|
556
|
+
assert "workflows" in result
|
|
557
|
+
|
|
558
|
+
def test_get_project_config_workflow_from_builder(self, workflow_client):
|
|
559
|
+
"""Test _get_project_config_workflow_from_builder method."""
|
|
560
|
+
# Mock the base method
|
|
561
|
+
workflow_client._get_project_config_workflow_from_builder_base = Mock(
|
|
562
|
+
return_value={"workflows": [{"name": "builder_workflow"}]}
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
result = workflow_client._get_project_config_workflow_from_builder()
|
|
566
|
+
|
|
567
|
+
workflow_client._get_project_config_workflow_from_builder_base.assert_called_once_with(
|
|
568
|
+
"Incident workflow - v2023.03.13"
|
|
569
|
+
)
|
|
570
|
+
assert result == {"workflows": [{"name": "builder_workflow"}]}
|
|
571
|
+
|
|
572
|
+
def test_get_project_config_workflow_from_builder_error(self, workflow_client):
|
|
573
|
+
"""Test builder method with HTTP error."""
|
|
574
|
+
# Mock the base method to raise an error
|
|
575
|
+
workflow_client._get_project_config_workflow_from_builder_base = Mock(
|
|
576
|
+
side_effect=HTTPError("HTTP error")
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
with pytest.raises(HTTPError):
|
|
580
|
+
workflow_client._get_project_config_workflow_from_builder()
|