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.
- firefighter/_version.py +16 -3
- firefighter/api/serializers.py +8 -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/firefighter/settings/components/raid.py +3 -0
- firefighter/incidents/admin.py +24 -24
- firefighter/incidents/factories.py +14 -5
- firefighter/incidents/forms/close_incident.py +4 -4
- firefighter/incidents/forms/create_incident.py +4 -4
- firefighter/incidents/forms/update_status.py +4 -4
- 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/models/__init__.py +1 -1
- firefighter/incidents/models/group.py +1 -1
- firefighter/incidents/models/incident.py +15 -15
- firefighter/incidents/models/{component.py → incident_category.py} +30 -29
- firefighter/incidents/models/incident_update.py +3 -3
- 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/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 +2 -2
- 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/client.py +3 -3
- firefighter/raid/forms.py +53 -19
- 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 +4 -2
- firefighter/raid/utils.py +1 -1
- firefighter/raid/views/__init__.py +1 -1
- firefighter/raid/views/open_normal.py +2 -2
- firefighter/slack/admin.py +8 -8
- firefighter/slack/management/commands/switch_test_users.py +272 -0
- firefighter/slack/messages/slack_messages.py +5 -5
- 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 +1 -1
- firefighter/slack/signals/get_users.py +2 -2
- firefighter/slack/signals/incident_updated.py +1 -1
- firefighter/slack/utils.py +2 -2
- firefighter/slack/views/events/home.py +2 -2
- firefighter/slack/views/modals/base_modal/form_utils.py +15 -0
- firefighter/slack/views/modals/close.py +3 -3
- firefighter/slack/views/modals/open.py +25 -1
- firefighter/slack/views/modals/opening/check_current_incidents.py +2 -2
- firefighter/slack/views/modals/opening/details/critical.py +1 -1
- firefighter/slack/views/modals/opening/select_impact.py +5 -2
- firefighter/slack/views/modals/update_status.py +4 -4
- firefighter_fixtures/incidents/{components.json → incident_categories.json} +52 -52
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/METADATA +2 -2
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/RECORD +98 -77
- 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_forms/test_form_utils.py +15 -15
- 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 +2 -2
- 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 +795 -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_views.py +196 -0
- firefighter_tests/test_slack/views/modals/test_close.py +6 -6
- firefighter_tests/test_slack/views/modals/test_update_status.py +4 -4
- firefighter_fixtures/raid/area.json +0 -1
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.13.dist-info → firefighter_incident-0.0.14.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,8 +6,8 @@ from django.forms import Form
|
|
|
6
6
|
|
|
7
7
|
from firefighter.incidents.enums import IncidentStatus
|
|
8
8
|
from firefighter.incidents.forms.utils import EnumChoiceField, GroupedModelChoiceField
|
|
9
|
-
from firefighter.incidents.models.component import Component
|
|
10
9
|
from firefighter.incidents.models.group import Group
|
|
10
|
+
from firefighter.incidents.models.incident_category import IncidentCategory
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class EnumChoiceFieldForm(Form):
|
|
@@ -15,8 +15,8 @@ class EnumChoiceFieldForm(Form):
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class GroupedModelChoiceFieldForm(Form):
|
|
18
|
-
|
|
19
|
-
choices_groupby="group", queryset=
|
|
18
|
+
incident_category = GroupedModelChoiceField(
|
|
19
|
+
choices_groupby="group", queryset=IncidentCategory.objects.all()
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
|
|
@@ -26,10 +26,10 @@ def group() -> Group:
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
@pytest.fixture
|
|
29
|
-
def
|
|
29
|
+
def incident_categories(group: Group):
|
|
30
30
|
return [
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
IncidentCategory.objects.create(name="Issue category 1", group=group, order=1),
|
|
32
|
+
IncidentCategory.objects.create(name="Issue category 2", group=group, order=2),
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
|
|
@@ -47,27 +47,27 @@ def test_enum_choice_field_invalid() -> None:
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
@pytest.mark.django_db
|
|
50
|
-
def test_grouped_model_choice_field_valid(
|
|
51
|
-
form = GroupedModelChoiceFieldForm({"
|
|
50
|
+
def test_grouped_model_choice_field_valid(incident_categories: list[IncidentCategory]):
|
|
51
|
+
form = GroupedModelChoiceFieldForm({"incident_category": incident_categories[0].id})
|
|
52
52
|
assert form.is_valid()
|
|
53
|
-
assert form.cleaned_data["
|
|
53
|
+
assert form.cleaned_data["incident_category"] == incident_categories[0]
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def test_grouped_model_choice_field_invalid() -> None:
|
|
57
|
-
form = GroupedModelChoiceFieldForm({"
|
|
57
|
+
form = GroupedModelChoiceFieldForm({"incident_category": "non-existent-id"})
|
|
58
58
|
assert not form.is_valid()
|
|
59
|
-
assert "
|
|
59
|
+
assert "incident_category" in form.errors
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
@pytest.fixture(scope="module")
|
|
63
63
|
def test_grouped_model_choice_field_grouping(
|
|
64
|
-
|
|
64
|
+
incident_categories: list[IncidentCategory],
|
|
65
65
|
):
|
|
66
66
|
form = GroupedModelChoiceFieldForm()
|
|
67
67
|
grouped_choices = list(
|
|
68
|
-
form.fields["
|
|
68
|
+
form.fields["incident_category"].iterator(field=form.fields["incident_category"])
|
|
69
69
|
)
|
|
70
70
|
assert len(grouped_choices) == 2 # One for the empty choice, and one for the group
|
|
71
|
-
assert grouped_choices[0] == ("", form.fields["
|
|
72
|
-
assert grouped_choices[1][0] ==
|
|
71
|
+
assert grouped_choices[0] == ("", form.fields["incident_category"].empty_label)
|
|
72
|
+
assert grouped_choices[1][0] == incident_categories[0].group
|
|
73
73
|
assert len(grouped_choices[1][1]) == 2 # Two components in the group
|
|
@@ -100,9 +100,9 @@ def test_incident_create_unauthorized(client: Client) -> None:
|
|
|
100
100
|
|
|
101
101
|
@pytest.mark.django_db
|
|
102
102
|
@pytest.mark.usefixtures("_debug")
|
|
103
|
-
def
|
|
104
|
-
"""This test ensures that the
|
|
105
|
-
response = client.get(reverse("incidents:
|
|
103
|
+
def test_incident_category_list(client: Client) -> None:
|
|
104
|
+
"""This test ensures that the incident category list is accessible."""
|
|
105
|
+
response = client.get(reverse("incidents:incident-category-list"))
|
|
106
106
|
|
|
107
107
|
assert response.status_code == 302
|
|
108
108
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from django.utils import timezone
|
|
7
|
+
|
|
8
|
+
from firefighter.incidents.factories import GroupFactory, IncidentCategoryFactory
|
|
9
|
+
from firefighter.incidents.models.incident_category import (
|
|
10
|
+
IncidentCategory,
|
|
11
|
+
IncidentCategoryFilterSet,
|
|
12
|
+
IncidentCategoryManager,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.django_db
|
|
17
|
+
class TestIncidentCategoryManager:
|
|
18
|
+
def test_queryset_with_mtbf_date_to_in_future(self):
|
|
19
|
+
"""Test that date_to is clamped to now() when it's in the future."""
|
|
20
|
+
# Given
|
|
21
|
+
IncidentCategoryFactory()
|
|
22
|
+
manager = IncidentCategoryManager()
|
|
23
|
+
manager.model = IncidentCategory
|
|
24
|
+
|
|
25
|
+
# Create dates where date_to is in the future
|
|
26
|
+
now = timezone.now()
|
|
27
|
+
date_from = now - timedelta(days=10)
|
|
28
|
+
date_to = now + timedelta(days=5) # Future date
|
|
29
|
+
|
|
30
|
+
# When
|
|
31
|
+
result = manager.queryset_with_mtbf(date_from, date_to)
|
|
32
|
+
|
|
33
|
+
# Then
|
|
34
|
+
assert result is not None
|
|
35
|
+
assert list(result) == list(result) # Should not fail to execute
|
|
36
|
+
|
|
37
|
+
def test_queryset_with_mtbf_with_custom_queryset(self):
|
|
38
|
+
"""Test that custom queryset parameter is used."""
|
|
39
|
+
# Given
|
|
40
|
+
category1 = IncidentCategoryFactory()
|
|
41
|
+
category2 = IncidentCategoryFactory()
|
|
42
|
+
|
|
43
|
+
manager = IncidentCategoryManager()
|
|
44
|
+
manager.model = IncidentCategory
|
|
45
|
+
|
|
46
|
+
# Create a custom queryset that filters only category1
|
|
47
|
+
custom_queryset = IncidentCategory.objects.filter(id=category1.id)
|
|
48
|
+
|
|
49
|
+
date_from = timezone.now() - timedelta(days=10)
|
|
50
|
+
date_to = timezone.now() - timedelta(days=1)
|
|
51
|
+
|
|
52
|
+
# When
|
|
53
|
+
result = manager.queryset_with_mtbf(date_from, date_to, queryset=custom_queryset)
|
|
54
|
+
|
|
55
|
+
# Then
|
|
56
|
+
assert category1 in result
|
|
57
|
+
assert category2 not in result
|
|
58
|
+
|
|
59
|
+
def test_search_with_none_queryset(self):
|
|
60
|
+
"""Test search method when queryset is None."""
|
|
61
|
+
# Given
|
|
62
|
+
category = IncidentCategoryFactory(name="Test Category")
|
|
63
|
+
|
|
64
|
+
# When
|
|
65
|
+
result, is_empty = IncidentCategoryManager.search(None, "Test")
|
|
66
|
+
|
|
67
|
+
# Then
|
|
68
|
+
assert is_empty is False
|
|
69
|
+
assert category in result
|
|
70
|
+
|
|
71
|
+
def test_search_with_empty_search_term(self):
|
|
72
|
+
"""Test search method with empty/None search term."""
|
|
73
|
+
# Given
|
|
74
|
+
IncidentCategoryFactory(name="Category One")
|
|
75
|
+
IncidentCategoryFactory(name="Category Two")
|
|
76
|
+
queryset = IncidentCategory.objects.all()
|
|
77
|
+
|
|
78
|
+
# When - Test with None
|
|
79
|
+
result_none, is_empty_none = IncidentCategoryManager.search(queryset, None)
|
|
80
|
+
|
|
81
|
+
# When - Test with empty string
|
|
82
|
+
result_empty, is_empty_empty = IncidentCategoryManager.search(queryset, "")
|
|
83
|
+
|
|
84
|
+
# When - Test with whitespace
|
|
85
|
+
result_spaces, is_empty_spaces = IncidentCategoryManager.search(queryset, " ")
|
|
86
|
+
|
|
87
|
+
# Then - All should return the original queryset
|
|
88
|
+
assert is_empty_none is False
|
|
89
|
+
assert is_empty_empty is False
|
|
90
|
+
assert is_empty_spaces is False
|
|
91
|
+
|
|
92
|
+
original_ids = set(queryset.values_list("id", flat=True))
|
|
93
|
+
assert set(result_none.values_list("id", flat=True)) == original_ids
|
|
94
|
+
assert set(result_empty.values_list("id", flat=True)) == original_ids
|
|
95
|
+
assert set(result_spaces.values_list("id", flat=True)) == original_ids
|
|
96
|
+
|
|
97
|
+
def test_search_with_valid_search_term(self):
|
|
98
|
+
"""Test search method with valid search term."""
|
|
99
|
+
# Given
|
|
100
|
+
group = GroupFactory(name="Test Group", description="Group for testing")
|
|
101
|
+
category = IncidentCategoryFactory(
|
|
102
|
+
name="Infrastructure Issue",
|
|
103
|
+
description="Issues with infrastructure",
|
|
104
|
+
group=group
|
|
105
|
+
)
|
|
106
|
+
other_category = IncidentCategoryFactory(
|
|
107
|
+
name="Database Problem",
|
|
108
|
+
description="Database related issues"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# When - Search for "infrastructure"
|
|
112
|
+
result, is_empty = IncidentCategoryManager.search(None, "infrastructure")
|
|
113
|
+
|
|
114
|
+
# Then
|
|
115
|
+
assert is_empty is False
|
|
116
|
+
result_list = list(result)
|
|
117
|
+
assert category in result_list
|
|
118
|
+
# Note: other_category might also appear if search is broad
|
|
119
|
+
|
|
120
|
+
# When - Search for "database"
|
|
121
|
+
result_db, is_empty_db = IncidentCategoryManager.search(None, "database")
|
|
122
|
+
|
|
123
|
+
# Then
|
|
124
|
+
assert is_empty_db is False
|
|
125
|
+
result_db_list = list(result_db)
|
|
126
|
+
assert other_category in result_db_list
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@pytest.mark.django_db
|
|
130
|
+
class TestIncidentCategoryFilterSet:
|
|
131
|
+
def test_incident_category_search(self):
|
|
132
|
+
"""Test the incident_category_search filter method."""
|
|
133
|
+
# Given
|
|
134
|
+
category = IncidentCategoryFactory(name="Network Issues")
|
|
135
|
+
queryset = IncidentCategory.objects.all()
|
|
136
|
+
|
|
137
|
+
# When
|
|
138
|
+
result = IncidentCategoryFilterSet.incident_category_search(queryset, "search", "network")
|
|
139
|
+
|
|
140
|
+
# Then
|
|
141
|
+
assert category in result
|
|
142
|
+
|
|
143
|
+
def test_metrics_period_filter(self):
|
|
144
|
+
"""Test the metrics period filter method."""
|
|
145
|
+
# Given
|
|
146
|
+
category = IncidentCategoryFactory()
|
|
147
|
+
queryset = IncidentCategory.objects.all()
|
|
148
|
+
|
|
149
|
+
# Create date range
|
|
150
|
+
now = timezone.now()
|
|
151
|
+
date_from = now - timedelta(days=30)
|
|
152
|
+
date_to = now
|
|
153
|
+
value = (date_from, date_to, None, None)
|
|
154
|
+
|
|
155
|
+
# When
|
|
156
|
+
result = IncidentCategoryFilterSet.metrics_period_filter(queryset, "metrics", value)
|
|
157
|
+
|
|
158
|
+
# Then
|
|
159
|
+
# Should return a queryset with mtbf annotation
|
|
160
|
+
assert result is not None
|
|
161
|
+
assert category in result
|
|
162
|
+
|
|
163
|
+
# Check that the mtbf annotation is present (will be None if no metrics)
|
|
164
|
+
category_with_mtbf = result.get(id=category.id)
|
|
165
|
+
assert hasattr(category_with_mtbf, "mtbf")
|
|
@@ -20,8 +20,8 @@ class TestIncident(django.TestCase):
|
|
|
20
20
|
@given(builds(IncidentFactory.build))
|
|
21
21
|
def test_model_properties(self, instance: Incident) -> None:
|
|
22
22
|
"""Tests that instance can be saved and has correct representation."""
|
|
23
|
-
instance.
|
|
24
|
-
instance.
|
|
23
|
+
instance.incident_category.group.save()
|
|
24
|
+
instance.incident_category.save()
|
|
25
25
|
instance.environment.save()
|
|
26
26
|
instance.priority.save()
|
|
27
27
|
instance.created_by.save()
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Test priority mapping from Impact to JIRA for all priority values including P5."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from django.test import TestCase
|
|
8
|
+
|
|
9
|
+
from firefighter.incidents.factories import (
|
|
10
|
+
IncidentFactory,
|
|
11
|
+
UserFactory,
|
|
12
|
+
)
|
|
13
|
+
from firefighter.incidents.models.priority import Priority
|
|
14
|
+
from firefighter.jira_app.client import JiraAPIError, JiraUserNotFoundError
|
|
15
|
+
from firefighter.jira_app.models import JiraUser
|
|
16
|
+
from firefighter.raid.signals.incident_created import create_ticket
|
|
17
|
+
from firefighter.slack.factories import IncidentChannelFactory
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestPriorityMapping(TestCase):
|
|
21
|
+
"""Test priority mapping from Impact to JIRA including P5 support."""
|
|
22
|
+
|
|
23
|
+
def setUp(self):
|
|
24
|
+
"""Set up test data."""
|
|
25
|
+
self.user = UserFactory()
|
|
26
|
+
|
|
27
|
+
# Create or get priorities P1-P5 (use get_or_create to avoid duplicates)
|
|
28
|
+
self.priority_p1, _ = Priority.objects.get_or_create(
|
|
29
|
+
value=1, defaults={"name": "Critical", "order": 1}
|
|
30
|
+
)
|
|
31
|
+
self.priority_p2, _ = Priority.objects.get_or_create(
|
|
32
|
+
value=2, defaults={"name": "High", "order": 2}
|
|
33
|
+
)
|
|
34
|
+
self.priority_p3, _ = Priority.objects.get_or_create(
|
|
35
|
+
value=3, defaults={"name": "Medium", "order": 3}
|
|
36
|
+
)
|
|
37
|
+
self.priority_p4, _ = Priority.objects.get_or_create(
|
|
38
|
+
value=4, defaults={"name": "Low", "order": 4}
|
|
39
|
+
)
|
|
40
|
+
self.priority_p5, _ = Priority.objects.get_or_create(
|
|
41
|
+
value=5, defaults={"name": "Lowest", "order": 5}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@patch("firefighter.raid.signals.incident_created.client")
|
|
45
|
+
@patch("firefighter.raid.signals.incident_created.get_jira_user_from_user")
|
|
46
|
+
def test_priority_p1_mapping(self, mock_get_jira_user, mock_client):
|
|
47
|
+
"""Test P1 priority mapping to JIRA."""
|
|
48
|
+
self._test_priority_mapping(self.priority_p1, 1, mock_get_jira_user, mock_client)
|
|
49
|
+
|
|
50
|
+
@patch("firefighter.raid.signals.incident_created.client")
|
|
51
|
+
@patch("firefighter.raid.signals.incident_created.get_jira_user_from_user")
|
|
52
|
+
def test_priority_p2_mapping(self, mock_get_jira_user, mock_client):
|
|
53
|
+
"""Test P2 priority mapping to JIRA."""
|
|
54
|
+
self._test_priority_mapping(self.priority_p2, 2, mock_get_jira_user, mock_client)
|
|
55
|
+
|
|
56
|
+
@patch("firefighter.raid.signals.incident_created.client")
|
|
57
|
+
@patch("firefighter.raid.signals.incident_created.get_jira_user_from_user")
|
|
58
|
+
def test_priority_p3_mapping(self, mock_get_jira_user, mock_client):
|
|
59
|
+
"""Test P3 priority mapping to JIRA."""
|
|
60
|
+
self._test_priority_mapping(self.priority_p3, 3, mock_get_jira_user, mock_client)
|
|
61
|
+
|
|
62
|
+
@patch("firefighter.raid.signals.incident_created.client")
|
|
63
|
+
@patch("firefighter.raid.signals.incident_created.get_jira_user_from_user")
|
|
64
|
+
def test_priority_p4_mapping(self, mock_get_jira_user, mock_client):
|
|
65
|
+
"""Test P4 priority mapping to JIRA."""
|
|
66
|
+
self._test_priority_mapping(self.priority_p4, 4, mock_get_jira_user, mock_client)
|
|
67
|
+
|
|
68
|
+
@patch("firefighter.raid.signals.incident_created.client")
|
|
69
|
+
@patch("firefighter.raid.signals.incident_created.get_jira_user_from_user")
|
|
70
|
+
def test_priority_p5_mapping(self, mock_get_jira_user, mock_client):
|
|
71
|
+
"""Test P5 priority mapping to JIRA - this should now work with our fix."""
|
|
72
|
+
self._test_priority_mapping(self.priority_p5, 5, mock_get_jira_user, mock_client)
|
|
73
|
+
|
|
74
|
+
@patch("firefighter.raid.signals.incident_created.client")
|
|
75
|
+
@patch("firefighter.raid.signals.incident_created.get_jira_user_from_user")
|
|
76
|
+
def test_priority_invalid_value_fallback(self, mock_get_jira_user, mock_client):
|
|
77
|
+
"""Test that invalid priority values fall back to P1."""
|
|
78
|
+
# Create a priority with an invalid value (>5)
|
|
79
|
+
invalid_priority, _ = Priority.objects.get_or_create(
|
|
80
|
+
value=6, defaults={"name": "Invalid", "order": 6}
|
|
81
|
+
)
|
|
82
|
+
self._test_priority_mapping(invalid_priority, 1, mock_get_jira_user, mock_client) # Should fallback to 1
|
|
83
|
+
|
|
84
|
+
def _test_priority_mapping(self, priority: Priority, expected_jira_priority: int, mock_get_jira_user, mock_client):
|
|
85
|
+
"""Helper method to test priority mapping."""
|
|
86
|
+
# Create a real JiraUser for testing
|
|
87
|
+
jira_user = JiraUser.objects.create(id="test-user-id", user=self.user)
|
|
88
|
+
mock_get_jira_user.return_value = jira_user
|
|
89
|
+
|
|
90
|
+
# Mock the create_issue return value (exclude watchers as it's a ManyToMany field)
|
|
91
|
+
mock_client.create_issue.return_value = {
|
|
92
|
+
"id": "123456",
|
|
93
|
+
"key": "TEST-123",
|
|
94
|
+
"assignee": None,
|
|
95
|
+
"reporter": jira_user, # Return the actual JiraUser object
|
|
96
|
+
"issue_type": "Incident",
|
|
97
|
+
"project_key": "INCIDENT",
|
|
98
|
+
"description": "Test incident",
|
|
99
|
+
"summary": "Test Incident"
|
|
100
|
+
}
|
|
101
|
+
mock_client.get_jira_user_from_jira_id.return_value = jira_user
|
|
102
|
+
mock_client.jira.add_watcher = Mock()
|
|
103
|
+
mock_client.jira.remove_watcher = Mock()
|
|
104
|
+
mock_client.jira.add_simple_link = Mock()
|
|
105
|
+
|
|
106
|
+
# Create incident with the specific priority
|
|
107
|
+
incident = IncidentFactory(
|
|
108
|
+
priority=priority,
|
|
109
|
+
created_by=self.user,
|
|
110
|
+
title="Test Incident",
|
|
111
|
+
description="Test incident description"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Create incident channel
|
|
115
|
+
channel = IncidentChannelFactory(incident=incident)
|
|
116
|
+
channel.add_bookmark = Mock()
|
|
117
|
+
channel.send_message_and_save = Mock()
|
|
118
|
+
|
|
119
|
+
# Call the create_ticket function
|
|
120
|
+
create_ticket(sender=None, incident=incident, channel=channel)
|
|
121
|
+
|
|
122
|
+
# Verify that create_issue was called with the expected priority
|
|
123
|
+
mock_client.create_issue.assert_called_once()
|
|
124
|
+
call_kwargs = mock_client.create_issue.call_args[1]
|
|
125
|
+
|
|
126
|
+
assert call_kwargs["priority"] == expected_jira_priority
|
|
127
|
+
assert call_kwargs["issuetype"] == "Incident"
|
|
128
|
+
assert call_kwargs["summary"] == "Test Incident"
|
|
129
|
+
|
|
130
|
+
@patch("firefighter.raid.signals.incident_created.client")
|
|
131
|
+
@patch("firefighter.raid.signals.incident_created.get_jira_user_from_user")
|
|
132
|
+
def test_create_ticket_no_issue_id_error(self, mock_get_jira_user, mock_client):
|
|
133
|
+
"""Test error handling when create_issue returns no ID."""
|
|
134
|
+
test_user = UserFactory()
|
|
135
|
+
jira_user = JiraUser.objects.create(id="test-user-no-id", user=test_user)
|
|
136
|
+
mock_get_jira_user.return_value = jira_user
|
|
137
|
+
|
|
138
|
+
# Mock create_issue to return None ID (error case)
|
|
139
|
+
mock_client.create_issue.return_value = {
|
|
140
|
+
"id": None, # This should trigger the error
|
|
141
|
+
"key": "TEST-123",
|
|
142
|
+
"summary": "Test Incident"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
incident = IncidentFactory(
|
|
146
|
+
priority=self.priority_p1,
|
|
147
|
+
created_by=test_user,
|
|
148
|
+
title="Test Incident"
|
|
149
|
+
)
|
|
150
|
+
channel = IncidentChannelFactory(incident=incident)
|
|
151
|
+
|
|
152
|
+
# Should raise JiraAPIError
|
|
153
|
+
with pytest.raises(JiraAPIError):
|
|
154
|
+
create_ticket(sender=None, incident=incident, channel=channel)
|
|
155
|
+
|
|
156
|
+
@patch("firefighter.raid.signals.incident_created.client")
|
|
157
|
+
@patch("firefighter.raid.signals.incident_created.get_jira_user_from_user")
|
|
158
|
+
def test_create_ticket_jira_user_not_found_error(self, mock_get_jira_user, mock_client):
|
|
159
|
+
"""Test error handling when default JIRA user is not found."""
|
|
160
|
+
test_user = UserFactory()
|
|
161
|
+
jira_user = JiraUser.objects.create(id="test-user-not-found", user=test_user)
|
|
162
|
+
mock_get_jira_user.return_value = jira_user
|
|
163
|
+
|
|
164
|
+
mock_client.create_issue.return_value = {
|
|
165
|
+
"id": "123456",
|
|
166
|
+
"key": "TEST-123",
|
|
167
|
+
"reporter": jira_user,
|
|
168
|
+
"summary": "Test Incident"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Mock get_jira_user_from_jira_id to raise JiraUserNotFoundError
|
|
172
|
+
mock_client.get_jira_user_from_jira_id.side_effect = JiraUserNotFoundError("User not found")
|
|
173
|
+
mock_client.jira.add_watcher = Mock()
|
|
174
|
+
mock_client.jira.remove_watcher = Mock()
|
|
175
|
+
mock_client.jira.add_simple_link = Mock()
|
|
176
|
+
|
|
177
|
+
incident = IncidentFactory(
|
|
178
|
+
priority=self.priority_p1,
|
|
179
|
+
created_by=test_user,
|
|
180
|
+
title="Test Incident"
|
|
181
|
+
)
|
|
182
|
+
channel = IncidentChannelFactory(incident=incident)
|
|
183
|
+
channel.add_bookmark = Mock()
|
|
184
|
+
channel.send_message_and_save = Mock()
|
|
185
|
+
|
|
186
|
+
# Should complete without error despite the exception
|
|
187
|
+
create_ticket(sender=None, incident=incident, channel=channel)
|
|
188
|
+
|
|
189
|
+
# Verify the ticket was still created
|
|
190
|
+
mock_client.create_issue.assert_called_once()
|
|
191
|
+
|
|
192
|
+
@patch("firefighter.raid.signals.incident_created.client")
|
|
193
|
+
@patch("firefighter.raid.signals.incident_created.get_jira_user_from_user")
|
|
194
|
+
def test_create_ticket_add_watcher_error(self, mock_get_jira_user, mock_client):
|
|
195
|
+
"""Test error handling when adding watcher fails."""
|
|
196
|
+
test_user = UserFactory()
|
|
197
|
+
default_user = UserFactory()
|
|
198
|
+
jira_user = JiraUser.objects.create(id="test-user-add-watcher", user=test_user)
|
|
199
|
+
default_jira_user = JiraUser.objects.create(id="default-user-add", user=default_user)
|
|
200
|
+
mock_get_jira_user.return_value = jira_user
|
|
201
|
+
|
|
202
|
+
mock_client.create_issue.return_value = {
|
|
203
|
+
"id": "123456",
|
|
204
|
+
"key": "TEST-123",
|
|
205
|
+
"reporter": jira_user,
|
|
206
|
+
"summary": "Test Incident"
|
|
207
|
+
}
|
|
208
|
+
mock_client.get_jira_user_from_jira_id.return_value = default_jira_user
|
|
209
|
+
|
|
210
|
+
# Mock add_watcher to raise JiraAPIError
|
|
211
|
+
mock_client.jira.add_watcher.side_effect = JiraAPIError("Cannot add watcher")
|
|
212
|
+
mock_client.jira.remove_watcher = Mock()
|
|
213
|
+
mock_client.jira.add_simple_link = Mock()
|
|
214
|
+
|
|
215
|
+
incident = IncidentFactory(
|
|
216
|
+
priority=self.priority_p1,
|
|
217
|
+
created_by=test_user,
|
|
218
|
+
title="Test Incident"
|
|
219
|
+
)
|
|
220
|
+
channel = IncidentChannelFactory(incident=incident)
|
|
221
|
+
channel.add_bookmark = Mock()
|
|
222
|
+
channel.send_message_and_save = Mock()
|
|
223
|
+
|
|
224
|
+
# Should complete without error despite the exception
|
|
225
|
+
create_ticket(sender=None, incident=incident, channel=channel)
|
|
226
|
+
|
|
227
|
+
# Verify remove_watcher was called as fallback
|
|
228
|
+
mock_client.jira.remove_watcher.assert_called_once()
|
|
229
|
+
|
|
230
|
+
@patch("firefighter.raid.signals.incident_created.client")
|
|
231
|
+
@patch("firefighter.raid.signals.incident_created.get_jira_user_from_user")
|
|
232
|
+
def test_create_ticket_remove_watcher_error(self, mock_get_jira_user, mock_client):
|
|
233
|
+
"""Test error handling when removing default watcher fails."""
|
|
234
|
+
test_user = UserFactory()
|
|
235
|
+
default_user = UserFactory()
|
|
236
|
+
jira_user = JiraUser.objects.create(id="test-user-remove-watcher", user=test_user)
|
|
237
|
+
default_jira_user = JiraUser.objects.create(id="default-user-remove", user=default_user)
|
|
238
|
+
mock_get_jira_user.return_value = jira_user
|
|
239
|
+
|
|
240
|
+
mock_client.create_issue.return_value = {
|
|
241
|
+
"id": "123456",
|
|
242
|
+
"key": "TEST-123",
|
|
243
|
+
"reporter": jira_user,
|
|
244
|
+
"summary": "Test Incident"
|
|
245
|
+
}
|
|
246
|
+
mock_client.get_jira_user_from_jira_id.return_value = default_jira_user
|
|
247
|
+
|
|
248
|
+
# Mock both add_watcher and remove_watcher to raise JiraAPIError
|
|
249
|
+
mock_client.jira.add_watcher.side_effect = JiraAPIError("Cannot add watcher")
|
|
250
|
+
mock_client.jira.remove_watcher.side_effect = JiraAPIError("Cannot remove watcher")
|
|
251
|
+
mock_client.jira.add_simple_link = Mock()
|
|
252
|
+
|
|
253
|
+
incident = IncidentFactory(
|
|
254
|
+
priority=self.priority_p1,
|
|
255
|
+
created_by=test_user,
|
|
256
|
+
title="Test Incident"
|
|
257
|
+
)
|
|
258
|
+
channel = IncidentChannelFactory(incident=incident)
|
|
259
|
+
channel.add_bookmark = Mock()
|
|
260
|
+
channel.send_message_and_save = Mock()
|
|
261
|
+
|
|
262
|
+
# Should complete without error despite both exceptions
|
|
263
|
+
create_ticket(sender=None, incident=incident, channel=channel)
|
|
264
|
+
|
|
265
|
+
# Verify both operations were attempted
|
|
266
|
+
mock_client.jira.add_watcher.assert_called_once()
|
|
267
|
+
mock_client.jira.remove_watcher.assert_called_once()
|