firefighter-incident 0.0.22__py3-none-any.whl → 0.0.23__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 +18 -0
- firefighter/api/views/incidents.py +3 -0
- firefighter/confluence/models.py +66 -6
- firefighter/confluence/signals/incident_updated.py +8 -26
- firefighter/firefighter/settings/components/jira_app.py +33 -0
- firefighter/incidents/admin.py +3 -0
- firefighter/incidents/models/impact.py +3 -5
- firefighter/incidents/models/incident.py +24 -9
- firefighter/incidents/views/views.py +2 -0
- firefighter/jira_app/admin.py +15 -1
- firefighter/jira_app/apps.py +3 -0
- firefighter/jira_app/client.py +151 -3
- firefighter/jira_app/management/__init__.py +1 -0
- firefighter/jira_app/management/commands/__init__.py +1 -0
- firefighter/jira_app/migrations/0002_add_jira_postmortem_model.py +71 -0
- firefighter/jira_app/models.py +50 -0
- firefighter/jira_app/service_postmortem.py +292 -0
- firefighter/jira_app/signals/__init__.py +10 -0
- firefighter/jira_app/signals/incident_key_events_updated.py +88 -0
- firefighter/jira_app/signals/postmortem_created.py +155 -0
- firefighter/jira_app/templates/jira/postmortem/impact.txt +12 -0
- firefighter/jira_app/templates/jira/postmortem/incident_summary.txt +17 -0
- firefighter/jira_app/templates/jira/postmortem/mitigation_actions.txt +9 -0
- firefighter/jira_app/templates/jira/postmortem/root_causes.txt +12 -0
- firefighter/jira_app/templates/jira/postmortem/timeline.txt +7 -0
- firefighter/raid/signals/incident_updated.py +31 -11
- firefighter/slack/messages/slack_messages.py +39 -3
- firefighter/slack/signals/postmortem_created.py +51 -3
- firefighter/slack/views/modals/closure_reason.py +15 -0
- firefighter/slack/views/modals/key_event_message.py +9 -0
- firefighter/slack/views/modals/postmortem.py +32 -40
- firefighter/slack/views/modals/update_status.py +7 -1
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/RECORD +50 -31
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/WHEEL +1 -1
- firefighter_tests/test_api/test_renderer.py +41 -0
- firefighter_tests/test_incidents/test_models/test_incident_model.py +29 -0
- firefighter_tests/test_jira_app/test_incident_key_events_sync.py +112 -0
- firefighter_tests/test_jira_app/test_models.py +138 -0
- firefighter_tests/test_jira_app/test_postmortem_issue_link.py +201 -0
- firefighter_tests/test_jira_app/test_postmortem_service.py +416 -0
- firefighter_tests/test_jira_app/test_timeline_template.py +135 -0
- firefighter_tests/test_raid/test_raid_signals.py +50 -8
- firefighter_tests/test_slack/messages/test_slack_messages.py +112 -23
- firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +18 -2
- firefighter_tests/test_slack/views/modals/test_key_event_message.py +30 -0
- firefighter_tests/test_slack/views/modals/test_update_status.py +161 -129
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Tests for Jira post-mortem timeline template rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from django.template.loader import render_to_string
|
|
9
|
+
from django.utils import timezone
|
|
10
|
+
|
|
11
|
+
from firefighter.incidents.enums import IncidentStatus
|
|
12
|
+
from firefighter.incidents.factories import IncidentFactory, UserFactory
|
|
13
|
+
from firefighter.incidents.models.incident_update import IncidentUpdate
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from firefighter.incidents.models.incident import Incident
|
|
17
|
+
from firefighter.incidents.models.user import User
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.django_db
|
|
21
|
+
class TestTimelineTemplate:
|
|
22
|
+
"""Test timeline template rendering with chronological ordering."""
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def test_timeline_includes_key_events_and_status_changes() -> None:
|
|
26
|
+
"""Test that timeline includes both key events and status changes in chronological order."""
|
|
27
|
+
# Create a user
|
|
28
|
+
user: User = UserFactory.create()
|
|
29
|
+
|
|
30
|
+
# Create an incident
|
|
31
|
+
incident: Incident = IncidentFactory.create(
|
|
32
|
+
_status=IncidentStatus.POST_MORTEM,
|
|
33
|
+
created_by=user,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Create key events and status changes at different times
|
|
37
|
+
base_time = timezone.now()
|
|
38
|
+
|
|
39
|
+
# Event 1: detected (key event) - earliest
|
|
40
|
+
IncidentUpdate.objects.create(
|
|
41
|
+
incident=incident,
|
|
42
|
+
event_type="detected",
|
|
43
|
+
event_ts=base_time,
|
|
44
|
+
created_by=user,
|
|
45
|
+
message="Issue was detected",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Event 2: status change to INVESTIGATING - middle
|
|
49
|
+
IncidentUpdate.objects.create(
|
|
50
|
+
incident=incident,
|
|
51
|
+
_status=IncidentStatus.INVESTIGATING,
|
|
52
|
+
event_ts=base_time + timezone.timedelta(minutes=5),
|
|
53
|
+
created_by=user,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Event 3: started (key event) - later
|
|
57
|
+
IncidentUpdate.objects.create(
|
|
58
|
+
incident=incident,
|
|
59
|
+
event_type="started",
|
|
60
|
+
event_ts=base_time + timezone.timedelta(minutes=10),
|
|
61
|
+
created_by=user,
|
|
62
|
+
message="Investigation started",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Event 4: status change to MITIGATING - latest
|
|
66
|
+
IncidentUpdate.objects.create(
|
|
67
|
+
incident=incident,
|
|
68
|
+
_status=IncidentStatus.MITIGATING,
|
|
69
|
+
event_ts=base_time + timezone.timedelta(minutes=15),
|
|
70
|
+
created_by=user,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Render the timeline template
|
|
74
|
+
timeline_content = render_to_string(
|
|
75
|
+
"jira/postmortem/timeline.txt",
|
|
76
|
+
{"incident": incident},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Verify timeline contains all events
|
|
80
|
+
assert "Incident created" in timeline_content
|
|
81
|
+
assert "Key event: Detected" in timeline_content
|
|
82
|
+
assert "Issue was detected" in timeline_content
|
|
83
|
+
assert "Status changed to: Investigating" in timeline_content
|
|
84
|
+
assert "Key event: Started" in timeline_content
|
|
85
|
+
assert "Investigation started" in timeline_content
|
|
86
|
+
assert "Status changed to: Mitigating" in timeline_content
|
|
87
|
+
|
|
88
|
+
# Verify chronological order by checking positions in the timeline
|
|
89
|
+
# The events should appear in this order:
|
|
90
|
+
# 1. Incident created
|
|
91
|
+
# 2. detected (key event)
|
|
92
|
+
# 3. INVESTIGATING (status change)
|
|
93
|
+
# 4. started (key event)
|
|
94
|
+
# 5. MITIGATING (status change)
|
|
95
|
+
created_pos = timeline_content.find("Incident created")
|
|
96
|
+
detected_pos = timeline_content.find("Key event: Detected")
|
|
97
|
+
investigating_pos = timeline_content.find("Status changed to: Investigating")
|
|
98
|
+
started_pos = timeline_content.find("Key event: Started")
|
|
99
|
+
mitigating_pos = timeline_content.find("Status changed to: Mitigating")
|
|
100
|
+
|
|
101
|
+
assert created_pos < detected_pos < investigating_pos < started_pos < mitigating_pos, (
|
|
102
|
+
"Events are not in chronological order"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def test_timeline_handles_key_events_without_message() -> None:
|
|
107
|
+
"""Test that timeline handles key events that have no message."""
|
|
108
|
+
# Create a user
|
|
109
|
+
user: User = UserFactory.create()
|
|
110
|
+
|
|
111
|
+
# Create an incident
|
|
112
|
+
incident: Incident = IncidentFactory.create(
|
|
113
|
+
_status=IncidentStatus.POST_MORTEM,
|
|
114
|
+
created_by=user,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Create a key event without message
|
|
118
|
+
IncidentUpdate.objects.create(
|
|
119
|
+
incident=incident,
|
|
120
|
+
event_type="detected",
|
|
121
|
+
event_ts=timezone.now(),
|
|
122
|
+
created_by=user,
|
|
123
|
+
message=None,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Render the timeline template
|
|
127
|
+
timeline_content = render_to_string(
|
|
128
|
+
"jira/postmortem/timeline.txt",
|
|
129
|
+
{"incident": incident},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Verify the key event appears without a dash for empty message
|
|
133
|
+
assert "Key event: Detected" in timeline_content
|
|
134
|
+
# Should not have " - " when there's no message
|
|
135
|
+
assert "Key event: Detected -" not in timeline_content
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from unittest.mock import Mock, patch
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
|
+
from django.test import override_settings
|
|
8
9
|
|
|
9
10
|
from firefighter.incidents.enums import IncidentStatus
|
|
10
11
|
from firefighter.incidents.models.incident_update import IncidentUpdate
|
|
@@ -19,11 +20,13 @@ class TestIncidentUpdatedCloseJiraTicket:
|
|
|
19
20
|
|
|
20
21
|
@patch("firefighter.raid.signals.incident_updated.client.close_issue")
|
|
21
22
|
def test_close_jira_ticket_when_status_changes_to_mitigated(
|
|
22
|
-
self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory
|
|
23
|
+
self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory, priority_factory
|
|
23
24
|
) -> None:
|
|
24
|
-
"""Test that Jira ticket is closed when incident status changes to MITIGATED
|
|
25
|
+
"""Test that Jira ticket is closed when incident status changes to MITIGATED for P3+."""
|
|
25
26
|
user = user_factory()
|
|
26
|
-
|
|
27
|
+
# Create P3 priority (no postmortem needed)
|
|
28
|
+
p3_priority = priority_factory(value=3, name="P3", needs_postmortem=False)
|
|
29
|
+
incident = incident_factory(created_by=user, priority=p3_priority)
|
|
27
30
|
jira_ticket = jira_ticket_factory(incident=incident)
|
|
28
31
|
incident.jira_ticket = jira_ticket
|
|
29
32
|
|
|
@@ -41,14 +44,53 @@ class TestIncidentUpdatedCloseJiraTicket:
|
|
|
41
44
|
updated_fields=["_status"],
|
|
42
45
|
)
|
|
43
46
|
|
|
44
|
-
# Verify close_issue was called
|
|
47
|
+
# Verify close_issue was called for P3+ incidents
|
|
45
48
|
mock_close_issue.assert_called_once_with(issue_id=jira_ticket.id)
|
|
46
49
|
|
|
50
|
+
@override_settings(ENABLE_JIRA_POSTMORTEM=True)
|
|
51
|
+
@patch("firefighter.raid.signals.incident_updated.client.close_issue")
|
|
52
|
+
def test_do_not_close_jira_ticket_when_p1_mitigated(
|
|
53
|
+
self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory, priority_factory, environment_factory
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Test that Jira ticket is NOT closed when P1 incident status changes to MITIGATED.
|
|
56
|
+
|
|
57
|
+
For P1/P2 incidents requiring postmortem, the ticket should stay open through
|
|
58
|
+
MITIGATED and POST_MORTEM phases, closing only at CLOSED.
|
|
59
|
+
"""
|
|
60
|
+
user = user_factory()
|
|
61
|
+
# Create P1 priority (needs postmortem)
|
|
62
|
+
p1_priority = priority_factory(value=1, name="P1", needs_postmortem=True)
|
|
63
|
+
prd_env = environment_factory(value="PRD", name="Production")
|
|
64
|
+
incident = incident_factory(created_by=user, priority=p1_priority, environment=prd_env)
|
|
65
|
+
jira_ticket = jira_ticket_factory(incident=incident)
|
|
66
|
+
incident.jira_ticket = jira_ticket
|
|
67
|
+
|
|
68
|
+
incident_update = IncidentUpdate(
|
|
69
|
+
incident=incident,
|
|
70
|
+
status=IncidentStatus.MITIGATED,
|
|
71
|
+
created_by=user,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Call the signal handler
|
|
75
|
+
incident_updated_close_ticket_when_mitigated_or_postmortem(
|
|
76
|
+
sender="update_status",
|
|
77
|
+
incident=incident,
|
|
78
|
+
incident_update=incident_update,
|
|
79
|
+
updated_fields=["_status"],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Verify close_issue was NOT called - P1 needs to go through postmortem first
|
|
83
|
+
mock_close_issue.assert_not_called()
|
|
84
|
+
|
|
47
85
|
@patch("firefighter.raid.signals.incident_updated.client.close_issue")
|
|
48
|
-
def
|
|
86
|
+
def test_do_not_close_jira_ticket_when_status_changes_to_postmortem(
|
|
49
87
|
self, mock_close_issue: Mock, incident_factory, user_factory, jira_ticket_factory
|
|
50
88
|
) -> None:
|
|
51
|
-
"""Test that Jira ticket is closed when incident status changes to POST_MORTEM.
|
|
89
|
+
"""Test that Jira ticket is NOT closed when incident status changes to POST_MORTEM.
|
|
90
|
+
|
|
91
|
+
The ticket should remain open during the post-mortem phase and only close
|
|
92
|
+
when the incident reaches CLOSED status.
|
|
93
|
+
"""
|
|
52
94
|
user = user_factory()
|
|
53
95
|
incident = incident_factory(created_by=user)
|
|
54
96
|
jira_ticket = jira_ticket_factory(incident=incident)
|
|
@@ -68,8 +110,8 @@ class TestIncidentUpdatedCloseJiraTicket:
|
|
|
68
110
|
updated_fields=["_status"],
|
|
69
111
|
)
|
|
70
112
|
|
|
71
|
-
# Verify close_issue was called
|
|
72
|
-
mock_close_issue.
|
|
113
|
+
# Verify close_issue was NOT called - ticket stays open during PM
|
|
114
|
+
mock_close_issue.assert_not_called()
|
|
73
115
|
|
|
74
116
|
@patch("firefighter.raid.signals.incident_updated.client.close_issue")
|
|
75
117
|
def test_close_jira_ticket_when_status_changes_to_closed(
|
|
@@ -5,6 +5,7 @@ import pytest
|
|
|
5
5
|
from firefighter.incidents.enums import IncidentStatus
|
|
6
6
|
from firefighter.incidents.factories import IncidentFactory, UserFactory
|
|
7
7
|
from firefighter.incidents.models import IncidentUpdate
|
|
8
|
+
from firefighter.jira_app.models import JiraPostMortem
|
|
8
9
|
from firefighter.slack.factories import IncidentChannelFactory
|
|
9
10
|
from firefighter.slack.messages.slack_messages import (
|
|
10
11
|
SlackMessageDeployWarning,
|
|
@@ -12,6 +13,11 @@ from firefighter.slack.messages.slack_messages import (
|
|
|
12
13
|
SlackMessageIncidentStatusUpdated,
|
|
13
14
|
)
|
|
14
15
|
|
|
16
|
+
try:
|
|
17
|
+
from firefighter.confluence.models import PostMortem
|
|
18
|
+
except (ImportError, AttributeError):
|
|
19
|
+
PostMortem = None
|
|
20
|
+
|
|
15
21
|
|
|
16
22
|
@pytest.mark.django_db
|
|
17
23
|
class TestSlackMessageIncidentStatusUpdated:
|
|
@@ -31,9 +37,7 @@ class TestSlackMessageIncidentStatusUpdated:
|
|
|
31
37
|
# Create an IncidentUpdate
|
|
32
38
|
user = UserFactory.create()
|
|
33
39
|
incident_update = IncidentUpdate.objects.create(
|
|
34
|
-
incident=incident,
|
|
35
|
-
status=IncidentStatus.MITIGATED,
|
|
36
|
-
created_by=user
|
|
40
|
+
incident=incident, status=IncidentStatus.MITIGATED, created_by=user
|
|
37
41
|
)
|
|
38
42
|
|
|
39
43
|
# Create the message with status_changed=True and in_channel=False
|
|
@@ -41,7 +45,7 @@ class TestSlackMessageIncidentStatusUpdated:
|
|
|
41
45
|
incident=incident,
|
|
42
46
|
incident_update=incident_update,
|
|
43
47
|
in_channel=False,
|
|
44
|
-
status_changed=True
|
|
48
|
+
status_changed=True,
|
|
45
49
|
)
|
|
46
50
|
|
|
47
51
|
# Verify the title contains "Mitigated" (the status label)
|
|
@@ -61,9 +65,7 @@ class TestSlackMessageIncidentStatusUpdated:
|
|
|
61
65
|
# Create an IncidentUpdate
|
|
62
66
|
user = UserFactory.create()
|
|
63
67
|
incident_update = IncidentUpdate.objects.create(
|
|
64
|
-
incident=incident,
|
|
65
|
-
status=IncidentStatus.INVESTIGATING,
|
|
66
|
-
created_by=user
|
|
68
|
+
incident=incident, status=IncidentStatus.INVESTIGATING, created_by=user
|
|
67
69
|
)
|
|
68
70
|
|
|
69
71
|
# Create the message with status_changed=True and in_channel=False
|
|
@@ -71,7 +73,7 @@ class TestSlackMessageIncidentStatusUpdated:
|
|
|
71
73
|
incident=incident,
|
|
72
74
|
incident_update=incident_update,
|
|
73
75
|
in_channel=False,
|
|
74
|
-
status_changed=True
|
|
76
|
+
status_changed=True,
|
|
75
77
|
)
|
|
76
78
|
|
|
77
79
|
# Verify the title does NOT contain the MITIGATED-specific format
|
|
@@ -93,9 +95,7 @@ class TestSlackMessageIncidentStatusUpdated:
|
|
|
93
95
|
# Create an IncidentUpdate with status change
|
|
94
96
|
user = UserFactory.create()
|
|
95
97
|
incident_update = IncidentUpdate.objects.create(
|
|
96
|
-
incident=incident,
|
|
97
|
-
status=IncidentStatus.CLOSED,
|
|
98
|
-
created_by=user
|
|
98
|
+
incident=incident, status=IncidentStatus.CLOSED, created_by=user
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
# Create the message with in_channel=True (normal case for in-channel messages)
|
|
@@ -103,7 +103,7 @@ class TestSlackMessageIncidentStatusUpdated:
|
|
|
103
103
|
incident=incident,
|
|
104
104
|
incident_update=incident_update,
|
|
105
105
|
in_channel=True,
|
|
106
|
-
status_changed=True
|
|
106
|
+
status_changed=True,
|
|
107
107
|
)
|
|
108
108
|
|
|
109
109
|
# Get the blocks
|
|
@@ -118,7 +118,9 @@ class TestSlackMessageIncidentStatusUpdated:
|
|
|
118
118
|
|
|
119
119
|
# If there is a status update block, verify it has NO accessory (button)
|
|
120
120
|
if status_update_block:
|
|
121
|
-
assert status_update_block.accessory is None,
|
|
121
|
+
assert status_update_block.accessory is None, (
|
|
122
|
+
"Update button should not be present when incident is CLOSED"
|
|
123
|
+
)
|
|
122
124
|
|
|
123
125
|
def test_update_button_shown_when_incident_not_closed(self) -> None:
|
|
124
126
|
"""Test that Update Status button IS displayed when incident is NOT CLOSED."""
|
|
@@ -131,9 +133,7 @@ class TestSlackMessageIncidentStatusUpdated:
|
|
|
131
133
|
# Create an IncidentUpdate
|
|
132
134
|
user = UserFactory.create()
|
|
133
135
|
incident_update = IncidentUpdate.objects.create(
|
|
134
|
-
incident=incident,
|
|
135
|
-
status=IncidentStatus.INVESTIGATING,
|
|
136
|
-
created_by=user
|
|
136
|
+
incident=incident, status=IncidentStatus.INVESTIGATING, created_by=user
|
|
137
137
|
)
|
|
138
138
|
|
|
139
139
|
# Create the message with in_channel=True
|
|
@@ -141,7 +141,7 @@ class TestSlackMessageIncidentStatusUpdated:
|
|
|
141
141
|
incident=incident,
|
|
142
142
|
incident_update=incident_update,
|
|
143
143
|
in_channel=True,
|
|
144
|
-
status_changed=True
|
|
144
|
+
status_changed=True,
|
|
145
145
|
)
|
|
146
146
|
|
|
147
147
|
# Get the blocks
|
|
@@ -156,7 +156,9 @@ class TestSlackMessageIncidentStatusUpdated:
|
|
|
156
156
|
|
|
157
157
|
# Verify the block has an accessory (Update button)
|
|
158
158
|
assert status_update_block is not None, "Should have a status update block"
|
|
159
|
-
assert status_update_block.accessory is not None,
|
|
159
|
+
assert status_update_block.accessory is not None, (
|
|
160
|
+
"Update button should be present when incident is not CLOSED"
|
|
161
|
+
)
|
|
160
162
|
assert status_update_block.accessory.text.text == "Update"
|
|
161
163
|
|
|
162
164
|
|
|
@@ -183,7 +185,7 @@ class TestSlackMessageDeployWarning:
|
|
|
183
185
|
|
|
184
186
|
# The first block should be a HeaderBlock with "(Mitigated)" in the text
|
|
185
187
|
header_block = blocks[0]
|
|
186
|
-
header_text = header_block.text.text
|
|
188
|
+
header_text = header_block.text.text
|
|
187
189
|
|
|
188
190
|
assert "(Mitigated)" in header_text
|
|
189
191
|
assert ":warning:" in header_text
|
|
@@ -204,7 +206,7 @@ class TestSlackMessageDeployWarning:
|
|
|
204
206
|
|
|
205
207
|
# The first block should be a HeaderBlock WITHOUT "(Mitigated)"
|
|
206
208
|
header_block = blocks[0]
|
|
207
|
-
header_text = header_block.text.text
|
|
209
|
+
header_text = header_block.text.text
|
|
208
210
|
|
|
209
211
|
assert "(Mitigated)" not in header_text
|
|
210
212
|
assert ":warning:" in header_text
|
|
@@ -283,7 +285,10 @@ class TestSlackMessageIncidentDeclaredAnnouncement:
|
|
|
283
285
|
assert fields_block is not None, "Should have a block with fields"
|
|
284
286
|
|
|
285
287
|
# Convert fields to strings for easier assertion (access .text attribute)
|
|
286
|
-
fields_text = " ".join(
|
|
288
|
+
fields_text = " ".join(
|
|
289
|
+
field.text if hasattr(field, "text") else str(field)
|
|
290
|
+
for field in fields_block.fields
|
|
291
|
+
)
|
|
287
292
|
|
|
288
293
|
# Verify custom fields are present
|
|
289
294
|
assert "Zendesk Ticket" in fields_text
|
|
@@ -316,7 +321,10 @@ class TestSlackMessageIncidentDeclaredAnnouncement:
|
|
|
316
321
|
assert fields_block is not None, "Should have a block with fields"
|
|
317
322
|
|
|
318
323
|
# Convert fields to strings (access .text attribute)
|
|
319
|
-
fields_text = " ".join(
|
|
324
|
+
fields_text = " ".join(
|
|
325
|
+
field.text if hasattr(field, "text") else str(field)
|
|
326
|
+
for field in fields_block.fields
|
|
327
|
+
)
|
|
320
328
|
|
|
321
329
|
# Verify custom fields are NOT present
|
|
322
330
|
assert "Zendesk Ticket" not in fields_text
|
|
@@ -354,7 +362,10 @@ class TestSlackMessageIncidentDeclaredAnnouncement:
|
|
|
354
362
|
assert fields_block is not None, "Should have a block with fields"
|
|
355
363
|
|
|
356
364
|
# Convert fields to strings (access .text attribute)
|
|
357
|
-
fields_text = " ".join(
|
|
365
|
+
fields_text = " ".join(
|
|
366
|
+
field.text if hasattr(field, "text") else str(field)
|
|
367
|
+
for field in fields_block.fields
|
|
368
|
+
)
|
|
358
369
|
|
|
359
370
|
# Verify only zendesk_ticket_id is present
|
|
360
371
|
assert "Zendesk Ticket" in fields_text
|
|
@@ -365,3 +376,81 @@ class TestSlackMessageIncidentDeclaredAnnouncement:
|
|
|
365
376
|
assert "Zoho Desk Ticket" not in fields_text
|
|
366
377
|
assert "Key Account" not in fields_text
|
|
367
378
|
assert "Golden List Seller" not in fields_text
|
|
379
|
+
|
|
380
|
+
def test_jira_postmortem_link_displayed_when_present(self) -> None:
|
|
381
|
+
"""Test that Jira post-mortem link is displayed when available."""
|
|
382
|
+
# Create an incident
|
|
383
|
+
incident = IncidentFactory.create()
|
|
384
|
+
user = UserFactory.create()
|
|
385
|
+
|
|
386
|
+
# Create a Jira post-mortem for the incident
|
|
387
|
+
jira_pm = JiraPostMortem.objects.create(
|
|
388
|
+
incident=incident,
|
|
389
|
+
jira_issue_key="PM-123",
|
|
390
|
+
jira_issue_id="12345",
|
|
391
|
+
created_by=user,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Create the message
|
|
395
|
+
message = SlackMessageIncidentDeclaredAnnouncement(incident=incident)
|
|
396
|
+
|
|
397
|
+
# Get the blocks
|
|
398
|
+
blocks = message.get_blocks()
|
|
399
|
+
|
|
400
|
+
# Collect all fields from all SectionBlocks
|
|
401
|
+
all_fields = []
|
|
402
|
+
for block in blocks:
|
|
403
|
+
if hasattr(block, "fields") and block.fields:
|
|
404
|
+
all_fields.extend(block.fields)
|
|
405
|
+
|
|
406
|
+
assert len(all_fields) > 0, "Should have blocks with fields"
|
|
407
|
+
|
|
408
|
+
# Convert fields to strings (access .text attribute)
|
|
409
|
+
fields_text = " ".join(
|
|
410
|
+
field.text if hasattr(field, "text") else str(field)
|
|
411
|
+
for field in all_fields
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Verify Jira post-mortem link is present (now in separate PM block)
|
|
415
|
+
assert "Jira Post-mortem" in fields_text
|
|
416
|
+
assert "PM-123" in fields_text
|
|
417
|
+
assert jira_pm.issue_url in fields_text
|
|
418
|
+
|
|
419
|
+
def test_confluence_postmortem_link_displayed_when_present(self) -> None:
|
|
420
|
+
"""Test that Confluence post-mortem link is displayed when available."""
|
|
421
|
+
if PostMortem is None:
|
|
422
|
+
pytest.skip("Confluence app not installed")
|
|
423
|
+
|
|
424
|
+
# Create an incident
|
|
425
|
+
incident = IncidentFactory.create()
|
|
426
|
+
|
|
427
|
+
# Create a Confluence post-mortem for the incident
|
|
428
|
+
confluence_pm = PostMortem.objects.create(
|
|
429
|
+
incident=incident,
|
|
430
|
+
page_id="123456",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Create the message
|
|
434
|
+
message = SlackMessageIncidentDeclaredAnnouncement(incident=incident)
|
|
435
|
+
|
|
436
|
+
# Get the blocks
|
|
437
|
+
blocks = message.get_blocks()
|
|
438
|
+
|
|
439
|
+
# Find the SectionBlock with fields
|
|
440
|
+
fields_block = None
|
|
441
|
+
for block in blocks:
|
|
442
|
+
if hasattr(block, "fields") and block.fields:
|
|
443
|
+
fields_block = block
|
|
444
|
+
break
|
|
445
|
+
|
|
446
|
+
assert fields_block is not None, "Should have a block with fields"
|
|
447
|
+
|
|
448
|
+
# Convert fields to strings (access .text attribute)
|
|
449
|
+
fields_text = " ".join(
|
|
450
|
+
field.text if hasattr(field, "text") else str(field)
|
|
451
|
+
for field in fields_block.fields
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# Verify Confluence post-mortem link is present
|
|
455
|
+
assert "Confluence Post-mortem" in fields_text
|
|
456
|
+
assert confluence_pm.page_url in fields_text
|
|
@@ -16,7 +16,7 @@ from firefighter.slack.views.modals.closure_reason import ClosureReasonModal
|
|
|
16
16
|
class TestClosureReasonModalMessageTabDisabled:
|
|
17
17
|
"""Test ClosureReasonModal handles messages_tab_disabled gracefully."""
|
|
18
18
|
|
|
19
|
-
def test_closure_reason_handles_messages_tab_disabled(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
19
|
+
def test_closure_reason_handles_messages_tab_disabled(self, caplog: pytest.LogCaptureFixture, mocker) -> None:
|
|
20
20
|
"""Test that messages_tab_disabled error is handled gracefully with warning log."""
|
|
21
21
|
# Create test data
|
|
22
22
|
user = UserFactory.build()
|
|
@@ -24,6 +24,14 @@ class TestClosureReasonModalMessageTabDisabled:
|
|
|
24
24
|
incident = IncidentFactory.build(_status=IncidentStatus.INVESTIGATING, created_by=user)
|
|
25
25
|
incident.save()
|
|
26
26
|
|
|
27
|
+
# Mock can_be_closed to return True so the closure can proceed
|
|
28
|
+
mocker.patch.object(
|
|
29
|
+
type(incident),
|
|
30
|
+
"can_be_closed",
|
|
31
|
+
new_callable=mocker.PropertyMock,
|
|
32
|
+
return_value=(True, [])
|
|
33
|
+
)
|
|
34
|
+
|
|
27
35
|
# Create modal and mock
|
|
28
36
|
modal = ClosureReasonModal()
|
|
29
37
|
ack = MagicMock()
|
|
@@ -83,7 +91,7 @@ class TestClosureReasonModalMessageTabDisabled:
|
|
|
83
91
|
for record in caplog.records
|
|
84
92
|
)
|
|
85
93
|
|
|
86
|
-
def test_closure_reason_reraises_other_slack_errors(self) -> None:
|
|
94
|
+
def test_closure_reason_reraises_other_slack_errors(self, mocker) -> None:
|
|
87
95
|
"""Test that other Slack API errors are re-raised."""
|
|
88
96
|
# Create test data
|
|
89
97
|
user = UserFactory.build()
|
|
@@ -91,6 +99,14 @@ class TestClosureReasonModalMessageTabDisabled:
|
|
|
91
99
|
incident = IncidentFactory.build(_status=IncidentStatus.INVESTIGATING, created_by=user)
|
|
92
100
|
incident.save()
|
|
93
101
|
|
|
102
|
+
# Mock can_be_closed to return True so the closure can proceed
|
|
103
|
+
mocker.patch.object(
|
|
104
|
+
type(incident),
|
|
105
|
+
"can_be_closed",
|
|
106
|
+
new_callable=mocker.PropertyMock,
|
|
107
|
+
return_value=(True, [])
|
|
108
|
+
)
|
|
109
|
+
|
|
94
110
|
# Create modal and mock
|
|
95
111
|
modal = ClosureReasonModal()
|
|
96
112
|
ack = MagicMock()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Tests for Slack key event message view."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from firefighter.slack.views.modals import key_event_message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestKeyEventMessageImports:
|
|
11
|
+
"""Test that key event message module has necessary imports."""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def test_signal_imported() -> None:
|
|
15
|
+
"""Test that incident_key_events_updated signal is imported."""
|
|
16
|
+
# Verify the signal is imported in the module
|
|
17
|
+
assert hasattr(
|
|
18
|
+
key_event_message, "incident_key_events_updated"
|
|
19
|
+
), "incident_key_events_updated signal should be imported"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.django_db
|
|
23
|
+
class TestKeyEventMessageSignalIntegration:
|
|
24
|
+
"""Integration tests verifying signal is sent - covered by test_jira_app tests."""
|
|
25
|
+
|
|
26
|
+
# Note: The actual signal sending is tested in:
|
|
27
|
+
# - tests/test_jira_app/test_incident_key_events_sync.py
|
|
28
|
+
# These tests verify the signal handler receives and processes signals correctly.
|
|
29
|
+
# Testing the Slack view's signal sending requires complex Slack mocking,
|
|
30
|
+
# so we rely on manual testing and the signal handler tests instead.
|