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.
Files changed (64) hide show
  1. firefighter/_version.py +2 -2
  2. firefighter/api/serializers.py +9 -0
  3. firefighter/confluence/signals/incident_updated.py +2 -2
  4. firefighter/incidents/enums.py +22 -2
  5. firefighter/incidents/forms/closure_reason.py +45 -0
  6. firefighter/incidents/forms/unified_incident.py +406 -0
  7. firefighter/incidents/forms/update_status.py +87 -1
  8. firefighter/incidents/migrations/0027_add_closure_fields.py +40 -0
  9. firefighter/incidents/migrations/0028_add_closure_reason_constraint.py +33 -0
  10. firefighter/incidents/migrations/0029_add_custom_fields_to_incident.py +22 -0
  11. firefighter/incidents/models/incident.py +32 -5
  12. firefighter/incidents/static/css/main.min.css +1 -1
  13. firefighter/incidents/templates/layouts/partials/status_pill.html +1 -1
  14. firefighter/incidents/views/reports.py +3 -3
  15. firefighter/raid/apps.py +9 -26
  16. firefighter/raid/client.py +2 -2
  17. firefighter/raid/forms.py +75 -238
  18. firefighter/raid/signals/incident_created.py +38 -13
  19. firefighter/raid/signals/incident_updated.py +3 -2
  20. firefighter/slack/messages/slack_messages.py +19 -4
  21. firefighter/slack/rules.py +1 -1
  22. firefighter/slack/signals/create_incident_conversation.py +6 -0
  23. firefighter/slack/signals/incident_updated.py +7 -1
  24. firefighter/slack/views/modals/__init__.py +4 -0
  25. firefighter/slack/views/modals/base_modal/form_utils.py +63 -0
  26. firefighter/slack/views/modals/close.py +15 -2
  27. firefighter/slack/views/modals/closure_reason.py +193 -0
  28. firefighter/slack/views/modals/open.py +60 -13
  29. firefighter/slack/views/modals/opening/details/unified.py +203 -0
  30. firefighter/slack/views/modals/opening/select_impact.py +1 -1
  31. firefighter/slack/views/modals/opening/set_details.py +3 -2
  32. firefighter/slack/views/modals/postmortem.py +10 -2
  33. firefighter/slack/views/modals/update_status.py +28 -2
  34. firefighter/slack/views/modals/utils.py +51 -0
  35. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/METADATA +1 -1
  36. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/RECORD +62 -38
  37. firefighter_tests/test_incidents/test_enums.py +100 -0
  38. firefighter_tests/test_incidents/test_forms/conftest.py +179 -0
  39. firefighter_tests/test_incidents/test_forms/test_closure_reason.py +91 -0
  40. firefighter_tests/test_incidents/test_forms/test_unified_incident_form.py +570 -0
  41. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_integration.py +581 -0
  42. firefighter_tests/test_incidents/test_forms/test_unified_incident_form_p4_p5.py +410 -0
  43. firefighter_tests/test_incidents/test_forms/test_update_status_workflow.py +343 -0
  44. firefighter_tests/test_incidents/test_forms/test_workflow_transitions.py +167 -0
  45. firefighter_tests/test_incidents/test_models/test_incident_model.py +68 -0
  46. firefighter_tests/test_raid/conftest.py +154 -0
  47. firefighter_tests/test_raid/test_p1_p3_jira_fields.py +372 -0
  48. firefighter_tests/test_raid/test_raid_forms.py +10 -253
  49. firefighter_tests/test_raid/test_raid_signals.py +187 -0
  50. firefighter_tests/test_slack/messages/__init__.py +0 -0
  51. firefighter_tests/test_slack/messages/test_slack_messages.py +367 -0
  52. firefighter_tests/test_slack/views/modals/conftest.py +140 -0
  53. firefighter_tests/test_slack/views/modals/test_close.py +65 -3
  54. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +138 -0
  55. firefighter_tests/test_slack/views/modals/test_form_utils_multiple_choice.py +249 -0
  56. firefighter_tests/test_slack/views/modals/test_open.py +146 -2
  57. firefighter_tests/test_slack/views/modals/test_opening_unified.py +421 -0
  58. firefighter_tests/test_slack/views/modals/test_update_status.py +327 -3
  59. firefighter_tests/test_slack/views/modals/test_utils.py +135 -0
  60. firefighter/raid/views/open_normal.py +0 -139
  61. firefighter/slack/views/modals/opening/details/critical.py +0 -88
  62. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/WHEEL +0 -0
  63. {firefighter_incident-0.0.14.dist-info → firefighter_incident-0.0.16.dist-info}/entry_points.txt +0 -0
  64. {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