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,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")
|