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,185 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+ from django.conf import settings
5
+
6
+ from firefighter.incidents.factories import UserFactory
7
+ from firefighter.incidents.models.impact import (
8
+ Impact,
9
+ ImpactLevel,
10
+ ImpactType,
11
+ LevelChoices,
12
+ )
13
+ from firefighter.jira_app.models import JiraUser
14
+ from firefighter.raid.models import FeatureTeam, JiraTicket, JiraTicketImpact
15
+
16
+
17
+ @pytest.mark.django_db
18
+ class TestJiraTicket:
19
+ def test_get_absolute_url(self):
20
+ # Given
21
+ user = UserFactory()
22
+ jira_user = JiraUser.objects.create(id="jira123", user=user)
23
+
24
+ jira_ticket = JiraTicket.objects.create(
25
+ id=12345,
26
+ key="TEST-123",
27
+ summary="Test ticket",
28
+ description="Test description",
29
+ reporter=jira_user,
30
+ )
31
+
32
+ # When
33
+ result = jira_ticket.get_absolute_url()
34
+
35
+ # Then
36
+ expected_url = f"{settings.RAID_JIRA_API_URL}/browse/TEST-123"
37
+ assert result == expected_url
38
+
39
+ def test_url_property(self):
40
+ # Given
41
+ user = UserFactory()
42
+ jira_user = JiraUser.objects.create(id="jira456", user=user)
43
+
44
+ jira_ticket = JiraTicket.objects.create(
45
+ id=45678,
46
+ key="TEST-456",
47
+ summary="Test ticket 2",
48
+ description="Test description 2",
49
+ reporter=jira_user,
50
+ )
51
+
52
+ # When
53
+ result = jira_ticket.url
54
+
55
+ # Then
56
+ expected_url = f"{settings.RAID_JIRA_API_URL}/browse/TEST-456"
57
+ assert result == expected_url
58
+
59
+ def test_url_property_equals_get_absolute_url(self):
60
+ # Given
61
+ user = UserFactory()
62
+ jira_user = JiraUser.objects.create(id="jira789", user=user)
63
+
64
+ jira_ticket = JiraTicket.objects.create(
65
+ id=78910,
66
+ key="TEST-789",
67
+ summary="Test ticket 3",
68
+ description="Test description 3",
69
+ reporter=jira_user,
70
+ )
71
+
72
+ # When & Then
73
+ assert jira_ticket.url == jira_ticket.get_absolute_url()
74
+
75
+
76
+ @pytest.mark.django_db
77
+ class TestJiraTicketImpact:
78
+ def test_string_representation(self):
79
+ # Given
80
+ user = UserFactory()
81
+ jira_user = JiraUser.objects.create(id="jira100", user=user)
82
+
83
+ jira_ticket = JiraTicket.objects.create(
84
+ id=10011,
85
+ key="TEST-100",
86
+ summary="Test ticket",
87
+ description="Test description",
88
+ reporter=jira_user,
89
+ )
90
+
91
+ # Create required objects for Impact
92
+ impact_type = ImpactType.objects.create(name="Test Impact Type")
93
+ impact_level = ImpactLevel.objects.create(
94
+ name="High",
95
+ impact_type=impact_type,
96
+ value=LevelChoices.HIGH,
97
+ )
98
+ impact = Impact.objects.create(
99
+ impact_type=impact_type,
100
+ impact_level=impact_level,
101
+ details="Test impact details",
102
+ )
103
+
104
+ jira_ticket_impact = JiraTicketImpact.objects.create(
105
+ jira_ticket=jira_ticket,
106
+ impact=impact,
107
+ )
108
+
109
+ # When
110
+ result = str(jira_ticket_impact)
111
+
112
+ # Then
113
+ expected = f"{jira_ticket.key}: {impact}"
114
+ assert result == expected
115
+
116
+
117
+ @pytest.mark.django_db
118
+ class TestFeatureTeam:
119
+ def test_string_representation(self):
120
+ # Given
121
+ feature_team = FeatureTeam.objects.create(
122
+ name="Test Team",
123
+ jira_project_key="TST",
124
+ )
125
+
126
+ # When
127
+ result = str(feature_team)
128
+
129
+ # Then
130
+ assert result == "Test Team"
131
+
132
+ def test_get_team_property(self):
133
+ # Given
134
+ feature_team = FeatureTeam.objects.create(
135
+ name="Backend Team",
136
+ jira_project_key="BACK",
137
+ )
138
+
139
+ # When
140
+ result = feature_team.get_team
141
+
142
+ # Then
143
+ expected = "Backend Team BACK"
144
+ assert result == expected
145
+
146
+ def test_get_key_property(self):
147
+ # Given
148
+ feature_team = FeatureTeam.objects.create(
149
+ name="Frontend Team",
150
+ jira_project_key="FRONT",
151
+ )
152
+
153
+ # When
154
+ result = feature_team.get_key
155
+
156
+ # Then
157
+ assert result == "FRONT"
158
+
159
+ def test_unique_constraint(self):
160
+ # Given - Create first team
161
+ FeatureTeam.objects.create(
162
+ name="Unique Team",
163
+ jira_project_key="UNIQ",
164
+ )
165
+
166
+ # When & Then - Try to create duplicate should raise error
167
+ with pytest.raises((Exception,)):
168
+ FeatureTeam.objects.create(
169
+ name="Unique Team",
170
+ jira_project_key="UNIQ",
171
+ )
172
+
173
+ def test_jira_project_key_unique_constraint(self):
174
+ # Given - Create first team
175
+ FeatureTeam.objects.create(
176
+ name="Team A",
177
+ jira_project_key="SHARED",
178
+ )
179
+
180
+ # When & Then - Try to create another team with same key should raise error
181
+ with pytest.raises((Exception,)):
182
+ FeatureTeam.objects.create(
183
+ name="Team B",
184
+ jira_project_key="SHARED",
185
+ )
@@ -0,0 +1,507 @@
1
+ """Test raid serializers, especially uncovered functionality."""
2
+ from __future__ import annotations
3
+
4
+ from unittest.mock import Mock, patch
5
+
6
+ import pytest
7
+ from django.test import TestCase
8
+ from rest_framework import serializers
9
+
10
+ from firefighter.incidents.factories import UserFactory
11
+ from firefighter.incidents.models.user import User
12
+ from firefighter.jira_app.client import (
13
+ JiraAPIError,
14
+ JiraUserNotFoundError,
15
+ SlackNotificationError,
16
+ )
17
+ from firefighter.jira_app.models import JiraUser
18
+ from firefighter.raid.models import JiraTicket
19
+ from firefighter.raid.serializers import (
20
+ IgnoreEmptyStringListField,
21
+ JiraWebhookCommentSerializer,
22
+ JiraWebhookUpdateSerializer,
23
+ LandbotIssueRequestSerializer,
24
+ get_reporter_user_from_email,
25
+ validate_no_spaces,
26
+ )
27
+ from firefighter.slack.factories import SlackUserFactory
28
+
29
+
30
+ class TestIgnoreEmptyStringListField(TestCase):
31
+ """Test IgnoreEmptyStringListField custom field."""
32
+
33
+ def setUp(self):
34
+ """Set up test field."""
35
+ self.field = IgnoreEmptyStringListField(child=serializers.CharField())
36
+
37
+ def test_valid_list_with_empty_strings(self):
38
+ """Test that empty strings are filtered out."""
39
+ data = ["valid", "", "another", ""]
40
+ result = self.field.to_internal_value(data)
41
+ assert result == ["valid", "another"]
42
+
43
+ def test_valid_list_no_empty_strings(self):
44
+ """Test list without empty strings."""
45
+ data = ["valid", "another"]
46
+ result = self.field.to_internal_value(data)
47
+ assert result == ["valid", "another"]
48
+
49
+ def test_empty_list(self):
50
+ """Test empty list."""
51
+ data = []
52
+ result = self.field.to_internal_value(data)
53
+ assert result == []
54
+
55
+ def test_list_with_only_empty_strings(self):
56
+ """Test list with only empty strings."""
57
+ data = ["", "", ""]
58
+ result = self.field.to_internal_value(data)
59
+ assert result == []
60
+
61
+ def test_invalid_non_list_data(self):
62
+ """Test that non-list data raises ValidationError."""
63
+ with pytest.raises(serializers.ValidationError) as exc_info:
64
+ self.field.to_internal_value("not a list")
65
+ assert 'Expected a list but got type "str"' in str(exc_info.value)
66
+
67
+
68
+ class TestValidateNoSpaces(TestCase):
69
+ """Test validate_no_spaces function."""
70
+
71
+ def test_valid_string_no_spaces(self):
72
+ """Test string without spaces passes validation."""
73
+ # Should not raise any exception
74
+ validate_no_spaces("validstring")
75
+ validate_no_spaces("valid-string")
76
+ validate_no_spaces("valid_string")
77
+
78
+ def test_invalid_string_with_spaces(self):
79
+ """Test string with spaces raises ValidationError."""
80
+ with pytest.raises(serializers.ValidationError) as exc_info:
81
+ validate_no_spaces("invalid string")
82
+ assert "The string cannot contain spaces" in str(exc_info.value)
83
+
84
+ def test_string_with_multiple_spaces(self):
85
+ """Test string with multiple spaces raises ValidationError."""
86
+ with pytest.raises(serializers.ValidationError):
87
+ validate_no_spaces("invalid string with spaces")
88
+
89
+
90
+ class TestGetReporterUserFromEmail(TestCase):
91
+ """Test get_reporter_user_from_email function."""
92
+
93
+ def setUp(self):
94
+ """Set up test data."""
95
+ self.user = UserFactory(email="test@manomano.com")
96
+
97
+ @patch("firefighter.raid.serializers.jira_client")
98
+ def test_existing_user_found(self, mock_jira_client):
99
+ """Test when user and JIRA user are found."""
100
+ # Create JiraUser
101
+ jira_user = JiraUser.objects.create(id="jira-123", user=self.user)
102
+ mock_jira_client.get_jira_user_from_user.return_value = jira_user
103
+
104
+ reporter_user, reporter, user_domain = get_reporter_user_from_email("test@manomano.com")
105
+
106
+ assert reporter_user == self.user
107
+ assert reporter == jira_user
108
+ assert user_domain == "manomano.com"
109
+ mock_jira_client.get_jira_user_from_user.assert_called_once_with(self.user)
110
+
111
+ @patch("firefighter.raid.serializers.jira_client")
112
+ @patch("firefighter.raid.serializers.SlackUser")
113
+ def test_user_not_found_with_slack_fallback(self, mock_slack_user, mock_jira_client):
114
+ """Test when user is not found but Slack user exists."""
115
+ # Setup mocks
116
+ slack_user = SlackUserFactory()
117
+ mock_slack_user.objects.upsert_by_email.return_value = slack_user.user
118
+
119
+ default_jira_user = JiraUser.objects.create(id="default-123", user=UserFactory())
120
+ mock_jira_client.get_jira_user_from_jira_id.return_value = default_jira_user
121
+
122
+ reporter_user, reporter, user_domain = get_reporter_user_from_email("nonexistent@example.com")
123
+
124
+ assert reporter_user == slack_user.user
125
+ assert reporter == default_jira_user
126
+ assert user_domain == "example.com"
127
+
128
+ @patch("firefighter.raid.serializers.jira_client")
129
+ @patch("firefighter.raid.serializers.SlackUser")
130
+ @patch("firefighter.raid.serializers.JIRA_USER_IDS", {"example.com": "domain-specific-123"})
131
+ def test_user_not_found_with_domain_specific_jira_user(self, mock_slack_user, mock_jira_client):
132
+ """Test when user is not found but domain has specific JIRA user."""
133
+ # Setup mocks
134
+ mock_slack_user.objects.upsert_by_email.return_value = None
135
+
136
+ domain_jira_user = JiraUser.objects.create(id="domain-specific-123", user=UserFactory())
137
+ mock_jira_client.get_jira_user_from_jira_id.return_value = domain_jira_user
138
+
139
+ reporter_user, reporter, user_domain = get_reporter_user_from_email("test@example.com")
140
+
141
+ assert reporter_user == domain_jira_user.user
142
+ assert reporter == domain_jira_user
143
+ assert user_domain == "example.com"
144
+ mock_jira_client.get_jira_user_from_jira_id.assert_called_once_with("domain-specific-123")
145
+
146
+ @patch("firefighter.raid.serializers.jira_client")
147
+ def test_jira_user_not_found_exception(self, mock_jira_client):
148
+ """Test when JiraUserNotFoundError is raised."""
149
+ mock_jira_client.get_jira_user_from_user.side_effect = JiraUserNotFoundError("User not found")
150
+
151
+ default_jira_user = JiraUser.objects.create(id="default-123", user=UserFactory())
152
+ mock_jira_client.get_jira_user_from_jira_id.return_value = default_jira_user
153
+
154
+ reporter_user, reporter, user_domain = get_reporter_user_from_email("test@manomano.com")
155
+
156
+ assert reporter_user == self.user
157
+ assert reporter == default_jira_user
158
+ assert user_domain == "manomano.com"
159
+
160
+
161
+ class TestLandbotIssueRequestSerializer(TestCase):
162
+ """Test LandbotIssueRequestSerializer functionality."""
163
+
164
+ def test_validate_environments_with_empty_value(self):
165
+ """Test validate_environments with empty/None value."""
166
+ serializer = LandbotIssueRequestSerializer()
167
+
168
+ # Test with None
169
+ result = serializer.validate_environments(None)
170
+ assert result == ["-"] # Default value
171
+
172
+ # Test with empty list
173
+ result = serializer.validate_environments([])
174
+ assert result == ["-"] # Default value
175
+
176
+ def test_validate_environments_with_valid_value(self):
177
+ """Test validate_environments with valid value."""
178
+ serializer = LandbotIssueRequestSerializer()
179
+
180
+ environments = ["PRD", "STG"]
181
+ result = serializer.validate_environments(environments)
182
+ assert result == environments
183
+
184
+ @patch("firefighter.raid.serializers.alert_slack_new_jira_ticket")
185
+ @patch("firefighter.raid.serializers.get_reporter_user_from_email")
186
+ @patch("firefighter.raid.serializers.jira_client")
187
+ def test_create_with_attachments_error(self, mock_jira_client, mock_get_reporter, mock_alert_slack):
188
+ """Test create method when JIRA returns no issue ID."""
189
+ # Setup mocks
190
+ user = UserFactory()
191
+ jira_user = JiraUser.objects.create(id="test-123", user=user)
192
+ mock_get_reporter.return_value = (user, jira_user, "example.com")
193
+ mock_alert_slack.return_value = None
194
+
195
+ # Mock create_issue to return None ID (error case)
196
+ mock_jira_client.create_issue.return_value = {"id": None, "key": "TEST-123"}
197
+
198
+ serializer = LandbotIssueRequestSerializer()
199
+ validated_data = {
200
+ "reporter_email": "test@example.com",
201
+ "issue_type": "Incident",
202
+ "summary": "Test Issue",
203
+ "description": "Test Description",
204
+ "labels": ["test"],
205
+ "priority": 1,
206
+ "seller_contract_id": "123",
207
+ "zoho": "456",
208
+ "platform": "ES",
209
+ "incident_category": "test",
210
+ "business_impact": "High",
211
+ "environments": ["PRD"],
212
+ "suggested_team_routing": "TEAM1",
213
+ "project": "SBI",
214
+ "attachments": None,
215
+ }
216
+
217
+ with pytest.raises(JiraAPIError):
218
+ serializer.create(validated_data)
219
+
220
+ @patch("firefighter.raid.serializers.alert_slack_new_jira_ticket")
221
+ @patch("firefighter.raid.serializers.get_reporter_user_from_email")
222
+ @patch("firefighter.raid.serializers.jira_client")
223
+ def test_create_with_attachments(self, mock_jira_client, mock_get_reporter, mock_alert_slack):
224
+ """Test create method with attachments."""
225
+ # Setup mocks
226
+ user = UserFactory()
227
+ jira_user = JiraUser.objects.create(id="test-123", user=user)
228
+ mock_get_reporter.return_value = (user, jira_user, "example.com")
229
+ mock_alert_slack.return_value = None
230
+
231
+ mock_jira_client.create_issue.return_value = {
232
+ "id": "12345",
233
+ "key": "TEST-123",
234
+ "summary": "Test Issue",
235
+ "reporter": jira_user,
236
+ }
237
+ mock_jira_client.add_attachments_to_issue = Mock()
238
+
239
+ serializer = LandbotIssueRequestSerializer()
240
+ validated_data = {
241
+ "reporter_email": "test@example.com",
242
+ "issue_type": "Incident",
243
+ "summary": "Test Issue",
244
+ "description": "Test Description",
245
+ "labels": ["test"],
246
+ "priority": 1,
247
+ "seller_contract_id": "123",
248
+ "zoho": "456",
249
+ "platform": "ES",
250
+ "incident_category": "test",
251
+ "business_impact": "High",
252
+ "environments": ["PRD"],
253
+ "suggested_team_routing": "TEAM1",
254
+ "project": "SBI",
255
+ "attachments": "['file1.jpg', 'file2.pdf', '']", # String with empty attachment
256
+ }
257
+
258
+ result = serializer.create(validated_data)
259
+
260
+ # Verify attachments were processed and empty strings filtered
261
+ mock_jira_client.add_attachments_to_issue.assert_called_once_with(
262
+ "12345", ["file1.jpg", "file2.pdf"]
263
+ )
264
+ assert isinstance(result, JiraTicket)
265
+ mock_alert_slack.assert_called_once()
266
+
267
+ @patch("firefighter.raid.serializers.alert_slack_new_jira_ticket")
268
+ @patch("firefighter.raid.serializers.get_reporter_user_from_email")
269
+ @patch("firefighter.raid.serializers.jira_client")
270
+ def test_create_external_user_description(self, mock_jira_client, mock_get_reporter, mock_alert_slack):
271
+ """Test create method adds email to description for external users."""
272
+ # Setup mocks - external domain
273
+ user = UserFactory()
274
+ jira_user = JiraUser.objects.create(id="test-123", user=user)
275
+ mock_get_reporter.return_value = (user, jira_user, "external.com")
276
+ mock_alert_slack.return_value = None
277
+
278
+ mock_jira_client.create_issue.return_value = {
279
+ "id": "12345",
280
+ "key": "TEST-123",
281
+ "summary": "Test Issue",
282
+ "reporter": jira_user,
283
+ }
284
+
285
+ serializer = LandbotIssueRequestSerializer()
286
+ validated_data = {
287
+ "reporter_email": "test@external.com",
288
+ "issue_type": "Incident",
289
+ "summary": "Test Issue",
290
+ "description": "Test Description",
291
+ "labels": [],
292
+ "priority": 1,
293
+ "seller_contract_id": None,
294
+ "zoho": None,
295
+ "platform": "ES",
296
+ "incident_category": None,
297
+ "business_impact": None,
298
+ "environments": ["PRD"],
299
+ "suggested_team_routing": "TEAM1",
300
+ "project": "SBI",
301
+ "attachments": None,
302
+ }
303
+
304
+ result = serializer.create(validated_data)
305
+
306
+ # Verify description includes reporter email for external users
307
+ create_call = mock_jira_client.create_issue.call_args[1]
308
+ assert "Reporter email test@external.com" in create_call["description"]
309
+ assert isinstance(result, JiraTicket)
310
+ mock_alert_slack.assert_called_once_with(
311
+ result, reporter_user=user, reporter_email="test@external.com"
312
+ )
313
+
314
+
315
+ class TestJiraWebhookUpdateSerializer(TestCase):
316
+ """Test JiraWebhookUpdateSerializer functionality."""
317
+
318
+ @patch("firefighter.raid.serializers.alert_slack_update_ticket")
319
+ def test_create_with_tracked_field(self, mock_alert):
320
+ """Test create method with a tracked field change."""
321
+ mock_alert.return_value = True
322
+
323
+ serializer = JiraWebhookUpdateSerializer()
324
+ validated_data = {
325
+ "issue": {"id": "12345", "key": "TEST-123"},
326
+ "changelog": {
327
+ "items": [
328
+ {
329
+ "field": "Priority",
330
+ "fromString": "High",
331
+ "toString": "Critical"
332
+ }
333
+ ]
334
+ },
335
+ "user": {"displayName": "John Doe"},
336
+ "webhookEvent": "jira:issue_updated"
337
+ }
338
+
339
+ result = serializer.create(validated_data)
340
+ assert result is True
341
+ mock_alert.assert_called_once()
342
+
343
+ @patch("firefighter.raid.serializers.alert_slack_update_ticket")
344
+ def test_create_with_untracked_field(self, mock_alert):
345
+ """Test create method with an untracked field change."""
346
+ serializer = JiraWebhookUpdateSerializer()
347
+ validated_data = {
348
+ "issue": {"id": "12345", "key": "TEST-123"},
349
+ "changelog": {
350
+ "items": [
351
+ {
352
+ "field": "labels", # Not tracked
353
+ "fromString": "old",
354
+ "toString": "new"
355
+ }
356
+ ]
357
+ },
358
+ "user": {"displayName": "John Doe"},
359
+ "webhookEvent": "jira:issue_updated"
360
+ }
361
+
362
+ result = serializer.create(validated_data)
363
+ assert result is True
364
+ mock_alert.assert_not_called()
365
+
366
+ @patch("firefighter.raid.serializers.alert_slack_update_ticket")
367
+ def test_create_slack_notification_error(self, mock_alert):
368
+ """Test create method when Slack notification fails."""
369
+ mock_alert.return_value = False
370
+
371
+ serializer = JiraWebhookUpdateSerializer()
372
+ validated_data = {
373
+ "issue": {"id": "12345", "key": "TEST-123"},
374
+ "changelog": {
375
+ "items": [
376
+ {
377
+ "field": "status",
378
+ "fromString": "Open",
379
+ "toString": "Closed"
380
+ }
381
+ ]
382
+ },
383
+ "user": {"displayName": "John Doe"},
384
+ "webhookEvent": "jira:issue_updated"
385
+ }
386
+
387
+ with pytest.raises(SlackNotificationError):
388
+ serializer.create(validated_data)
389
+
390
+ def test_update_not_implemented(self):
391
+ """Test that update method raises NotImplementedError."""
392
+ serializer = JiraWebhookUpdateSerializer()
393
+ with pytest.raises(NotImplementedError):
394
+ serializer.update(None, {})
395
+
396
+
397
+ class TestJiraWebhookCommentSerializer(TestCase):
398
+ """Test JiraWebhookCommentSerializer functionality."""
399
+
400
+ @patch("firefighter.raid.serializers.alert_slack_comment_ticket")
401
+ def test_create_successful(self, mock_alert):
402
+ """Test create method successful case."""
403
+ mock_alert.return_value = True
404
+
405
+ serializer = JiraWebhookCommentSerializer()
406
+ validated_data = {
407
+ "issue": {"id": "12345", "key": "TEST-123"},
408
+ "comment": {
409
+ "author": {"displayName": "John Doe"},
410
+ "body": "This is a test comment"
411
+ },
412
+ "webhookEvent": "comment_created"
413
+ }
414
+
415
+ result = serializer.create(validated_data)
416
+ assert result is True
417
+ mock_alert.assert_called_once_with(
418
+ webhook_event="comment_created",
419
+ jira_ticket_id="12345",
420
+ jira_ticket_key="TEST-123",
421
+ author_jira_name="John Doe",
422
+ comment="This is a test comment"
423
+ )
424
+
425
+ @patch("firefighter.raid.serializers.alert_slack_comment_ticket")
426
+ def test_create_slack_notification_error(self, mock_alert):
427
+ """Test create method when Slack notification fails."""
428
+ mock_alert.return_value = False
429
+
430
+ serializer = JiraWebhookCommentSerializer()
431
+ validated_data = {
432
+ "issue": {"id": "12345", "key": "TEST-123"},
433
+ "comment": {
434
+ "author": {"displayName": "John Doe"},
435
+ "body": "This is a test comment"
436
+ },
437
+ "webhookEvent": "comment_updated"
438
+ }
439
+
440
+ with pytest.raises(SlackNotificationError):
441
+ serializer.create(validated_data)
442
+
443
+ def test_update_not_implemented(self):
444
+ """Test that update method raises NotImplementedError."""
445
+ serializer = JiraWebhookCommentSerializer()
446
+ with pytest.raises(NotImplementedError):
447
+ serializer.update(None, {})
448
+
449
+
450
+ @pytest.mark.django_db
451
+ class TestGetReporterUserFromEmailAdditional:
452
+ """Additional tests for get_reporter_user_from_email to reach 100% coverage."""
453
+
454
+ @patch("firefighter.raid.serializers.jira_client")
455
+ @patch("firefighter.raid.serializers.SlackUser")
456
+ def test_user_does_not_exist_no_slack_fallback(self, mock_slack_user, mock_jira_client):
457
+ """Test when User.DoesNotExist and no Slack user exists."""
458
+ # Setup mocks
459
+ mock_slack_user.objects.upsert_by_email.return_value = None
460
+
461
+ default_jira_user = JiraUser.objects.create(id="default-123", user=UserFactory())
462
+ mock_jira_client.get_jira_user_from_jira_id.return_value = default_jira_user
463
+
464
+ reporter_user, reporter, user_domain = get_reporter_user_from_email("test@example.com")
465
+
466
+ assert reporter_user == default_jira_user.user
467
+ assert reporter == default_jira_user
468
+ assert user_domain == "example.com"
469
+
470
+ @patch("firefighter.raid.serializers.jira_client")
471
+ @patch("firefighter.raid.serializers.SlackUser")
472
+ def test_slack_user_exists_but_reporter_user_tmp_is_none(self, mock_slack_user, mock_jira_client):
473
+ """Test when Slack upsert returns None and we use default JIRA user."""
474
+ # Setup mocks - simulate User.DoesNotExist
475
+ mock_slack_user.objects.upsert_by_email.return_value = None
476
+
477
+ default_jira_user = JiraUser.objects.create(id="default-123", user=UserFactory())
478
+ mock_jira_client.get_jira_user_from_jira_id.return_value = default_jira_user
479
+
480
+ with patch("firefighter.raid.serializers.User.objects.get", side_effect=User.DoesNotExist):
481
+ reporter_user, reporter, user_domain = get_reporter_user_from_email("test@example.com")
482
+
483
+ # Should use default JIRA user's user since reporter_user_tmp is None
484
+ assert reporter_user == default_jira_user.user
485
+ assert reporter == default_jira_user
486
+ assert user_domain == "example.com"
487
+
488
+ @patch("firefighter.raid.serializers.jira_client")
489
+ @patch("firefighter.raid.serializers.SlackUser")
490
+ @patch("firefighter.raid.serializers.JIRA_USER_IDS", {"special.com": "special-user-123"})
491
+ def test_domain_specific_jira_user_with_slack_fallback(self, mock_slack_user, mock_jira_client):
492
+ """Test domain-specific JIRA user when Slack user exists."""
493
+ # Setup mocks
494
+ slack_user = UserFactory(email="test@special.com")
495
+ mock_slack_user.objects.upsert_by_email.return_value = slack_user
496
+
497
+ domain_jira_user = JiraUser.objects.create(id="special-user-123", user=UserFactory())
498
+ mock_jira_client.get_jira_user_from_jira_id.return_value = domain_jira_user
499
+
500
+ with patch("firefighter.raid.serializers.User.objects.get", side_effect=User.DoesNotExist):
501
+ reporter_user, reporter, user_domain = get_reporter_user_from_email("test@special.com")
502
+
503
+ # Should use slack_user since reporter_user_tmp is not None
504
+ assert reporter_user == slack_user
505
+ assert reporter == domain_jira_user
506
+ assert user_domain == "special.com"
507
+ mock_jira_client.get_jira_user_from_jira_id.assert_called_once_with("special-user-123")