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,367 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from firefighter.incidents.enums import IncidentStatus
|
|
6
|
+
from firefighter.incidents.factories import IncidentFactory, UserFactory
|
|
7
|
+
from firefighter.incidents.models import IncidentUpdate
|
|
8
|
+
from firefighter.slack.factories import IncidentChannelFactory
|
|
9
|
+
from firefighter.slack.messages.slack_messages import (
|
|
10
|
+
SlackMessageDeployWarning,
|
|
11
|
+
SlackMessageIncidentDeclaredAnnouncement,
|
|
12
|
+
SlackMessageIncidentStatusUpdated,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.django_db
|
|
17
|
+
class TestSlackMessageIncidentStatusUpdated:
|
|
18
|
+
"""Test SlackMessageIncidentStatusUpdated message generation."""
|
|
19
|
+
|
|
20
|
+
def test_title_when_status_mitigated_and_changed(self) -> None:
|
|
21
|
+
"""Test that title mentions MITIGATED when incident reaches MITIGATED status.
|
|
22
|
+
|
|
23
|
+
Covers line 445: elif incident.status == IncidentStatus.MITIGATED and status_changed:
|
|
24
|
+
"""
|
|
25
|
+
# Create an incident in MITIGATED status
|
|
26
|
+
incident = IncidentFactory.create(_status=IncidentStatus.MITIGATED)
|
|
27
|
+
|
|
28
|
+
# Create an IncidentChannel for slack_channel_name
|
|
29
|
+
IncidentChannelFactory.create(incident=incident)
|
|
30
|
+
|
|
31
|
+
# Create an IncidentUpdate
|
|
32
|
+
user = UserFactory.create()
|
|
33
|
+
incident_update = IncidentUpdate.objects.create(
|
|
34
|
+
incident=incident,
|
|
35
|
+
status=IncidentStatus.MITIGATED,
|
|
36
|
+
created_by=user
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Create the message with status_changed=True and in_channel=False
|
|
40
|
+
message = SlackMessageIncidentStatusUpdated(
|
|
41
|
+
incident=incident,
|
|
42
|
+
incident_update=incident_update,
|
|
43
|
+
in_channel=False,
|
|
44
|
+
status_changed=True
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Verify the title contains "Mitigated" (the status label)
|
|
48
|
+
assert message.title_text is not None
|
|
49
|
+
assert "Mitigated" in message.title_text
|
|
50
|
+
assert ":large_green_circle:" in message.title_text
|
|
51
|
+
assert incident.slack_channel_name in message.title_text
|
|
52
|
+
|
|
53
|
+
def test_title_when_status_not_mitigated(self) -> None:
|
|
54
|
+
"""Test that title does not use MITIGATED-specific format when status is different."""
|
|
55
|
+
# Create an incident in INVESTIGATING status
|
|
56
|
+
incident = IncidentFactory.create(_status=IncidentStatus.INVESTIGATING)
|
|
57
|
+
|
|
58
|
+
# Create an IncidentChannel
|
|
59
|
+
IncidentChannelFactory.create(incident=incident)
|
|
60
|
+
|
|
61
|
+
# Create an IncidentUpdate
|
|
62
|
+
user = UserFactory.create()
|
|
63
|
+
incident_update = IncidentUpdate.objects.create(
|
|
64
|
+
incident=incident,
|
|
65
|
+
status=IncidentStatus.INVESTIGATING,
|
|
66
|
+
created_by=user
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Create the message with status_changed=True and in_channel=False
|
|
70
|
+
message = SlackMessageIncidentStatusUpdated(
|
|
71
|
+
incident=incident,
|
|
72
|
+
incident_update=incident_update,
|
|
73
|
+
in_channel=False,
|
|
74
|
+
status_changed=True
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Verify the title does NOT contain the MITIGATED-specific format
|
|
78
|
+
assert message.title_text is not None
|
|
79
|
+
assert ":large_green_circle:" not in message.title_text
|
|
80
|
+
assert "has received an update" in message.title_text
|
|
81
|
+
|
|
82
|
+
def test_no_update_button_when_incident_closed(self) -> None:
|
|
83
|
+
"""Test that Update Status button is NOT displayed when incident is CLOSED.
|
|
84
|
+
|
|
85
|
+
This prevents showing an action button in an archived channel.
|
|
86
|
+
"""
|
|
87
|
+
# Create an incident in CLOSED status
|
|
88
|
+
incident = IncidentFactory.create(_status=IncidentStatus.CLOSED)
|
|
89
|
+
|
|
90
|
+
# Create an IncidentChannel
|
|
91
|
+
IncidentChannelFactory.create(incident=incident)
|
|
92
|
+
|
|
93
|
+
# Create an IncidentUpdate with status change
|
|
94
|
+
user = UserFactory.create()
|
|
95
|
+
incident_update = IncidentUpdate.objects.create(
|
|
96
|
+
incident=incident,
|
|
97
|
+
status=IncidentStatus.CLOSED,
|
|
98
|
+
created_by=user
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Create the message with in_channel=True (normal case for in-channel messages)
|
|
102
|
+
message = SlackMessageIncidentStatusUpdated(
|
|
103
|
+
incident=incident,
|
|
104
|
+
incident_update=incident_update,
|
|
105
|
+
in_channel=True,
|
|
106
|
+
status_changed=True
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Get the blocks
|
|
110
|
+
blocks = message.get_blocks()
|
|
111
|
+
|
|
112
|
+
# Find any SectionBlock with the "message_status_update" block_id
|
|
113
|
+
status_update_block = None
|
|
114
|
+
for block in blocks:
|
|
115
|
+
if hasattr(block, "block_id") and block.block_id == "message_status_update":
|
|
116
|
+
status_update_block = block
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
# If there is a status update block, verify it has NO accessory (button)
|
|
120
|
+
if status_update_block:
|
|
121
|
+
assert status_update_block.accessory is None, "Update button should not be present when incident is CLOSED"
|
|
122
|
+
|
|
123
|
+
def test_update_button_shown_when_incident_not_closed(self) -> None:
|
|
124
|
+
"""Test that Update Status button IS displayed when incident is NOT CLOSED."""
|
|
125
|
+
# Create an incident in INVESTIGATING status (not closed)
|
|
126
|
+
incident = IncidentFactory.create(_status=IncidentStatus.INVESTIGATING)
|
|
127
|
+
|
|
128
|
+
# Create an IncidentChannel
|
|
129
|
+
IncidentChannelFactory.create(incident=incident)
|
|
130
|
+
|
|
131
|
+
# Create an IncidentUpdate
|
|
132
|
+
user = UserFactory.create()
|
|
133
|
+
incident_update = IncidentUpdate.objects.create(
|
|
134
|
+
incident=incident,
|
|
135
|
+
status=IncidentStatus.INVESTIGATING,
|
|
136
|
+
created_by=user
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Create the message with in_channel=True
|
|
140
|
+
message = SlackMessageIncidentStatusUpdated(
|
|
141
|
+
incident=incident,
|
|
142
|
+
incident_update=incident_update,
|
|
143
|
+
in_channel=True,
|
|
144
|
+
status_changed=True
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Get the blocks
|
|
148
|
+
blocks = message.get_blocks()
|
|
149
|
+
|
|
150
|
+
# Find the SectionBlock with "message_status_update" block_id
|
|
151
|
+
status_update_block = None
|
|
152
|
+
for block in blocks:
|
|
153
|
+
if hasattr(block, "block_id") and block.block_id == "message_status_update":
|
|
154
|
+
status_update_block = block
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
# Verify the block has an accessory (Update button)
|
|
158
|
+
assert status_update_block is not None, "Should have a status update block"
|
|
159
|
+
assert status_update_block.accessory is not None, "Update button should be present when incident is not CLOSED"
|
|
160
|
+
assert status_update_block.accessory.text.text == "Update"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.mark.django_db
|
|
164
|
+
class TestSlackMessageDeployWarning:
|
|
165
|
+
"""Test SlackMessageDeployWarning message generation."""
|
|
166
|
+
|
|
167
|
+
def test_header_when_incident_mitigated(self) -> None:
|
|
168
|
+
"""Test that header includes '(Mitigated)' when incident is MITIGATED.
|
|
169
|
+
|
|
170
|
+
Covers line 694: text=f":warning: Deploy warning {'(Mitigated) ' if self.incident.status == IncidentStatus.MITIGATED else ''}:warning:"
|
|
171
|
+
"""
|
|
172
|
+
# Create an incident in MITIGATED status
|
|
173
|
+
incident = IncidentFactory.create(_status=IncidentStatus.MITIGATED)
|
|
174
|
+
|
|
175
|
+
# Create an IncidentChannel for conversation.name
|
|
176
|
+
IncidentChannelFactory.create(incident=incident)
|
|
177
|
+
|
|
178
|
+
# Create the deploy warning message
|
|
179
|
+
message = SlackMessageDeployWarning(incident=incident)
|
|
180
|
+
|
|
181
|
+
# Get the blocks
|
|
182
|
+
blocks = message.get_blocks()
|
|
183
|
+
|
|
184
|
+
# The first block should be a HeaderBlock with "(Mitigated)" in the text
|
|
185
|
+
header_block = blocks[0]
|
|
186
|
+
header_text = header_block.text.text # type: ignore[attr-defined]
|
|
187
|
+
|
|
188
|
+
assert "(Mitigated)" in header_text
|
|
189
|
+
assert ":warning:" in header_text
|
|
190
|
+
|
|
191
|
+
def test_header_when_incident_not_mitigated(self) -> None:
|
|
192
|
+
"""Test that header does NOT include '(Mitigated)' when incident is not MITIGATED."""
|
|
193
|
+
# Create an incident in INVESTIGATING status
|
|
194
|
+
incident = IncidentFactory.create(_status=IncidentStatus.INVESTIGATING)
|
|
195
|
+
|
|
196
|
+
# Create an IncidentChannel
|
|
197
|
+
IncidentChannelFactory.create(incident=incident)
|
|
198
|
+
|
|
199
|
+
# Create the deploy warning message
|
|
200
|
+
message = SlackMessageDeployWarning(incident=incident)
|
|
201
|
+
|
|
202
|
+
# Get the blocks
|
|
203
|
+
blocks = message.get_blocks()
|
|
204
|
+
|
|
205
|
+
# The first block should be a HeaderBlock WITHOUT "(Mitigated)"
|
|
206
|
+
header_block = blocks[0]
|
|
207
|
+
header_text = header_block.text.text # type: ignore[attr-defined]
|
|
208
|
+
|
|
209
|
+
assert "(Mitigated)" not in header_text
|
|
210
|
+
assert ":warning:" in header_text
|
|
211
|
+
|
|
212
|
+
def test_additional_blocks_when_incident_mitigated_or_above(self) -> None:
|
|
213
|
+
"""Test that additional blocks are added when incident status >= MITIGATED.
|
|
214
|
+
|
|
215
|
+
Covers line 705: if self.incident.status >= IncidentStatus.MITIGATED:
|
|
216
|
+
"""
|
|
217
|
+
# Create an incident in MITIGATED status
|
|
218
|
+
incident = IncidentFactory.create(_status=IncidentStatus.MITIGATED)
|
|
219
|
+
|
|
220
|
+
# Create an IncidentChannel
|
|
221
|
+
IncidentChannelFactory.create(incident=incident)
|
|
222
|
+
|
|
223
|
+
# Create the deploy warning message
|
|
224
|
+
message = SlackMessageDeployWarning(incident=incident)
|
|
225
|
+
|
|
226
|
+
# Get the blocks
|
|
227
|
+
blocks = message.get_blocks()
|
|
228
|
+
|
|
229
|
+
# When status >= MITIGATED, there should be MORE than 2 blocks
|
|
230
|
+
# (HeaderBlock + SectionBlock + additional blocks from line 706)
|
|
231
|
+
assert len(blocks) > 2
|
|
232
|
+
|
|
233
|
+
def test_no_additional_blocks_when_incident_below_mitigated(self) -> None:
|
|
234
|
+
"""Test that NO additional blocks are added when incident status < MITIGATED."""
|
|
235
|
+
# Create an incident in INVESTIGATING status (below MITIGATED)
|
|
236
|
+
incident = IncidentFactory.create(_status=IncidentStatus.INVESTIGATING)
|
|
237
|
+
|
|
238
|
+
# Create an IncidentChannel
|
|
239
|
+
IncidentChannelFactory.create(incident=incident)
|
|
240
|
+
|
|
241
|
+
# Create the deploy warning message
|
|
242
|
+
message = SlackMessageDeployWarning(incident=incident)
|
|
243
|
+
|
|
244
|
+
# Get the blocks
|
|
245
|
+
blocks = message.get_blocks()
|
|
246
|
+
|
|
247
|
+
# When status < MITIGATED, there should be exactly 2 blocks
|
|
248
|
+
# (HeaderBlock + SectionBlock only, no additional blocks)
|
|
249
|
+
assert len(blocks) == 2
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@pytest.mark.django_db
|
|
253
|
+
class TestSlackMessageIncidentDeclaredAnnouncement:
|
|
254
|
+
"""Test SlackMessageIncidentDeclaredAnnouncement message with custom fields."""
|
|
255
|
+
|
|
256
|
+
def test_custom_fields_displayed_when_present(self) -> None:
|
|
257
|
+
"""Test that custom fields are displayed in the incident announcement."""
|
|
258
|
+
# Create an incident with custom fields
|
|
259
|
+
incident = IncidentFactory.create(
|
|
260
|
+
custom_fields={
|
|
261
|
+
"zendesk_ticket_id": "12345",
|
|
262
|
+
"seller_contract_id": "SELLER-67890",
|
|
263
|
+
"zoho_desk_ticket_id": "ZD-11111",
|
|
264
|
+
"is_key_account": True,
|
|
265
|
+
"is_seller_in_golden_list": True,
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Create the message
|
|
270
|
+
message = SlackMessageIncidentDeclaredAnnouncement(incident=incident)
|
|
271
|
+
|
|
272
|
+
# Get the blocks
|
|
273
|
+
blocks = message.get_blocks()
|
|
274
|
+
|
|
275
|
+
# Find the SectionBlock with fields (should contain custom fields)
|
|
276
|
+
# This is typically the 4th block (index 3)
|
|
277
|
+
fields_block = None
|
|
278
|
+
for block in blocks:
|
|
279
|
+
if hasattr(block, "fields") and block.fields:
|
|
280
|
+
fields_block = block
|
|
281
|
+
break
|
|
282
|
+
|
|
283
|
+
assert fields_block is not None, "Should have a block with fields"
|
|
284
|
+
|
|
285
|
+
# Convert fields to strings for easier assertion (access .text attribute)
|
|
286
|
+
fields_text = " ".join(field.text if hasattr(field, "text") else str(field) for field in fields_block.fields)
|
|
287
|
+
|
|
288
|
+
# Verify custom fields are present
|
|
289
|
+
assert "Zendesk Ticket" in fields_text
|
|
290
|
+
assert "12345" in fields_text
|
|
291
|
+
assert "Seller Contract" in fields_text
|
|
292
|
+
assert "SELLER-67890" in fields_text
|
|
293
|
+
assert "Zoho Desk Ticket" in fields_text
|
|
294
|
+
assert "ZD-11111" in fields_text
|
|
295
|
+
assert "Key Account" in fields_text
|
|
296
|
+
assert "Golden List Seller" in fields_text
|
|
297
|
+
|
|
298
|
+
def test_custom_fields_not_displayed_when_absent(self) -> None:
|
|
299
|
+
"""Test that custom fields are NOT displayed when not present."""
|
|
300
|
+
# Create an incident WITHOUT custom fields
|
|
301
|
+
incident = IncidentFactory.create(custom_fields={})
|
|
302
|
+
|
|
303
|
+
# Create the message
|
|
304
|
+
message = SlackMessageIncidentDeclaredAnnouncement(incident=incident)
|
|
305
|
+
|
|
306
|
+
# Get the blocks
|
|
307
|
+
blocks = message.get_blocks()
|
|
308
|
+
|
|
309
|
+
# Find the SectionBlock with fields
|
|
310
|
+
fields_block = None
|
|
311
|
+
for block in blocks:
|
|
312
|
+
if hasattr(block, "fields") and block.fields:
|
|
313
|
+
fields_block = block
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
assert fields_block is not None, "Should have a block with fields"
|
|
317
|
+
|
|
318
|
+
# Convert fields to strings (access .text attribute)
|
|
319
|
+
fields_text = " ".join(field.text if hasattr(field, "text") else str(field) for field in fields_block.fields)
|
|
320
|
+
|
|
321
|
+
# Verify custom fields are NOT present
|
|
322
|
+
assert "Zendesk Ticket" not in fields_text
|
|
323
|
+
assert "Seller Contract" not in fields_text
|
|
324
|
+
assert "Zoho Desk Ticket" not in fields_text
|
|
325
|
+
assert "Key Account" not in fields_text
|
|
326
|
+
assert "Golden List Seller" not in fields_text
|
|
327
|
+
|
|
328
|
+
def test_partial_custom_fields_displayed(self) -> None:
|
|
329
|
+
"""Test that only filled custom fields are displayed."""
|
|
330
|
+
# Create an incident with only some custom fields
|
|
331
|
+
incident = IncidentFactory.create(
|
|
332
|
+
custom_fields={
|
|
333
|
+
"zendesk_ticket_id": "12345",
|
|
334
|
+
# seller_contract_id not set
|
|
335
|
+
# zoho_desk_ticket_id not set
|
|
336
|
+
"is_key_account": False, # False should not display
|
|
337
|
+
# is_seller_in_golden_list not set
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Create the message
|
|
342
|
+
message = SlackMessageIncidentDeclaredAnnouncement(incident=incident)
|
|
343
|
+
|
|
344
|
+
# Get the blocks
|
|
345
|
+
blocks = message.get_blocks()
|
|
346
|
+
|
|
347
|
+
# Find the SectionBlock with fields
|
|
348
|
+
fields_block = None
|
|
349
|
+
for block in blocks:
|
|
350
|
+
if hasattr(block, "fields") and block.fields:
|
|
351
|
+
fields_block = block
|
|
352
|
+
break
|
|
353
|
+
|
|
354
|
+
assert fields_block is not None, "Should have a block with fields"
|
|
355
|
+
|
|
356
|
+
# Convert fields to strings (access .text attribute)
|
|
357
|
+
fields_text = " ".join(field.text if hasattr(field, "text") else str(field) for field in fields_block.fields)
|
|
358
|
+
|
|
359
|
+
# Verify only zendesk_ticket_id is present
|
|
360
|
+
assert "Zendesk Ticket" in fields_text
|
|
361
|
+
assert "12345" in fields_text
|
|
362
|
+
|
|
363
|
+
# Verify others are NOT present
|
|
364
|
+
assert "Seller Contract" not in fields_text
|
|
365
|
+
assert "Zoho Desk Ticket" not in fields_text
|
|
366
|
+
assert "Key Account" not in fields_text
|
|
367
|
+
assert "Golden List Seller" not in fields_text
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Fixtures for unified modal tests."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from firefighter.incidents.models import Environment, Priority
|
|
7
|
+
from firefighter.incidents.models.impact import ImpactLevel, ImpactType, LevelChoices
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def priority_factory(db):
|
|
12
|
+
"""Factory to create Priority instances."""
|
|
13
|
+
|
|
14
|
+
def _create(**kwargs):
|
|
15
|
+
value = kwargs.get("value", 1)
|
|
16
|
+
name = kwargs.get("name", f"P{value}")
|
|
17
|
+
set_as_default = kwargs.get("default", False)
|
|
18
|
+
|
|
19
|
+
# If default=True, clear any other defaults first
|
|
20
|
+
if set_as_default:
|
|
21
|
+
Priority.objects.filter(default=True).update(default=False)
|
|
22
|
+
|
|
23
|
+
defaults = {
|
|
24
|
+
"emoji": "🔴",
|
|
25
|
+
"order": value,
|
|
26
|
+
"default": set_as_default,
|
|
27
|
+
"enabled_create": True,
|
|
28
|
+
"enabled_update": True,
|
|
29
|
+
"needs_postmortem": value <= 2, # P1-P2 need postmortem
|
|
30
|
+
}
|
|
31
|
+
# Remove name and value from kwargs if present
|
|
32
|
+
kwargs_copy = kwargs.copy()
|
|
33
|
+
kwargs_copy.pop("name", None)
|
|
34
|
+
kwargs_copy.pop("value", None)
|
|
35
|
+
defaults.update(kwargs_copy)
|
|
36
|
+
|
|
37
|
+
priority, created = Priority.objects.get_or_create(
|
|
38
|
+
name=name,
|
|
39
|
+
value=value,
|
|
40
|
+
defaults=defaults,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# If already exists and we want it as default, just set that
|
|
44
|
+
if not created and set_as_default:
|
|
45
|
+
priority.default = True
|
|
46
|
+
priority.save(update_fields=["default"])
|
|
47
|
+
|
|
48
|
+
return priority
|
|
49
|
+
|
|
50
|
+
return _create
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def environment_factory(db):
|
|
55
|
+
"""Factory to create Environment instances."""
|
|
56
|
+
|
|
57
|
+
def _create(**kwargs):
|
|
58
|
+
value = kwargs.get("value", "TST")
|
|
59
|
+
set_as_default = kwargs.get("default", False)
|
|
60
|
+
|
|
61
|
+
# If default=True, clear any other defaults first
|
|
62
|
+
if set_as_default:
|
|
63
|
+
Environment.objects.filter(default=True).update(default=False)
|
|
64
|
+
|
|
65
|
+
defaults = {
|
|
66
|
+
"description": f"Environment {value}",
|
|
67
|
+
"order": 1,
|
|
68
|
+
"default": set_as_default,
|
|
69
|
+
}
|
|
70
|
+
# Remove value and default from kwargs if present
|
|
71
|
+
kwargs_copy = kwargs.copy()
|
|
72
|
+
kwargs_copy.pop("value", None)
|
|
73
|
+
kwargs_copy.pop("default", None)
|
|
74
|
+
defaults.update(kwargs_copy)
|
|
75
|
+
|
|
76
|
+
environment, created = Environment.objects.get_or_create(
|
|
77
|
+
value=value,
|
|
78
|
+
defaults=defaults,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# If already exists and we want it as default, just set that
|
|
82
|
+
if not created and set_as_default:
|
|
83
|
+
environment.default = True
|
|
84
|
+
environment.save(update_fields=["default"])
|
|
85
|
+
|
|
86
|
+
return environment
|
|
87
|
+
|
|
88
|
+
return _create
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.fixture
|
|
92
|
+
def impact_level_factory(db):
|
|
93
|
+
"""Factory to create ImpactLevel instances."""
|
|
94
|
+
|
|
95
|
+
def _create(**kwargs):
|
|
96
|
+
# Handle impact__name syntax by extracting nested parameters
|
|
97
|
+
impact_type_data = {}
|
|
98
|
+
keys_to_remove = []
|
|
99
|
+
for key in list(kwargs.keys()):
|
|
100
|
+
if key.startswith("impact__"):
|
|
101
|
+
nested_key = key.split("__", 1)[1]
|
|
102
|
+
impact_type_data[nested_key] = kwargs[key]
|
|
103
|
+
keys_to_remove.append(key)
|
|
104
|
+
for key in keys_to_remove:
|
|
105
|
+
kwargs.pop(key)
|
|
106
|
+
|
|
107
|
+
# Create or get ImpactType
|
|
108
|
+
impact_type = kwargs.pop("impact_type", None)
|
|
109
|
+
if isinstance(impact_type, ImpactType):
|
|
110
|
+
pass # Already have ImpactType instance
|
|
111
|
+
elif impact_type_data:
|
|
112
|
+
impact_type_name = impact_type_data.get("name", "Test Impact")
|
|
113
|
+
impact_type, _ = ImpactType.objects.get_or_create(name=impact_type_name, defaults={
|
|
114
|
+
"emoji": "📊",
|
|
115
|
+
"help_text": f"Test {impact_type_name} impact",
|
|
116
|
+
"value": impact_type_name.lower().replace(" ", "_"),
|
|
117
|
+
"order": 10,
|
|
118
|
+
})
|
|
119
|
+
else:
|
|
120
|
+
impact_type_name = "Test Impact"
|
|
121
|
+
impact_type, _ = ImpactType.objects.get_or_create(name=impact_type_name, defaults={
|
|
122
|
+
"emoji": "📊",
|
|
123
|
+
"help_text": "Test impact",
|
|
124
|
+
"value": "test_impact",
|
|
125
|
+
"order": 10,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
# Handle value parameter
|
|
129
|
+
value = kwargs.pop("value", LevelChoices.LOW)
|
|
130
|
+
|
|
131
|
+
defaults = {
|
|
132
|
+
"impact_type": impact_type,
|
|
133
|
+
"value": value,
|
|
134
|
+
"name": value.label if hasattr(value, "label") else "Test Level",
|
|
135
|
+
"emoji": "📊",
|
|
136
|
+
}
|
|
137
|
+
defaults.update(kwargs)
|
|
138
|
+
return ImpactLevel.objects.create(**defaults)
|
|
139
|
+
|
|
140
|
+
return _create
|
|
@@ -27,7 +27,7 @@ class TestCloseModal:
|
|
|
27
27
|
new_callable=PropertyMock(return_value=(True, [])),
|
|
28
28
|
)
|
|
29
29
|
incident = IncidentFactory.build()
|
|
30
|
-
incident.status = IncidentStatus.
|
|
30
|
+
incident.status = IncidentStatus.MITIGATED
|
|
31
31
|
|
|
32
32
|
# Act
|
|
33
33
|
res = modal.build_modal_fn(incident=incident, body={})
|
|
@@ -54,7 +54,7 @@ class TestCloseModal:
|
|
|
54
54
|
return_value=(True, []),
|
|
55
55
|
new_callable=mocker.PropertyMock,
|
|
56
56
|
)
|
|
57
|
-
incident.status = IncidentStatus.
|
|
57
|
+
incident.status = IncidentStatus.MITIGATED
|
|
58
58
|
incident.title = "This is the title"
|
|
59
59
|
incident.description = "This is the description"
|
|
60
60
|
|
|
@@ -72,7 +72,8 @@ class TestCloseModal:
|
|
|
72
72
|
def test_close_modal_build_cant_close(incident: Incident) -> None:
|
|
73
73
|
# Arrange
|
|
74
74
|
modal = CloseModal()
|
|
75
|
-
|
|
75
|
+
# Use MITIGATING (Mitigating) status - cannot close from this status without going through MITIGATED
|
|
76
|
+
incident.status = IncidentStatus.MITIGATING
|
|
76
77
|
|
|
77
78
|
# Act
|
|
78
79
|
res = modal.build_modal_fn(incident=incident, body={})
|
|
@@ -90,6 +91,67 @@ class TestCloseModal:
|
|
|
90
91
|
"This incident can't be closed yet." in values["blocks"][0]["text"]["text"]
|
|
91
92
|
)
|
|
92
93
|
|
|
94
|
+
@staticmethod
|
|
95
|
+
def test_close_modal_build_shows_mitigated_status_requirement(
|
|
96
|
+
mocker: MockerFixture, incident: Incident
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Test that the modal shows STATUS_NOT_MITIGATED error with proper references.
|
|
99
|
+
|
|
100
|
+
This test covers lines 151, 154, 159 in close.py where MITIGATED.label is used.
|
|
101
|
+
"""
|
|
102
|
+
# Arrange
|
|
103
|
+
modal = CloseModal()
|
|
104
|
+
incident.status = IncidentStatus.INVESTIGATING
|
|
105
|
+
|
|
106
|
+
# Mock requires_closure_reason to return False so we bypass the closure reason modal
|
|
107
|
+
mocker.patch(
|
|
108
|
+
"firefighter.slack.views.modals.utils.UpdateStatusForm.requires_closure_reason",
|
|
109
|
+
return_value=False
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Mock can_be_closed to return False with STATUS_NOT_MITIGATED reason
|
|
113
|
+
mocker.patch.object(
|
|
114
|
+
Incident,
|
|
115
|
+
"can_be_closed",
|
|
116
|
+
new_callable=PropertyMock(return_value=(False, [("STATUS_NOT_MITIGATED", "Status not mitigated")])),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Act
|
|
120
|
+
res = modal.build_modal_fn(incident=incident, body={})
|
|
121
|
+
|
|
122
|
+
# Assert
|
|
123
|
+
assert res.to_dict()
|
|
124
|
+
values = res.to_dict()
|
|
125
|
+
assert "blocks" in values
|
|
126
|
+
|
|
127
|
+
# Convert blocks to text for easier searching
|
|
128
|
+
blocks_text = str(values["blocks"])
|
|
129
|
+
|
|
130
|
+
# Verify that "Mitigated" (the label) appears in the error message
|
|
131
|
+
# Line 154: text=f":warning: *Status is not _{IncidentStatus.MITIGATED.label}_* :warning:\n"
|
|
132
|
+
# Line 159: text=f"You can only close an incident when its status is _{IncidentStatus.MITIGATED.label}_ or _{IncidentStatus.POST_MORTEM.label}_..."
|
|
133
|
+
assert "Mitigated" in blocks_text
|
|
134
|
+
assert "Post-mortem" in blocks_text or "Post Mortem" in blocks_text
|
|
135
|
+
|
|
136
|
+
# Verify the error message structure
|
|
137
|
+
assert "This incident can't be closed yet." in values["blocks"][0]["text"]["text"]
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def test_close_modal_build_shows_closure_reason_from_open(incident: Incident) -> None:
|
|
141
|
+
# Arrange
|
|
142
|
+
modal = CloseModal()
|
|
143
|
+
incident.status = IncidentStatus.OPEN
|
|
144
|
+
|
|
145
|
+
# Act
|
|
146
|
+
res = modal.build_modal_fn(incident=incident, body={})
|
|
147
|
+
|
|
148
|
+
# Assert
|
|
149
|
+
assert res.to_dict()
|
|
150
|
+
values = res.to_dict()
|
|
151
|
+
assert "blocks" in values
|
|
152
|
+
# Should show closure reason form
|
|
153
|
+
assert "Closure Reason Required" in values["blocks"][0]["text"]["text"]
|
|
154
|
+
|
|
93
155
|
@staticmethod
|
|
94
156
|
def test_submit_empty_bodied_form() -> None:
|
|
95
157
|
modal = CloseModal()
|