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.
Files changed (50) hide show
  1. firefighter/_version.py +2 -2
  2. firefighter/api/serializers.py +18 -0
  3. firefighter/api/views/incidents.py +3 -0
  4. firefighter/confluence/models.py +66 -6
  5. firefighter/confluence/signals/incident_updated.py +8 -26
  6. firefighter/firefighter/settings/components/jira_app.py +33 -0
  7. firefighter/incidents/admin.py +3 -0
  8. firefighter/incidents/models/impact.py +3 -5
  9. firefighter/incidents/models/incident.py +24 -9
  10. firefighter/incidents/views/views.py +2 -0
  11. firefighter/jira_app/admin.py +15 -1
  12. firefighter/jira_app/apps.py +3 -0
  13. firefighter/jira_app/client.py +151 -3
  14. firefighter/jira_app/management/__init__.py +1 -0
  15. firefighter/jira_app/management/commands/__init__.py +1 -0
  16. firefighter/jira_app/migrations/0002_add_jira_postmortem_model.py +71 -0
  17. firefighter/jira_app/models.py +50 -0
  18. firefighter/jira_app/service_postmortem.py +292 -0
  19. firefighter/jira_app/signals/__init__.py +10 -0
  20. firefighter/jira_app/signals/incident_key_events_updated.py +88 -0
  21. firefighter/jira_app/signals/postmortem_created.py +155 -0
  22. firefighter/jira_app/templates/jira/postmortem/impact.txt +12 -0
  23. firefighter/jira_app/templates/jira/postmortem/incident_summary.txt +17 -0
  24. firefighter/jira_app/templates/jira/postmortem/mitigation_actions.txt +9 -0
  25. firefighter/jira_app/templates/jira/postmortem/root_causes.txt +12 -0
  26. firefighter/jira_app/templates/jira/postmortem/timeline.txt +7 -0
  27. firefighter/raid/signals/incident_updated.py +31 -11
  28. firefighter/slack/messages/slack_messages.py +39 -3
  29. firefighter/slack/signals/postmortem_created.py +51 -3
  30. firefighter/slack/views/modals/closure_reason.py +15 -0
  31. firefighter/slack/views/modals/key_event_message.py +9 -0
  32. firefighter/slack/views/modals/postmortem.py +32 -40
  33. firefighter/slack/views/modals/update_status.py +7 -1
  34. {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/METADATA +1 -1
  35. {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/RECORD +50 -31
  36. {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/WHEEL +1 -1
  37. firefighter_tests/test_api/test_renderer.py +41 -0
  38. firefighter_tests/test_incidents/test_models/test_incident_model.py +29 -0
  39. firefighter_tests/test_jira_app/test_incident_key_events_sync.py +112 -0
  40. firefighter_tests/test_jira_app/test_models.py +138 -0
  41. firefighter_tests/test_jira_app/test_postmortem_issue_link.py +201 -0
  42. firefighter_tests/test_jira_app/test_postmortem_service.py +416 -0
  43. firefighter_tests/test_jira_app/test_timeline_template.py +135 -0
  44. firefighter_tests/test_raid/test_raid_signals.py +50 -8
  45. firefighter_tests/test_slack/messages/test_slack_messages.py +112 -23
  46. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +18 -2
  47. firefighter_tests/test_slack/views/modals/test_key_event_message.py +30 -0
  48. firefighter_tests/test_slack/views/modals/test_update_status.py +161 -129
  49. {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/entry_points.txt +0 -0
  50. {firefighter_incident-0.0.22.dist-info → firefighter_incident-0.0.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,201 @@
1
+ """Tests for Jira post-mortem issue link creation with robust error handling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ from jira import exceptions as jira_exceptions
8
+
9
+ from firefighter.jira_app.client import JiraClient
10
+
11
+
12
+ class TestPostmortemIssueLink:
13
+ """Test robust issue link creation between incident and post-mortem."""
14
+
15
+ @staticmethod
16
+ @patch("firefighter.jira_app.client.JIRA")
17
+ def test_create_issue_link_success_first_try(mock_jira_class: MagicMock) -> None:
18
+ """Test that issue link is created successfully on first try with 'Relates' type."""
19
+ # Setup mock
20
+ mock_jira_instance = MagicMock()
21
+ mock_jira_class.return_value = mock_jira_instance
22
+
23
+ # Mock issue validation - both issues exist
24
+ mock_parent_issue = MagicMock()
25
+ mock_postmortem_issue = MagicMock()
26
+ mock_jira_instance.issue.side_effect = [
27
+ mock_parent_issue,
28
+ mock_postmortem_issue,
29
+ ]
30
+
31
+ # Mock successful link creation
32
+ mock_jira_instance.create_issue_link.return_value = None
33
+
34
+ # Create client and call the method
35
+ client = JiraClient()
36
+ client._create_issue_link_safe(
37
+ parent_issue_key="INCIDENT-123", postmortem_issue_key="PM-456"
38
+ )
39
+
40
+ # Verify link was created with 'Relates' type
41
+ mock_jira_instance.create_issue_link.assert_called_once_with(
42
+ type="Relates",
43
+ inwardIssue="INCIDENT-123",
44
+ outwardIssue="PM-456",
45
+ comment={"body": "Post-mortem PM-456 created for incident INCIDENT-123"},
46
+ )
47
+
48
+ @staticmethod
49
+ @patch("firefighter.jira_app.client.JIRA")
50
+ def test_create_issue_link_fallback_to_blocks(mock_jira_class: MagicMock) -> None:
51
+ """Test that issue link falls back to 'Blocks' type when 'Relates' fails."""
52
+ # Setup mock
53
+ mock_jira_instance = MagicMock()
54
+ mock_jira_class.return_value = mock_jira_instance
55
+
56
+ # Mock issue validation - both issues exist (called multiple times)
57
+ mock_issue = MagicMock()
58
+ mock_jira_instance.issue.return_value = mock_issue
59
+
60
+ # Mock link creation: first attempt fails, second succeeds
61
+ mock_jira_instance.create_issue_link.side_effect = [
62
+ jira_exceptions.JIRAError(
63
+ status_code=400, text="Link type 'Relates' not found"
64
+ ),
65
+ None, # Second attempt succeeds
66
+ ]
67
+
68
+ # Create client and call the method
69
+ client = JiraClient()
70
+ client._create_issue_link_safe(
71
+ parent_issue_key="INCIDENT-123", postmortem_issue_key="PM-456"
72
+ )
73
+
74
+ # Verify it tried 'Relates' first, then 'Blocks'
75
+ assert mock_jira_instance.create_issue_link.call_count == 2
76
+ first_call = mock_jira_instance.create_issue_link.call_args_list[0]
77
+ second_call = mock_jira_instance.create_issue_link.call_args_list[1]
78
+ assert first_call.kwargs["type"] == "Relates"
79
+ assert second_call.kwargs["type"] == "Blocks"
80
+
81
+ @staticmethod
82
+ @patch("firefighter.jira_app.client.JIRA")
83
+ def test_create_issue_link_all_types_fail(mock_jira_class: MagicMock) -> None:
84
+ """Test that method handles gracefully when all link types fail."""
85
+ # Setup mock
86
+ mock_jira_instance = MagicMock()
87
+ mock_jira_class.return_value = mock_jira_instance
88
+
89
+ # Mock issue validation - both issues exist
90
+ mock_issue = MagicMock()
91
+ mock_jira_instance.issue.return_value = mock_issue
92
+
93
+ # Mock link creation: all attempts fail
94
+ mock_jira_instance.create_issue_link.side_effect = jira_exceptions.JIRAError(
95
+ status_code=400, text="Link type not found"
96
+ )
97
+
98
+ # Create client and call the method - should not raise exception
99
+ client = JiraClient()
100
+ client._create_issue_link_safe(
101
+ parent_issue_key="INCIDENT-123", postmortem_issue_key="PM-456"
102
+ )
103
+
104
+ # Verify it tried all 3 link types
105
+ assert mock_jira_instance.create_issue_link.call_count == 3
106
+
107
+ @staticmethod
108
+ @patch("firefighter.jira_app.client.JIRA")
109
+ def test_create_issue_link_parent_not_found(mock_jira_class: MagicMock) -> None:
110
+ """Test that method handles gracefully when parent issue doesn't exist."""
111
+ # Setup mock
112
+ mock_jira_instance = MagicMock()
113
+ mock_jira_class.return_value = mock_jira_instance
114
+
115
+ # Mock issue validation: parent issue not found
116
+ mock_jira_instance.issue.side_effect = jira_exceptions.JIRAError(
117
+ status_code=404, text="Issue not found"
118
+ )
119
+
120
+ # Create client and call the method - should not raise exception
121
+ client = JiraClient()
122
+ client._create_issue_link_safe(
123
+ parent_issue_key="INCIDENT-999", postmortem_issue_key="PM-456"
124
+ )
125
+
126
+ # Verify link creation was not attempted
127
+ mock_jira_instance.create_issue_link.assert_not_called()
128
+
129
+ @staticmethod
130
+ @patch("firefighter.jira_app.client.JIRA")
131
+ def test_create_postmortem_issue_with_link(mock_jira_class: MagicMock) -> None:
132
+ """Test that post-mortem issue is created and linked successfully."""
133
+ # Setup mock
134
+ mock_jira_instance = MagicMock()
135
+ mock_jira_class.return_value = mock_jira_instance
136
+
137
+ # Mock issue creation
138
+ mock_created_issue = MagicMock()
139
+ mock_created_issue.key = "PM-789"
140
+ mock_created_issue.id = "12345"
141
+ mock_jira_instance.create_issue.return_value = mock_created_issue
142
+
143
+ # Mock issue validation
144
+ mock_issue = MagicMock()
145
+ mock_jira_instance.issue.return_value = mock_issue
146
+
147
+ # Mock link creation
148
+ mock_jira_instance.create_issue_link.return_value = None
149
+
150
+ # Create client and call the method
151
+ client = JiraClient()
152
+ result = client.create_postmortem_issue(
153
+ project_key="PM",
154
+ issue_type="Post-mortem",
155
+ fields={"summary": "Test post-mortem"},
156
+ parent_issue_key="INCIDENT-123",
157
+ )
158
+
159
+ # Verify issue was created
160
+ mock_jira_instance.create_issue.assert_called_once()
161
+ assert result == {"key": "PM-789", "id": "12345"}
162
+
163
+ # Verify link was created
164
+ mock_jira_instance.create_issue_link.assert_called_once()
165
+
166
+ @staticmethod
167
+ @patch("firefighter.jira_app.client.JIRA")
168
+ def test_create_postmortem_issue_link_fails_but_issue_created(
169
+ mock_jira_class: MagicMock,
170
+ ) -> None:
171
+ """Test that post-mortem issue is still created even if linking fails."""
172
+ # Setup mock
173
+ mock_jira_instance = MagicMock()
174
+ mock_jira_class.return_value = mock_jira_instance
175
+
176
+ # Mock issue creation
177
+ mock_created_issue = MagicMock()
178
+ mock_created_issue.key = "PM-789"
179
+ mock_created_issue.id = "12345"
180
+ mock_jira_instance.create_issue.return_value = mock_created_issue
181
+
182
+ # Mock issue validation - parent doesn't exist
183
+ mock_jira_instance.issue.side_effect = jira_exceptions.JIRAError(
184
+ status_code=404, text="Issue not found"
185
+ )
186
+
187
+ # Create client and call the method
188
+ client = JiraClient()
189
+ result = client.create_postmortem_issue(
190
+ project_key="PM",
191
+ issue_type="Post-mortem",
192
+ fields={"summary": "Test post-mortem"},
193
+ parent_issue_key="INCIDENT-999",
194
+ )
195
+
196
+ # Verify issue was created successfully despite link failure
197
+ mock_jira_instance.create_issue.assert_called_once()
198
+ assert result == {"key": "PM-789", "id": "12345"}
199
+
200
+ # Verify link creation was attempted but failed gracefully
201
+ mock_jira_instance.create_issue_link.assert_not_called()
@@ -0,0 +1,416 @@
1
+ """Tests for Jira post-mortem service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from typing import TYPE_CHECKING
7
+ from unittest.mock import MagicMock, patch
8
+
9
+ import pytest
10
+ from django.utils import timezone
11
+
12
+ from firefighter.incidents.enums import IncidentStatus
13
+ from firefighter.incidents.factories import IncidentFactory, UserFactory
14
+ from firefighter.incidents.models.incident_membership import IncidentRole
15
+ from firefighter.incidents.models.incident_role_type import IncidentRoleType
16
+ from firefighter.incidents.models.incident_update import IncidentUpdate
17
+ from firefighter.jira_app.client import JiraUser
18
+ from firefighter.jira_app.models import JiraUser as JiraUserDB
19
+ from firefighter.jira_app.service_postmortem import JiraPostMortemService
20
+ from firefighter.raid.models import JiraTicket
21
+
22
+ if TYPE_CHECKING:
23
+ from firefighter.incidents.models.incident import Incident
24
+ from firefighter.incidents.models.user import User
25
+
26
+
27
+ @pytest.mark.django_db
28
+ class TestJiraPostMortemService:
29
+ """Test Jira post-mortem service."""
30
+
31
+ @staticmethod
32
+ def test_incident_summary_excludes_status_and_created() -> None:
33
+ """Test that incident summary does not include Status and Created fields."""
34
+ # Create a user
35
+ user: User = UserFactory.create()
36
+
37
+ # Create an incident with some updates
38
+ incident: Incident = IncidentFactory.create(
39
+ _status=IncidentStatus.POST_MORTEM,
40
+ created_by=user,
41
+ )
42
+
43
+ # Generate the incident summary
44
+ service = JiraPostMortemService()
45
+ fields = service._generate_issue_fields(incident)
46
+
47
+ incident_summary = fields[service.field_ids["incident_summary"]]
48
+
49
+ # Verify that Status and Created are NOT in the summary
50
+ assert "Status:" not in incident_summary
51
+ assert "Created:" not in incident_summary
52
+
53
+ # Verify that required fields ARE present
54
+ assert "Incident Summary" in incident_summary
55
+ assert "Incident:" in incident_summary
56
+ assert "Priority:" in incident_summary
57
+
58
+ @staticmethod
59
+ def test_timeline_includes_status_changes() -> None:
60
+ """Test that timeline includes incident status changes."""
61
+ # Create a user
62
+ user: User = UserFactory.create()
63
+
64
+ # Create an incident
65
+ incident: Incident = IncidentFactory.create(
66
+ _status=IncidentStatus.OPEN,
67
+ created_by=user,
68
+ )
69
+
70
+ # Create status updates
71
+ now = timezone.now()
72
+
73
+ # Status change to INVESTIGATING
74
+ IncidentUpdate.objects.create(
75
+ incident=incident,
76
+ status=IncidentStatus.INVESTIGATING,
77
+ event_ts=now,
78
+ created_by=user,
79
+ )
80
+
81
+ # Status change to MITIGATING
82
+ IncidentUpdate.objects.create(
83
+ incident=incident,
84
+ status=IncidentStatus.MITIGATING,
85
+ event_ts=now,
86
+ created_by=user,
87
+ )
88
+
89
+ # Status change to MITIGATED
90
+ IncidentUpdate.objects.create(
91
+ incident=incident,
92
+ status=IncidentStatus.MITIGATED,
93
+ event_ts=now,
94
+ created_by=user,
95
+ )
96
+
97
+ # Generate the timeline
98
+ service = JiraPostMortemService()
99
+ fields = service._generate_issue_fields(incident)
100
+
101
+ timeline = fields[service.field_ids["timeline"]]
102
+
103
+ # Verify that status changes are in the timeline
104
+ assert "Status changed to: Investigating" in timeline
105
+ assert "Status changed to: Mitigating" in timeline
106
+ assert "Status changed to: Mitigated" in timeline
107
+
108
+ # Verify the initial creation event is present
109
+ assert "Incident created" in timeline
110
+
111
+ @staticmethod
112
+ def test_generate_issue_fields_sets_due_date() -> None:
113
+ user: User = UserFactory.create()
114
+ incident: Incident = IncidentFactory.create(
115
+ _status=IncidentStatus.OPEN,
116
+ created_by=user,
117
+ )
118
+ incident.created_at = datetime(2024, 1, 1, tzinfo=UTC)
119
+ incident.save(update_fields=["created_at"])
120
+
121
+ service = JiraPostMortemService()
122
+ fields = service._generate_issue_fields(incident)
123
+
124
+ expected_due = (
125
+ service._add_business_days(incident.created_at, 40).date().isoformat()
126
+ )
127
+ assert fields["duedate"] == expected_due
128
+
129
+ @staticmethod
130
+ @patch("firefighter.jira_app.service_postmortem.JiraClient")
131
+ def test_create_postmortem_prefetches_updates(
132
+ mock_jira_client: MagicMock,
133
+ ) -> None:
134
+ """Test that creating post-mortem prefetches incident updates."""
135
+ # Mock Jira client responses
136
+ mock_client_instance = MagicMock()
137
+ mock_client_instance.create_postmortem_issue.return_value = {
138
+ "id": "12345",
139
+ "key": "TEST-123",
140
+ }
141
+ mock_jira_client.return_value = mock_client_instance
142
+
143
+ # Create a user
144
+ user: User = UserFactory.create()
145
+
146
+ # Create an incident with status updates
147
+ incident: Incident = IncidentFactory.create(
148
+ _status=IncidentStatus.POST_MORTEM,
149
+ created_by=user,
150
+ )
151
+
152
+ now = timezone.now()
153
+ IncidentUpdate.objects.create(
154
+ incident=incident,
155
+ status=IncidentStatus.INVESTIGATING,
156
+ event_ts=now,
157
+ created_by=user,
158
+ )
159
+
160
+ # Create post-mortem
161
+ service = JiraPostMortemService()
162
+ jira_pm = service.create_postmortem_for_incident(incident, created_by=user)
163
+
164
+ # Verify post-mortem was created
165
+ assert jira_pm is not None
166
+ assert jira_pm.jira_issue_key == "TEST-123"
167
+ assert jira_pm.incident == incident
168
+
169
+ # Verify Jira client was called with correct fields
170
+ mock_client_instance.create_postmortem_issue.assert_called_once()
171
+ call_kwargs = mock_client_instance.create_postmortem_issue.call_args.kwargs
172
+ assert "fields" in call_kwargs
173
+
174
+ # Verify timeline contains status change
175
+ timeline = call_kwargs["fields"][service.field_ids["timeline"]]
176
+ assert "Status changed to: Investigating" in timeline
177
+
178
+ @staticmethod
179
+ @patch("firefighter.jira_app.service_postmortem.JiraClient")
180
+ def test_create_postmortem_handles_assignment_failure_gracefully(
181
+ mock_jira_client: MagicMock,
182
+ ) -> None:
183
+ """Test that post-mortem creation succeeds even if assignment fails."""
184
+ # Mock Jira client responses
185
+ mock_client_instance = MagicMock()
186
+ mock_client_instance.create_postmortem_issue.return_value = {
187
+ "id": "12345",
188
+ "key": "TEST-123",
189
+ }
190
+ # Mock assignment failure - return False instead of raising exception
191
+ mock_client_instance.assign_issue.return_value = False
192
+ mock_jira_client.return_value = mock_client_instance
193
+
194
+ # Create a user
195
+ user: User = UserFactory.create()
196
+
197
+ # Create an incident
198
+ incident: Incident = IncidentFactory.create(
199
+ _status=IncidentStatus.POST_MORTEM,
200
+ created_by=user,
201
+ )
202
+
203
+ # Create post-mortem
204
+ service = JiraPostMortemService()
205
+ jira_pm = service.create_postmortem_for_incident(incident, created_by=user)
206
+
207
+ # Verify post-mortem was created successfully despite assignment failure
208
+ assert jira_pm is not None
209
+ assert jira_pm.jira_issue_key == "TEST-123"
210
+ assert jira_pm.incident == incident
211
+
212
+ # Verify Jira client create was called
213
+ mock_client_instance.create_postmortem_issue.assert_called_once()
214
+
215
+ # Verify assignment was not attempted (no commander role)
216
+ # or if attempted, it returned False without raising exception
217
+
218
+ @staticmethod
219
+ @pytest.mark.django_db
220
+ def test_assigns_commander_without_jira_user() -> None:
221
+ """Commander without Jira user should trigger lookup and assignment."""
222
+ service = JiraPostMortemService()
223
+ mock_client = MagicMock()
224
+ mock_client.create_postmortem_issue.return_value = {"id": "1", "key": "INC-1"}
225
+ mock_client.assign_issue.return_value = True
226
+ mock_client.get_jira_user_from_user.return_value = JiraUser(
227
+ id="acct-123", user=None
228
+ )
229
+ service.client = mock_client
230
+
231
+ user = UserFactory()
232
+ incident = IncidentFactory.create(
233
+ _status=IncidentStatus.POST_MORTEM,
234
+ created_by=user,
235
+ )
236
+ role_type, _ = IncidentRoleType.objects.get_or_create(
237
+ slug="commander",
238
+ defaults={
239
+ "name": "Commander",
240
+ "summary": "cmd",
241
+ "description": "Commander role",
242
+ },
243
+ )
244
+ IncidentRole.objects.create(incident=incident, user=user, role_type=role_type)
245
+
246
+ service.create_postmortem_for_incident(incident, created_by=user)
247
+
248
+ mock_client.get_jira_user_from_user.assert_called_once_with(user)
249
+ mock_client.assign_issue.assert_called_once_with(
250
+ issue_key="INC-1", account_id="acct-123"
251
+ )
252
+
253
+ @staticmethod
254
+ def test_replicate_custom_fields_all_present() -> None:
255
+ """Test that all custom fields are replicated when present."""
256
+ user: User = UserFactory.create()
257
+ incident: Incident = IncidentFactory.create(
258
+ _status=IncidentStatus.POST_MORTEM,
259
+ created_by=user,
260
+ custom_fields={
261
+ "zendesk_ticket_id": "12345",
262
+ "zoho_desk_ticket_id": "67890",
263
+ "seller_contract_id": "98765",
264
+ "platform": "platform-FR",
265
+ "environments": ["PRD", "STG"],
266
+ },
267
+ )
268
+
269
+ # Create a Jira user for reporter
270
+ jira_user = JiraUserDB.objects.create(
271
+ id="test-jira-user-id",
272
+ user=user,
273
+ )
274
+
275
+ # Create a Jira ticket with business_impact
276
+ JiraTicket.objects.create(
277
+ id=12345,
278
+ incident=incident,
279
+ key="TEST-123",
280
+ reporter=jira_user,
281
+ business_impact="High",
282
+ )
283
+ incident.refresh_from_db()
284
+
285
+ service = JiraPostMortemService()
286
+ fields = service._generate_issue_fields(incident)
287
+
288
+ # Verify Priority is replicated
289
+ assert "customfield_11064" in fields
290
+ assert fields["customfield_11064"] == {"value": str(incident.priority.value)}
291
+
292
+ # Verify Environments are replicated
293
+ assert "customfield_11049" in fields
294
+ assert fields["customfield_11049"] == [{"value": "PRD"}, {"value": "STG"}]
295
+
296
+ # Verify Zendesk ticket is replicated
297
+ assert "customfield_10895" in fields
298
+ assert fields["customfield_10895"] == "12345"
299
+
300
+ # Verify Zoho desk ticket is replicated
301
+ assert "customfield_10896" in fields
302
+ assert fields["customfield_10896"] == "67890"
303
+
304
+ # Verify Seller Contract ID is replicated
305
+ assert "customfield_10908" in fields
306
+ assert fields["customfield_10908"] == "98765"
307
+
308
+ # Verify Platform is replicated (without "platform-" prefix)
309
+ assert "customfield_10201" in fields
310
+ assert fields["customfield_10201"] == {"value": "FR"}
311
+
312
+ # Verify Business Impact is replicated
313
+ assert "customfield_10936" in fields
314
+ assert fields["customfield_10936"] == {"value": "High"}
315
+
316
+ @staticmethod
317
+ def test_replicate_custom_fields_empty_not_sent() -> None:
318
+ """Test that empty custom fields are not sent to Jira."""
319
+ user: User = UserFactory.create()
320
+ incident: Incident = IncidentFactory.create(
321
+ _status=IncidentStatus.POST_MORTEM,
322
+ created_by=user,
323
+ custom_fields={}, # No custom fields
324
+ )
325
+
326
+ service = JiraPostMortemService()
327
+ fields = service._generate_issue_fields(incident)
328
+
329
+ # Priority should always be present
330
+ assert "customfield_11064" in fields
331
+
332
+ # Other fields should not be present when empty
333
+ assert "customfield_11049" not in fields # Environments
334
+ assert "customfield_10895" not in fields # Zendesk
335
+ assert "customfield_10896" not in fields # Zoho
336
+ assert "customfield_10908" not in fields # Seller
337
+ assert "customfield_10201" not in fields # Platform
338
+ assert "customfield_10936" not in fields # Business Impact (no jira_ticket)
339
+
340
+ @staticmethod
341
+ def test_replicate_custom_fields_partial() -> None:
342
+ """Test that only present custom fields are replicated."""
343
+ user: User = UserFactory.create()
344
+ incident: Incident = IncidentFactory.create(
345
+ _status=IncidentStatus.POST_MORTEM,
346
+ created_by=user,
347
+ custom_fields={
348
+ "zendesk_ticket_id": "12345",
349
+ "environments": ["PRD"],
350
+ # seller_contract_id and zoho_desk_ticket_id are missing
351
+ },
352
+ )
353
+
354
+ service = JiraPostMortemService()
355
+ fields = service._generate_issue_fields(incident)
356
+
357
+ # Present fields should be replicated
358
+ assert "customfield_10895" in fields # Zendesk
359
+ assert fields["customfield_10895"] == "12345"
360
+ assert "customfield_11049" in fields # Environments
361
+ assert fields["customfield_11049"] == [{"value": "PRD"}]
362
+
363
+ # Missing fields should not be present
364
+ assert "customfield_10896" not in fields # Zoho
365
+ assert "customfield_10908" not in fields # Seller
366
+ assert "customfield_10201" not in fields # Platform
367
+
368
+ @staticmethod
369
+ def test_business_impact_not_replicated_when_na() -> None:
370
+ """Test that business_impact is not replicated when it's 'N/A' or empty."""
371
+ user: User = UserFactory.create()
372
+ incident: Incident = IncidentFactory.create(
373
+ _status=IncidentStatus.POST_MORTEM,
374
+ created_by=user,
375
+ )
376
+
377
+ # Create a Jira user for reporter
378
+ jira_user = JiraUserDB.objects.create(
379
+ id="test-jira-user-id-2",
380
+ user=user,
381
+ )
382
+
383
+ # Create a Jira ticket with business_impact = "N/A"
384
+ JiraTicket.objects.create(
385
+ id=12346,
386
+ incident=incident,
387
+ key="TEST-123",
388
+ reporter=jira_user,
389
+ business_impact="N/A",
390
+ )
391
+ incident.refresh_from_db()
392
+
393
+ service = JiraPostMortemService()
394
+ fields = service._generate_issue_fields(incident)
395
+
396
+ # Business Impact should not be present when "N/A"
397
+ assert "customfield_10936" not in fields
398
+
399
+ @staticmethod
400
+ def test_platform_prefix_removal() -> None:
401
+ """Test that 'platform-' prefix is removed from platform value."""
402
+ user: User = UserFactory.create()
403
+ incident: Incident = IncidentFactory.create(
404
+ _status=IncidentStatus.POST_MORTEM,
405
+ created_by=user,
406
+ custom_fields={
407
+ "platform": "platform-DE",
408
+ },
409
+ )
410
+
411
+ service = JiraPostMortemService()
412
+ fields = service._generate_issue_fields(incident)
413
+
414
+ # Verify platform prefix is removed
415
+ assert "customfield_10201" in fields
416
+ assert fields["customfield_10201"] == {"value": "DE"}