firefighter-incident 0.0.13__py3-none-any.whl → 0.0.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. firefighter/_version.py +16 -3
  2. firefighter/api/serializers.py +8 -8
  3. firefighter/api/urls.py +8 -1
  4. firefighter/api/views/_base.py +1 -1
  5. firefighter/api/views/components.py +5 -5
  6. firefighter/api/views/incidents.py +9 -9
  7. firefighter/firefighter/settings/components/raid.py +3 -0
  8. firefighter/incidents/admin.py +24 -24
  9. firefighter/incidents/factories.py +14 -5
  10. firefighter/incidents/forms/close_incident.py +4 -4
  11. firefighter/incidents/forms/create_incident.py +4 -4
  12. firefighter/incidents/forms/update_status.py +4 -4
  13. firefighter/incidents/menus.py +2 -2
  14. firefighter/incidents/migrations/0005_enable_from_p1_to_p5_priority.py +7 -5
  15. firefighter/incidents/migrations/0009_update_sla.py +7 -5
  16. firefighter/incidents/migrations/0020_create_incident_category_model.py +64 -0
  17. firefighter/incidents/migrations/0021_copy_component_data_to_incident_category.py +57 -0
  18. firefighter/incidents/migrations/0022_add_incident_category_fields.py +34 -0
  19. firefighter/incidents/migrations/0023_populate_incident_category_references.py +57 -0
  20. firefighter/incidents/migrations/0024_remove_component_fields_and_model.py +26 -0
  21. firefighter/incidents/migrations/0025_make_incident_category_required.py +24 -0
  22. firefighter/incidents/migrations/0026_alter_incidentcategory_options_and_more.py +39 -0
  23. firefighter/incidents/models/__init__.py +1 -1
  24. firefighter/incidents/models/group.py +1 -1
  25. firefighter/incidents/models/incident.py +15 -15
  26. firefighter/incidents/models/{component.py → incident_category.py} +30 -29
  27. firefighter/incidents/models/incident_update.py +3 -3
  28. firefighter/incidents/tables.py +9 -9
  29. firefighter/incidents/templates/layouts/partials/incident_card.html +1 -1
  30. firefighter/incidents/templates/layouts/partials/incident_timeline.html +2 -2
  31. firefighter/incidents/templates/pages/{component_detail.html → incident_category_detail.html} +13 -13
  32. firefighter/incidents/templates/pages/{component_list.html → incident_category_list.html} +2 -2
  33. firefighter/incidents/templates/pages/incident_detail.html +3 -3
  34. firefighter/incidents/urls.py +6 -6
  35. firefighter/incidents/views/components/details.py +9 -9
  36. firefighter/incidents/views/components/list.py +9 -9
  37. firefighter/incidents/views/reports.py +2 -2
  38. firefighter/incidents/views/users/details.py +2 -2
  39. firefighter/incidents/views/views.py +7 -7
  40. firefighter/jira_app/client.py +1 -1
  41. firefighter/logging/custom_json_formatter.py +2 -1
  42. firefighter/pagerduty/tasks/trigger_oncall.py +1 -1
  43. firefighter/raid/admin.py +0 -11
  44. firefighter/raid/client.py +3 -3
  45. firefighter/raid/forms.py +53 -19
  46. firefighter/raid/migrations/0003_delete_raidarea.py +16 -0
  47. firefighter/raid/models.py +2 -21
  48. firefighter/raid/serializers.py +5 -4
  49. firefighter/raid/service.py +29 -27
  50. firefighter/raid/signals/incident_created.py +4 -2
  51. firefighter/raid/utils.py +1 -1
  52. firefighter/raid/views/__init__.py +1 -1
  53. firefighter/raid/views/open_normal.py +2 -2
  54. firefighter/slack/admin.py +8 -8
  55. firefighter/slack/management/commands/switch_test_users.py +272 -0
  56. firefighter/slack/messages/slack_messages.py +5 -5
  57. firefighter/slack/migrations/0005_add_incident_categories_fields.py +33 -0
  58. firefighter/slack/migrations/0006_copy_components_to_incident_categories.py +57 -0
  59. firefighter/slack/migrations/0007_remove_components_fields.py +22 -0
  60. firefighter/slack/migrations/0008_alter_conversation_incident_categories_and_more.py +33 -0
  61. firefighter/slack/models/conversation.py +3 -3
  62. firefighter/slack/models/incident_channel.py +1 -1
  63. firefighter/slack/models/user.py +1 -1
  64. firefighter/slack/models/user_group.py +3 -3
  65. firefighter/slack/rules.py +1 -1
  66. firefighter/slack/signals/get_users.py +2 -2
  67. firefighter/slack/signals/incident_updated.py +1 -1
  68. firefighter/slack/utils.py +2 -2
  69. firefighter/slack/views/events/home.py +2 -2
  70. firefighter/slack/views/modals/base_modal/form_utils.py +15 -0
  71. firefighter/slack/views/modals/close.py +3 -3
  72. firefighter/slack/views/modals/open.py +25 -1
  73. firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
  74. firefighter/slack/views/modals/opening/details/critical.py +1 -1
  75. firefighter/slack/views/modals/opening/select_impact.py +5 -2
  76. firefighter/slack/views/modals/update_status.py +4 -4
  77. firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
  78. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/METADATA +2 -2
  79. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/RECORD +98 -77
  80. firefighter_tests/conftest.py +4 -5
  81. firefighter_tests/test_api/test_api_landbot.py +1 -1
  82. firefighter_tests/test_firefighter/test_sso.py +146 -0
  83. firefighter_tests/test_incidents/test_forms/test_form_utils.py +15 -15
  84. firefighter_tests/test_incidents/test_incident_urls.py +3 -3
  85. firefighter_tests/test_incidents/test_models/test_incident_category.py +165 -0
  86. firefighter_tests/test_incidents/test_models/test_incident_model.py +2 -2
  87. firefighter_tests/test_raid/test_priority_mapping.py +267 -0
  88. firefighter_tests/test_raid/test_raid_client.py +580 -0
  89. firefighter_tests/test_raid/test_raid_forms.py +795 -0
  90. firefighter_tests/test_raid/test_raid_models.py +185 -0
  91. firefighter_tests/test_raid/test_raid_serializers.py +507 -0
  92. firefighter_tests/test_raid/test_raid_service.py +442 -0
  93. firefighter_tests/test_raid/test_raid_views.py +196 -0
  94. firefighter_tests/test_slack/views/modals/test_close.py +6 -6
  95. firefighter_tests/test_slack/views/modals/test_update_status.py +4 -4
  96. firefighter_fixtures/raid/area.json +0 -1
  97. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/WHEEL +0 -0
  98. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/entry_points.txt +0 -0
  99. {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.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()