firefighter-incident 0.0.14__py3-none-any.whl → 0.0.16__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 +2 -2
- firefighter/api/serializers.py +9 -0
- firefighter/confluence/signals/incident_updated.py +2 -2
- firefighter/incidents/enums.py +22 -2
- firefighter/incidents/forms/closure_reason.py +45 -0
- firefighter/incidents/forms/unified_incident.py +406 -0
- firefighter/incidents/forms/update_status.py +87 -1
- 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/incident.py +32 -5
- firefighter/incidents/static/css/main.min.css +1 -1
- firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
- firefighter/incidents/views/reports.py +3 -3
- firefighter/raid/apps.py +9 -26
- firefighter/raid/client.py +2 -2
- firefighter/raid/forms.py +75 -238
- firefighter/raid/signals/incident_created.py +38 -13
- firefighter/raid/signals/incident_updated.py +3 -2
- firefighter/slack/messages/slack_messages.py +19 -4
- firefighter/slack/rules.py +1 -1
- firefighter/slack/signals/create_incident_conversation.py +6 -0
- firefighter/slack/signals/incident_updated.py +7 -1
- firefighter/slack/views/modals/__init__.py +4 -0
- firefighter/slack/views/modals/base_modal/form_utils.py +63 -0
- firefighter/slack/views/modals/close.py +15 -2
- firefighter/slack/views/modals/closure_reason.py +193 -0
- firefighter/slack/views/modals/open.py +60 -13
- firefighter/slack/views/modals/opening/details/unified.py +203 -0
- firefighter/slack/views/modals/opening/select_impact.py +1 -1
- 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 +28 -2
- firefighter/slack/views/modals/utils.py +51 -0
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/RECORD +62 -38
- 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_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_models/test_incident_model.py +68 -0
- 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_raid_forms.py +10 -253
- firefighter_tests/test_raid/test_raid_signals.py +187 -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 +65 -3
- 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 +327 -3
- 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_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Fixtures for unified incident form tests."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from firefighter.incidents.factories import (
|
|
7
|
+
IncidentCategoryFactory,
|
|
8
|
+
UserFactory,
|
|
9
|
+
)
|
|
10
|
+
from firefighter.incidents.models import Environment, Priority
|
|
11
|
+
from firefighter.incidents.models.impact import ImpactLevel, ImpactType, LevelChoices
|
|
12
|
+
from firefighter.jira_app.models import JiraUser
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def priority_factory(db):
|
|
17
|
+
"""Factory to create Priority instances."""
|
|
18
|
+
|
|
19
|
+
def _create(**kwargs):
|
|
20
|
+
value = kwargs.get("value", 1)
|
|
21
|
+
name = kwargs.get("name", f"P{value}")
|
|
22
|
+
set_as_default = kwargs.get("default", False)
|
|
23
|
+
|
|
24
|
+
# If default=True, clear any other defaults first
|
|
25
|
+
if set_as_default:
|
|
26
|
+
Priority.objects.filter(default=True).update(default=False)
|
|
27
|
+
|
|
28
|
+
defaults = {
|
|
29
|
+
"emoji": "🔴",
|
|
30
|
+
"order": value,
|
|
31
|
+
"default": set_as_default,
|
|
32
|
+
"enabled_create": True,
|
|
33
|
+
"enabled_update": True,
|
|
34
|
+
"needs_postmortem": value <= 2, # P1-P2 need postmortem
|
|
35
|
+
}
|
|
36
|
+
# Remove name and value from kwargs if present
|
|
37
|
+
kwargs_copy = kwargs.copy()
|
|
38
|
+
kwargs_copy.pop("name", None)
|
|
39
|
+
kwargs_copy.pop("value", None)
|
|
40
|
+
defaults.update(kwargs_copy)
|
|
41
|
+
|
|
42
|
+
priority, created = Priority.objects.get_or_create(
|
|
43
|
+
name=name,
|
|
44
|
+
value=value,
|
|
45
|
+
defaults=defaults,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# If already exists and we want it as default, just set that
|
|
49
|
+
if not created and set_as_default:
|
|
50
|
+
priority.default = True
|
|
51
|
+
priority.save(update_fields=["default"])
|
|
52
|
+
|
|
53
|
+
return priority
|
|
54
|
+
|
|
55
|
+
return _create
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.fixture
|
|
59
|
+
def environment_factory(db):
|
|
60
|
+
"""Factory to create Environment instances."""
|
|
61
|
+
|
|
62
|
+
def _create(**kwargs):
|
|
63
|
+
value = kwargs.get("value", "TST")
|
|
64
|
+
set_as_default = kwargs.get("default", False)
|
|
65
|
+
|
|
66
|
+
# If default=True, clear any other defaults first
|
|
67
|
+
if set_as_default:
|
|
68
|
+
Environment.objects.filter(default=True).update(default=False)
|
|
69
|
+
|
|
70
|
+
defaults = {
|
|
71
|
+
"description": f"Environment {value}",
|
|
72
|
+
"order": 1,
|
|
73
|
+
"default": set_as_default,
|
|
74
|
+
}
|
|
75
|
+
# Remove value and default from kwargs if present
|
|
76
|
+
kwargs_copy = kwargs.copy()
|
|
77
|
+
kwargs_copy.pop("value", None)
|
|
78
|
+
kwargs_copy.pop("default", None)
|
|
79
|
+
defaults.update(kwargs_copy)
|
|
80
|
+
|
|
81
|
+
environment, created = Environment.objects.get_or_create(
|
|
82
|
+
value=value,
|
|
83
|
+
defaults=defaults,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# If already exists and we want it as default, just set that
|
|
87
|
+
if not created and set_as_default:
|
|
88
|
+
environment.default = True
|
|
89
|
+
environment.save(update_fields=["default"])
|
|
90
|
+
|
|
91
|
+
return environment
|
|
92
|
+
|
|
93
|
+
return _create
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.fixture
|
|
97
|
+
def impact_level_factory(db):
|
|
98
|
+
"""Factory to create ImpactLevel instances."""
|
|
99
|
+
|
|
100
|
+
def _create(**kwargs):
|
|
101
|
+
# Handle impact__name syntax by extracting nested parameters
|
|
102
|
+
impact_type_data = {}
|
|
103
|
+
keys_to_remove = []
|
|
104
|
+
for key in list(kwargs.keys()):
|
|
105
|
+
if key.startswith("impact__"):
|
|
106
|
+
nested_key = key.split("__", 1)[1]
|
|
107
|
+
impact_type_data[nested_key] = kwargs[key]
|
|
108
|
+
keys_to_remove.append(key)
|
|
109
|
+
for key in keys_to_remove:
|
|
110
|
+
kwargs.pop(key)
|
|
111
|
+
|
|
112
|
+
# Create or get ImpactType
|
|
113
|
+
impact_type = kwargs.pop("impact_type", None)
|
|
114
|
+
if isinstance(impact_type, ImpactType):
|
|
115
|
+
pass # Already have ImpactType instance
|
|
116
|
+
elif impact_type_data:
|
|
117
|
+
impact_type_name = impact_type_data.get("name", "Test Impact")
|
|
118
|
+
impact_type, _ = ImpactType.objects.get_or_create(name=impact_type_name, defaults={
|
|
119
|
+
"emoji": "📊",
|
|
120
|
+
"help_text": f"Test {impact_type_name} impact",
|
|
121
|
+
"value": impact_type_name.lower().replace(" ", "_"),
|
|
122
|
+
"order": 10,
|
|
123
|
+
})
|
|
124
|
+
else:
|
|
125
|
+
impact_type_name = "Test Impact"
|
|
126
|
+
impact_type, _ = ImpactType.objects.get_or_create(name=impact_type_name, defaults={
|
|
127
|
+
"emoji": "📊",
|
|
128
|
+
"help_text": "Test impact",
|
|
129
|
+
"value": "test_impact",
|
|
130
|
+
"order": 10,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
# Handle value parameter
|
|
134
|
+
value = kwargs.pop("value", LevelChoices.LOW)
|
|
135
|
+
|
|
136
|
+
defaults = {
|
|
137
|
+
"impact_type": impact_type,
|
|
138
|
+
"value": value,
|
|
139
|
+
"name": value.label if hasattr(value, "label") else "Test Level",
|
|
140
|
+
"emoji": "📊",
|
|
141
|
+
}
|
|
142
|
+
defaults.update(kwargs)
|
|
143
|
+
return ImpactLevel.objects.create(**defaults)
|
|
144
|
+
|
|
145
|
+
return _create
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@pytest.fixture
|
|
149
|
+
def incident_category_factory(db):
|
|
150
|
+
"""Factory to create IncidentCategory instances."""
|
|
151
|
+
|
|
152
|
+
def _create(**kwargs):
|
|
153
|
+
return IncidentCategoryFactory(**kwargs)
|
|
154
|
+
|
|
155
|
+
return _create
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@pytest.fixture
|
|
159
|
+
def user_factory(db):
|
|
160
|
+
"""Factory to create User instances."""
|
|
161
|
+
|
|
162
|
+
def _create(**kwargs):
|
|
163
|
+
return UserFactory(**kwargs)
|
|
164
|
+
|
|
165
|
+
return _create
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pytest.fixture
|
|
169
|
+
def jira_user_factory(db):
|
|
170
|
+
"""Factory to create JiraUser instances."""
|
|
171
|
+
|
|
172
|
+
def _create(**kwargs):
|
|
173
|
+
user = kwargs.pop("user", None)
|
|
174
|
+
if user is None:
|
|
175
|
+
user = UserFactory()
|
|
176
|
+
jira_id = kwargs.get("id", f"jira-{user.id}")
|
|
177
|
+
return JiraUser.objects.create(id=jira_id, user=user, **kwargs)
|
|
178
|
+
|
|
179
|
+
return _create
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Test the closure reason form."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from django.test import TestCase
|
|
6
|
+
|
|
7
|
+
from firefighter.incidents.enums import ClosureReason
|
|
8
|
+
from firefighter.incidents.forms.closure_reason import IncidentClosureReasonForm
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.django_db
|
|
12
|
+
class TestIncidentClosureReasonForm(TestCase):
|
|
13
|
+
"""Test the IncidentClosureReasonForm."""
|
|
14
|
+
|
|
15
|
+
def test_form_initialization(self):
|
|
16
|
+
"""Test that the form initializes correctly."""
|
|
17
|
+
form = IncidentClosureReasonForm()
|
|
18
|
+
|
|
19
|
+
# Check that all required fields are present
|
|
20
|
+
assert "closure_reason" in form.fields
|
|
21
|
+
assert "closure_reference" in form.fields
|
|
22
|
+
assert "message" in form.fields
|
|
23
|
+
|
|
24
|
+
def test_closure_reason_field_choices(self):
|
|
25
|
+
"""Test that closure reason field has correct choices."""
|
|
26
|
+
form = IncidentClosureReasonForm()
|
|
27
|
+
closure_reason_field = form.fields["closure_reason"]
|
|
28
|
+
|
|
29
|
+
# Check that closure reasons are available (except RESOLVED which is excluded)
|
|
30
|
+
choices = dict(closure_reason_field.choices)
|
|
31
|
+
assert "resolved" not in choices # RESOLVED is excluded
|
|
32
|
+
assert "duplicate" in choices
|
|
33
|
+
assert "false_positive" in choices
|
|
34
|
+
assert "superseded" in choices
|
|
35
|
+
assert "external" in choices
|
|
36
|
+
assert "cancelled" in choices
|
|
37
|
+
|
|
38
|
+
def test_valid_form_submission(self):
|
|
39
|
+
"""Test form with valid data."""
|
|
40
|
+
data = {
|
|
41
|
+
"closure_reason": ClosureReason.DUPLICATE, # Use a valid choice (not RESOLVED)
|
|
42
|
+
"closure_reference": "Fixed by restarting service",
|
|
43
|
+
"message": "The incident has been resolved by restarting the affected service."
|
|
44
|
+
}
|
|
45
|
+
form = IncidentClosureReasonForm(data=data)
|
|
46
|
+
assert form.is_valid(), f"Form errors: {form.errors}"
|
|
47
|
+
|
|
48
|
+
def test_form_with_minimal_data(self):
|
|
49
|
+
"""Test form with minimal required data."""
|
|
50
|
+
data = {
|
|
51
|
+
"closure_reason": ClosureReason.DUPLICATE,
|
|
52
|
+
"message": "Duplicate of incident #123"
|
|
53
|
+
}
|
|
54
|
+
form = IncidentClosureReasonForm(data=data)
|
|
55
|
+
assert form.is_valid(), f"Form errors: {form.errors}"
|
|
56
|
+
|
|
57
|
+
def test_form_missing_closure_reason(self):
|
|
58
|
+
"""Test form validation when closure_reason is missing."""
|
|
59
|
+
data = {
|
|
60
|
+
"message": "Test message"
|
|
61
|
+
}
|
|
62
|
+
form = IncidentClosureReasonForm(data=data)
|
|
63
|
+
assert not form.is_valid()
|
|
64
|
+
assert "closure_reason" in form.errors
|
|
65
|
+
|
|
66
|
+
def test_form_missing_message(self):
|
|
67
|
+
"""Test form validation when message is missing."""
|
|
68
|
+
data = {
|
|
69
|
+
"closure_reason": ClosureReason.DUPLICATE
|
|
70
|
+
}
|
|
71
|
+
form = IncidentClosureReasonForm(data=data)
|
|
72
|
+
assert not form.is_valid()
|
|
73
|
+
assert "message" in form.errors
|
|
74
|
+
|
|
75
|
+
def test_message_field_constraints(self):
|
|
76
|
+
"""Test message field length constraints."""
|
|
77
|
+
# Test valid message (no length constraints defined in the form)
|
|
78
|
+
data = {
|
|
79
|
+
"closure_reason": ClosureReason.DUPLICATE,
|
|
80
|
+
"message": "This is a valid message that meets the requirements."
|
|
81
|
+
}
|
|
82
|
+
form = IncidentClosureReasonForm(data=data)
|
|
83
|
+
assert form.is_valid(), f"Form errors: {form.errors}"
|
|
84
|
+
|
|
85
|
+
def test_form_excludes_resolved_reason(self):
|
|
86
|
+
"""Test that RESOLVED is excluded from choices."""
|
|
87
|
+
form = IncidentClosureReasonForm()
|
|
88
|
+
choices = dict(form.fields["closure_reason"].choices)
|
|
89
|
+
|
|
90
|
+
# RESOLVED should be excluded for early closure forms
|
|
91
|
+
assert "resolved" not in choices
|