firefighter-incident 0.0.18__py3-none-any.whl → 0.0.19__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 CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.18'
32
- __version_tuple__ = version_tuple = (0, 0, 18)
31
+ __version__ = version = '0.0.19'
32
+ __version_tuple__ = version_tuple = (0, 0, 19)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -72,6 +72,7 @@ class RaidJiraClient(JiraClient):
72
72
  extra_args["customfield_10896"] = str(zoho_desk_ticket_id)
73
73
  if zendesk_ticket_id:
74
74
  extra_args["customfield_10895"] = str(zendesk_ticket_id)
75
+
75
76
  if seller_contract_id:
76
77
  description_addendum.append(
77
78
  f"Seller link to TOOLBOX: {TOOLBOX_URL}?seller_id={seller_contract_id}"
@@ -109,6 +110,7 @@ class RaidJiraClient(JiraClient):
109
110
  project = (
110
111
  feature_team.jira_project_key if feature_team else RAID_JIRA_PROJECT_KEY
111
112
  )
113
+
112
114
  issue = self.jira.create_issue(
113
115
  project=project,
114
116
  summary=summary,
firefighter/raid/forms.py CHANGED
@@ -107,8 +107,12 @@ def alert_slack_new_jira_ticket(
107
107
  reporter_user: User | None = None,
108
108
  reporter_email: str | None = None,
109
109
  ) -> None:
110
- # These alerts are not for critical incidents
111
- if hasattr(jira_ticket, "incident") and jira_ticket.incident:
110
+ # These alerts are for P4-P5 incidents only, not P1-P3 critical incidents
111
+ if (
112
+ hasattr(jira_ticket, "incident")
113
+ and jira_ticket.incident
114
+ and jira_ticket.incident.priority.value <= 3
115
+ ):
112
116
  raise ValueError("This is a critical incident, not a raid incident.")
113
117
 
114
118
  # Get the reporter's email and user from ticket if not provided
@@ -35,6 +35,7 @@ if TYPE_CHECKING:
35
35
  "description": "Description test where you want to depict your issue",
36
36
  "seller_contract_id": "12345678",
37
37
  "zoho": "https://crmplus.zoho.eu/mycrmlink/index.do/cxapp/agent/mycompany/all/tickets/details/123456789",
38
+ "zendesk": "12345",
38
39
  "platform": "FR",
39
40
  "reporter_email": "john.doe@mycompany.com",
40
41
  "incident_category": "Payment Processing",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firefighter-incident
3
- Version: 0.0.18
3
+ Version: 0.0.19
4
4
  Summary: Incident Management tool made for Slack using Django
5
5
  Project-URL: Repository, https://github.com/ManoManoTech/firefighter-incident
6
6
  Project-URL: Documentation, https://manomanotech.github.io/firefighter-incident/latest/
@@ -6,7 +6,7 @@ gunicorn.conf.py,sha256=vHsTGjaKOr8FDMp6fTKYTX4AtokmPgYvvt5Mr0Q6APc,273
6
6
  main.py,sha256=CsbprHoOYhjCLpTJmq9Z_aRYFoFgWxoz2pDLuwm8Eqg,1558
7
7
  manage.py,sha256=5ivHGD13C6nJ8QvltKsJ9T9akA5he8da70HLWaEP3k8,689
8
8
  firefighter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- firefighter/_version.py,sha256=tzG3REjXftgEO32YP2rgLirgULNff34B-C5Yb8oweEc,706
9
+ firefighter/_version.py,sha256=2t1q5JYEYkNwbHa29phdM-U8VWR1m6EwW48aFKWPj_w,706
10
10
  firefighter/api/__init__.py,sha256=JQW0Bv6xwGqy7ioxx3h6UGMzkkJ4DntDpbvV1Ncgi8k,136
11
11
  firefighter/api/admin.py,sha256=x9Ysy-GiYjb0rynmFdS9g56e6n24fkN0ouGy5QD9Yrc,4629
12
12
  firefighter/api/apps.py,sha256=P5uU1_gMrDfzurdMbfqw1Bnb2uNKKcMq17WBPg2sLhc,204
@@ -311,8 +311,8 @@ firefighter/pagerduty/views/oncall_trigger.py,sha256=LYHpWyEaR6O8NazmsTl5ydtw1XH
311
311
  firefighter/raid/__init__.py,sha256=nMNmvHCSkyLQsdhTow7myMU62vXk1e755gUntVfFFlY,154
312
312
  firefighter/raid/admin.py,sha256=WhIHaRAv7JPp2NH27w7_0JfvGHrvoyRJhYr3_WwedrA,1117
313
313
  firefighter/raid/apps.py,sha256=olDKua1rqhhIJUhCu6A2PnPWloW_jbeD4XWL94b2owo,1117
314
- firefighter/raid/client.py,sha256=ja_tiKAcBFpI5ZwbGxSVyf7_ETq2K1Qauq26pMR-Ze4,8124
315
- firefighter/raid/forms.py,sha256=gbbXDizCYDs8vz33ZFClTVZ_yM3jqZtWFMYRy3K3xuM,11863
314
+ firefighter/raid/client.py,sha256=5PzW7Ub0rU4Z_gdGqPB42CMcM5TCUoWeOZRZZmnhJXg,8126
315
+ firefighter/raid/forms.py,sha256=XivNADFHOl2ewNRXev17HthDIUvwt4pdH9t4So-BE-A,11968
316
316
  firefighter/raid/messages.py,sha256=e75kwi0hCe5ChwU4t-_6Q3Rcy22MLLdVSsYyjvG2SCM,5542
317
317
  firefighter/raid/models.py,sha256=29Smci739K1ZdcMu7uXYvoVEhgDpwLQoCzBbc5wvwhs,2211
318
318
  firefighter/raid/resources.py,sha256=39GhITs3OAWA1eSPZme-rLd818kuz7gwYzdN38zNz8Y,436
@@ -328,7 +328,7 @@ firefighter/raid/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
328
328
  firefighter/raid/signals/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
329
329
  firefighter/raid/signals/incident_updated.py,sha256=5LYeqvgTrfINu_SOwkZa3hD6rvTsl8BL8Py-immqK3I,1374
330
330
  firefighter/raid/tasks/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
331
- firefighter/raid/views/__init__.py,sha256=noK-M1X4L46OMAp6S29jqajovzL6sIMSnIBv5l9vxIM,5174
331
+ firefighter/raid/views/__init__.py,sha256=C3WhAJfEoUasi2afHPuLpKiuRYixK-tc3j0-2Rw_g3E,5210
332
332
  firefighter/slack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
333
333
  firefighter/slack/admin.py,sha256=pNJbA-szxUUrghxv_Z0BNezu6lULDzFcOu_K5i4m7Cs,13963
334
334
  firefighter/slack/apps.py,sha256=gR0zWTtqT58tjPayBX22ZSzMkLiNpmoOvLShNvhJA6Q,664
@@ -464,17 +464,20 @@ firefighter_tests/test_incidents/test_utils/test_date_utils.py,sha256=ogP7qOEwIt
464
464
  firefighter_tests/test_incidents/test_views/test_incident_detail_view.py,sha256=gKKFWIZVrD_P4p6DJjeHCW5uGXBUBVlCd95gJJYDpWQ,680
465
465
  firefighter_tests/test_incidents/test_views/test_index_view.py,sha256=InpxbaWOFwRn4YWeIKZhj17vMymrQQf2p2LFhe2Bcdw,816
466
466
  firefighter_tests/test_raid/conftest.py,sha256=i_TOquYIMLDyVQ97uqxTqPJszVz4qq7L_Q7YJxTuS1o,4090
467
- firefighter_tests/test_raid/test_raid_client.py,sha256=8iRBR4bKewPPbtz33hCLOHiZDTELPQBOrQb7EdNDS4c,21227
467
+ firefighter_tests/test_raid/test_raid_alert_p4_p5.py,sha256=rz9orbt1E1vJ5POQyVZ6-SEPvqB55-xhwIWHicdfgDg,9356
468
+ firefighter_tests/test_raid/test_raid_client.py,sha256=KTqELERpWno7XhF9LpabpxkHoJiWWrryUg5LHi5Yfjo,22456
468
469
  firefighter_tests/test_raid/test_raid_client_users.py,sha256=9uma1wBhaiCoG75XAZHqpT8oGTnqFJRMCi7a3XctNtM,3631
469
- firefighter_tests/test_raid/test_raid_forms.py,sha256=y89ZqP6Wf9h3S2hegJHVfVdCuVbfL4SvT-kPVoAYyy8,19618
470
+ firefighter_tests/test_raid/test_raid_forms.py,sha256=lLZvhczAge0Ftt3OdFEH2BcE1OMF6_vvy1ghHAe3HWM,19758
470
471
  firefighter_tests/test_raid/test_raid_models.py,sha256=nq-fVClB_P24W8WrZruOPt8wlHUVGYI7wxJR7tH6AnM,5042
471
- firefighter_tests/test_raid/test_raid_serializers.py,sha256=nejl4ceBPegOcks6z6B8_7Ekg8ef18j3i7C8Y8dS8v0,20665
472
+ firefighter_tests/test_raid/test_raid_serializers.py,sha256=xkMK9npvDMAr2pUpP4mFbLszrR6qyTzsJRd-uNOzEhM,22560
472
473
  firefighter_tests/test_raid/test_raid_service.py,sha256=AqVyrRjW2tr0sfbXS4lGlJ7mcxB2ACEXAR8Bv0pXnj0,16755
473
474
  firefighter_tests/test_raid/test_raid_signals.py,sha256=twNxB3NQs58s8ZcP-wsZXG7iTHR4yKFC9x-zpq1ZuAo,7002
474
475
  firefighter_tests/test_raid/test_raid_transitions.py,sha256=mtmMKwukxmZSM-R619BQ3Z_2AB-qY6imvDgUF0A3_tw,4784
475
476
  firefighter_tests/test_raid/test_raid_utils.py,sha256=i6JBwim1G-qynwxprNZekxl9K7Vis4FFvNkw3wT2jTM,1016
476
477
  firefighter_tests/test_raid/test_raid_views.py,sha256=paAhh4k2EDlmG1ehwNhMuYIhr1okqrvM7xlkaTAo2V0,6825
478
+ firefighter_tests/test_raid/test_zendesk_integration.py,sha256=RtHIKWog_YZ6g-uZSr37k5XjAGe0c8WEeBWqkt1ZNqk,7113
477
479
  firefighter_tests/test_slack/conftest.py,sha256=MCg04JFQ0iBeYUN_moEvvH4PVA3wE48Q6LkrIw7Bic0,2015
480
+ firefighter_tests/test_slack/test_conversation_tags.py,sha256=nNqTZRRBfF6Z4wpFSYZ83DH7_mR9z9F9LAHSq8AHFN0,12744
478
481
  firefighter_tests/test_slack/test_signals_downgrade.py,sha256=mgl4H5vwr2kImf6g4IZbhv7YEPmMzbYSaVr8E6taL88,5420
479
482
  firefighter_tests/test_slack/test_slack_utils.py,sha256=9PLobMNXh3xDyFuwzcQFpKJhe4j__sIgf_WRHIpANJw,3957
480
483
  firefighter_tests/test_slack/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -491,10 +494,10 @@ firefighter_tests/test_slack/views/modals/test_open.py,sha256=IzgG9le5NN_CvltehA
491
494
  firefighter_tests/test_slack/views/modals/test_opening_unified.py,sha256=OejtLyc_mehav2TDaLzUnhilMNvhCzc6T4FodCqfQPk,17406
492
495
  firefighter_tests/test_slack/views/modals/test_send_sos.py,sha256=_rE6jD-gOzcGyhlY0R9GzlGtPx65oOOguJYdENgxtLc,1289
493
496
  firefighter_tests/test_slack/views/modals/test_status.py,sha256=oQzPfwdg2tkbo9nfkO1GfS3WydxqSC6vy1AZjZDKT30,2226
494
- firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=aqLQ_9TV4Zn-S2sSXL8VNqTMygcbgOIrxg4OUovgUrw,55630
497
+ firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=OVGqVSeim7aancl0RnGzqwM21Qs3ZVFlaryZiOWi_wM,55748
495
498
  firefighter_tests/test_slack/views/modals/test_utils.py,sha256=DJd2n9q6fFu8UuCRdiq9U_Cn19MdnC5c-ydLLrk6rkc,5218
496
- firefighter_incident-0.0.18.dist-info/METADATA,sha256=IoKMnbPTxFmn7Cfb2UIV6x77XIp1pY6WSrOhW2JxsFg,5541
497
- firefighter_incident-0.0.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
498
- firefighter_incident-0.0.18.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
499
- firefighter_incident-0.0.18.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
500
- firefighter_incident-0.0.18.dist-info/RECORD,,
499
+ firefighter_incident-0.0.19.dist-info/METADATA,sha256=3oyEnmXZl2Ps9iciGESrGodxRAXbOJfiDcOaY2uh8NU,5541
500
+ firefighter_incident-0.0.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
501
+ firefighter_incident-0.0.19.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
502
+ firefighter_incident-0.0.19.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
503
+ firefighter_incident-0.0.19.dist-info/RECORD,,
@@ -0,0 +1,255 @@
1
+ """Tests for RAID alert notifications for P4-P5 incidents with linked Incident objects.
2
+
3
+ This module tests the alert_slack_new_jira_ticket() function behavior when called
4
+ with P4-P5 JiraTickets that have associated Incident objects (since 0.0.17 unified workflow).
5
+
6
+ Before 0.0.17: P4-P5 created only JiraTicket (no Incident) → alerts worked
7
+ Since 0.0.17: P4-P5 create Incident + JiraTicket → alerts broken due to incorrect check
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from unittest.mock import patch
13
+
14
+ import pytest
15
+
16
+ from firefighter.incidents.factories import (
17
+ IncidentCategoryFactory,
18
+ IncidentFactory,
19
+ UserFactory,
20
+ )
21
+ from firefighter.incidents.models.priority import Priority
22
+ from firefighter.jira_app.models import JiraUser
23
+ from firefighter.raid.forms import (
24
+ alert_slack_new_jira_ticket,
25
+ get_internal_alert_conversations,
26
+ )
27
+ from firefighter.raid.models import JiraTicket
28
+ from firefighter.slack.models.conversation import Conversation
29
+ from firefighter.slack.models.user import SlackUser
30
+
31
+
32
+ @pytest.mark.django_db
33
+ class TestAlertSlackNewJiraTicketWithIncident:
34
+ """Test alert_slack_new_jira_ticket for P4-P5 with linked Incident (unified workflow)."""
35
+
36
+ @pytest.fixture
37
+ def p4_priority(self):
38
+ """Get or create P4 priority."""
39
+ priority, _ = Priority.objects.get_or_create(value=4, defaults={"name": "P4"})
40
+ return priority
41
+
42
+ @pytest.fixture
43
+ def p5_priority(self):
44
+ """Get or create P5 priority."""
45
+ priority, _ = Priority.objects.get_or_create(value=5, defaults={"name": "P5"})
46
+ return priority
47
+
48
+ @pytest.fixture
49
+ def incident_category(self):
50
+ """Create incident category."""
51
+ return IncidentCategoryFactory()
52
+
53
+ @pytest.fixture
54
+ def reporter_user_with_slack(self):
55
+ """Create user with Slack account."""
56
+ user = UserFactory(email="reporter@manomano.com")
57
+ jira_user = JiraUser.objects.create(id="jira-123", user=user)
58
+ slack_user = SlackUser.objects.create(user=user, slack_id="U12345")
59
+ return user, jira_user, slack_user
60
+
61
+ @pytest.fixture
62
+ def raid_alert_channel_sbi(self):
63
+ """Create the raid_alert__sbi_normal channel for SBI tickets."""
64
+ return Conversation.objects.create(
65
+ name="incidents",
66
+ channel_id="C_INCIDENTS",
67
+ tag="raid_alert__sbi_normal",
68
+ )
69
+
70
+ def test_alert_p4_incident_with_linked_incident_should_succeed(
71
+ self,
72
+ p4_priority,
73
+ incident_category,
74
+ reporter_user_with_slack,
75
+ raid_alert_channel_sbi, # noqa: ARG002 - fixture creates channel in DB
76
+ ):
77
+ """Test that P4 JiraTicket with linked Incident can send raid_alert notifications.
78
+
79
+ This test reproduces the CURRENT BROKEN behavior since 0.0.17:
80
+ - UnifiedIncidentForm creates Incident + JiraTicket for P4-P5
81
+ - alert_slack_new_jira_ticket() raises ValueError because jira_ticket.incident exists
82
+ - Notifications to #incidents channel are never sent
83
+
84
+ Expected: Should send notifications (test will FAIL until bug is fixed)
85
+ """
86
+ user, jira_user, _ = reporter_user_with_slack
87
+
88
+ # Create P4 incident (simulating UnifiedIncidentForm behavior)
89
+ incident = IncidentFactory(
90
+ priority=p4_priority,
91
+ incident_category=incident_category,
92
+ created_by=user,
93
+ )
94
+
95
+ # Create JiraTicket linked to Incident (UNIFIED workflow since 0.0.17)
96
+ jira_ticket = JiraTicket.objects.create(
97
+ id=12345,
98
+ key="SBI-12345",
99
+ summary="P4 incident with linked Incident",
100
+ business_impact="N/A",
101
+ project_key="SBI",
102
+ reporter=jira_user,
103
+ incident=incident, # ← This is what breaks alert_slack_new_jira_ticket()
104
+ )
105
+
106
+ # Mock Slack API to capture messages sent
107
+ with (
108
+ patch(
109
+ "firefighter.slack.models.user.SlackUser.send_private_message"
110
+ ) as mock_dm,
111
+ patch(
112
+ "firefighter.slack.models.conversation.Conversation.send_message_and_save"
113
+ ) as mock_channel_msg,
114
+ ):
115
+ # This should NOT raise ValueError and should send notifications
116
+ alert_slack_new_jira_ticket(
117
+ jira_ticket, reporter_user=user, reporter_email=user.email
118
+ )
119
+
120
+ # Verify DM was sent to reporter
121
+ assert mock_dm.called, "Should send DM to reporter"
122
+
123
+ # Verify message was sent to #incidents channel
124
+ assert (
125
+ mock_channel_msg.called
126
+ ), "Should send message to raid_alert channel"
127
+
128
+ def test_alert_p5_incident_with_linked_incident_should_succeed(
129
+ self,
130
+ p5_priority,
131
+ incident_category,
132
+ reporter_user_with_slack,
133
+ raid_alert_channel_sbi, # noqa: ARG002 - fixture creates channel in DB
134
+ ):
135
+ """Test that P5 JiraTicket with linked Incident can send raid_alert notifications."""
136
+ user, jira_user, _ = reporter_user_with_slack
137
+
138
+ # Create P5 incident
139
+ incident = IncidentFactory(
140
+ priority=p5_priority,
141
+ incident_category=incident_category,
142
+ created_by=user,
143
+ )
144
+
145
+ # Create JiraTicket linked to Incident
146
+ jira_ticket = JiraTicket.objects.create(
147
+ id=12346,
148
+ key="SBI-12346",
149
+ summary="P5 incident with linked Incident",
150
+ business_impact="N/A",
151
+ project_key="SBI",
152
+ reporter=jira_user,
153
+ incident=incident,
154
+ )
155
+
156
+ # Mock Slack API
157
+ with (
158
+ patch(
159
+ "firefighter.slack.models.user.SlackUser.send_private_message"
160
+ ) as mock_dm,
161
+ patch(
162
+ "firefighter.slack.models.conversation.Conversation.send_message_and_save"
163
+ ) as mock_channel_msg,
164
+ ):
165
+ # Should succeed for P5 as well
166
+ alert_slack_new_jira_ticket(
167
+ jira_ticket, reporter_user=user, reporter_email=user.email
168
+ )
169
+
170
+ assert mock_dm.called
171
+ assert mock_channel_msg.called
172
+
173
+ def test_alert_p1_incident_should_fail(
174
+ self, incident_category, reporter_user_with_slack
175
+ ):
176
+ """Test that P1 JiraTicket with linked Incident correctly raises ValueError.
177
+
178
+ P1-P3 incidents should NOT use raid_alert notifications.
179
+ They have dedicated Slack channels and use different notification flow.
180
+ """
181
+ user, jira_user, _ = reporter_user_with_slack
182
+ p1_priority, _ = Priority.objects.get_or_create(value=1, defaults={"name": "P1"})
183
+
184
+ incident = IncidentFactory(
185
+ priority=p1_priority,
186
+ incident_category=incident_category,
187
+ created_by=user,
188
+ )
189
+
190
+ jira_ticket = JiraTicket.objects.create(
191
+ id=12347,
192
+ key="SBI-12347",
193
+ summary="P1 critical incident",
194
+ business_impact="High",
195
+ project_key="SBI",
196
+ reporter=jira_user,
197
+ incident=incident,
198
+ )
199
+
200
+ # P1 should correctly raise ValueError
201
+ with pytest.raises(
202
+ ValueError, match="This is a critical incident, not a raid incident"
203
+ ):
204
+ alert_slack_new_jira_ticket(
205
+ jira_ticket, reporter_user=user, reporter_email=user.email
206
+ )
207
+
208
+ def test_get_internal_alert_conversations_for_normal_impact(
209
+ self, raid_alert_channel_sbi # noqa: ARG002 - fixture creates channel in DB
210
+ ):
211
+ """Test that get_internal_alert_conversations finds raid_alert__sbi_normal channel."""
212
+ # Create a JiraTicket with normal/N/A business impact for SBI project
213
+ jira_user = JiraUser.objects.create(id="jira-999", user=UserFactory())
214
+ jira_ticket = JiraTicket.objects.create(
215
+ id=99999,
216
+ key="SBI-99999",
217
+ summary="Test ticket",
218
+ business_impact="N/A",
219
+ project_key="SBI",
220
+ reporter=jira_user,
221
+ )
222
+
223
+ # Should find the raid_alert__sbi_normal channel
224
+ channels = get_internal_alert_conversations(jira_ticket)
225
+ channel_list = list(channels)
226
+
227
+ assert len(channel_list) == 1
228
+ assert channel_list[0].tag == "raid_alert__sbi_normal"
229
+ assert channel_list[0].name == "incidents"
230
+
231
+ def test_get_internal_alert_conversations_for_high_impact(self):
232
+ """Test that get_internal_alert_conversations finds raid_alert__sbi_high channel."""
233
+ # Create channel for high impact on SBI
234
+ Conversation.objects.create(
235
+ name="incidents-high-impact",
236
+ channel_id="C_INCIDENTS_HIGH",
237
+ tag="raid_alert__sbi_high",
238
+ )
239
+
240
+ # Create JiraTicket with High business impact for SBI
241
+ jira_user = JiraUser.objects.create(id="jira-998", user=UserFactory())
242
+ jira_ticket = JiraTicket.objects.create(
243
+ id=99998,
244
+ key="SBI-99998",
245
+ summary="Test high impact ticket",
246
+ business_impact="High",
247
+ project_key="SBI",
248
+ reporter=jira_user,
249
+ )
250
+
251
+ channels = get_internal_alert_conversations(jira_ticket)
252
+ channel_list = list(channels)
253
+
254
+ assert len(channel_list) == 1
255
+ assert channel_list[0].tag == "raid_alert__sbi_high"
@@ -368,6 +368,38 @@ class TestRaidJiraClientBasics:
368
368
  priority=1,
369
369
  )
370
370
 
371
+ def test_create_issue_zendesk_field_mapping(self, mock_jira_client):
372
+ """Test that zendesk_ticket_id is correctly mapped to customfield_10895."""
373
+ mock_issue = Mock()
374
+ mock_issue.raw = {
375
+ "id": "12355",
376
+ "key": "TEST-130",
377
+ "fields": {
378
+ "summary": "Test zendesk mapping",
379
+ "description": "Test description",
380
+ "reporter": {"accountId": "reporter123"},
381
+ "issuetype": {"name": "Bug"},
382
+ },
383
+ }
384
+ mock_jira_client.jira.create_issue.return_value = mock_issue
385
+
386
+ result = mock_jira_client.create_issue(
387
+ issuetype="Bug",
388
+ summary="Test zendesk mapping",
389
+ description="Test description",
390
+ assignee=None,
391
+ reporter="test_reporter",
392
+ priority=1,
393
+ zendesk_ticket_id="ZD-98765",
394
+ )
395
+
396
+ # Verify create_issue was called with customfield_10895
397
+ call_kwargs = mock_jira_client.jira.create_issue.call_args[1]
398
+ assert "customfield_10895" in call_kwargs
399
+ assert call_kwargs["customfield_10895"] == "ZD-98765"
400
+ assert result["id"] == 12355
401
+ assert result["key"] == "TEST-130"
402
+
371
403
  def test_jira_object_static_method(self):
372
404
  """Test _jira_object static method."""
373
405
  test_issue = {
@@ -195,9 +195,12 @@ class TestAlertSlackNewJiraTicket:
195
195
  )
196
196
 
197
197
  def test_alert_slack_new_jira_ticket_with_incident_raises_error(self):
198
- """Test that function raises ValueError for critical incidents."""
199
- # Given - Create an incident and link it to the ticket
200
- incident = IncidentFactory()
198
+ """Test that function raises ValueError for P1-P3 critical incidents."""
199
+ # Given - Create a P1 incident and link it to the ticket
200
+ p1_priority = Priority.objects.get_or_create(value=1, defaults={"name": "P1"})[
201
+ 0
202
+ ]
203
+ incident = IncidentFactory(priority=p1_priority)
201
204
  self.jira_ticket.incident = incident
202
205
  self.jira_ticket.save()
203
206
 
@@ -311,6 +311,53 @@ class TestLandbotIssueRequestSerializer(TestCase):
311
311
  result, reporter_user=user, reporter_email="test@external.com"
312
312
  )
313
313
 
314
+ @patch("firefighter.raid.serializers.alert_slack_new_jira_ticket")
315
+ @patch("firefighter.raid.serializers.get_reporter_user_from_email")
316
+ @patch("firefighter.raid.serializers.jira_client")
317
+ def test_create_with_zendesk_field(self, mock_jira_client, mock_get_reporter, mock_alert_slack):
318
+ """Test create method passes zendesk field to jira_client."""
319
+ # Setup mocks
320
+ user = UserFactory()
321
+ jira_user = JiraUser.objects.create(id="test-123", user=user)
322
+ mock_get_reporter.return_value = (user, jira_user, "manomano.com")
323
+ mock_alert_slack.return_value = None
324
+
325
+ mock_jira_client.create_issue.return_value = {
326
+ "id": "12345",
327
+ "key": "TEST-123",
328
+ "summary": "Test Issue",
329
+ "reporter": jira_user,
330
+ }
331
+
332
+ serializer = LandbotIssueRequestSerializer()
333
+ validated_data = {
334
+ "reporter_email": "test@manomano.com",
335
+ "issue_type": "Incident",
336
+ "summary": "Test Issue",
337
+ "description": "Test Description",
338
+ "labels": [],
339
+ "priority": 1,
340
+ "seller_contract_id": None,
341
+ "zoho": None,
342
+ "zendesk": "ZD-12345", # Zendesk ticket ID
343
+ "platform": "FR",
344
+ "incident_category": None,
345
+ "business_impact": None,
346
+ "environments": ["PRD"],
347
+ "suggested_team_routing": "TEAM1",
348
+ "project": "SBI",
349
+ "attachments": None,
350
+ }
351
+
352
+ result = serializer.create(validated_data)
353
+
354
+ # Verify zendesk_ticket_id was passed to jira_client.create_issue
355
+ create_call = mock_jira_client.create_issue.call_args[1]
356
+ assert "zendesk_ticket_id" in create_call
357
+ assert create_call["zendesk_ticket_id"] == "ZD-12345"
358
+ assert isinstance(result, JiraTicket)
359
+ mock_alert_slack.assert_called_once()
360
+
314
361
 
315
362
  class TestJiraWebhookUpdateSerializer(TestCase):
316
363
  """Test JiraWebhookUpdateSerializer functionality."""
@@ -0,0 +1,198 @@
1
+ """Comprehensive integration tests for zendesk field flow from API endpoint to Jira."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import Mock, patch
6
+
7
+ import pytest
8
+
9
+ from firefighter.incidents.factories import UserFactory
10
+ from firefighter.jira_app.models import JiraUser
11
+ from firefighter.raid.client import RaidJiraClient
12
+ from firefighter.raid.models import JiraTicket
13
+ from firefighter.raid.serializers import LandbotIssueRequestSerializer
14
+
15
+
16
+ @pytest.mark.django_db
17
+ class TestZendeskFieldIntegration:
18
+ """Integration tests for zendesk field flow from serializer to Jira API."""
19
+
20
+ @pytest.fixture
21
+ def mock_jira_client(self):
22
+ """Create a mock RaidJiraClient with mocked Jira connection."""
23
+ with patch("firefighter.jira_app.client.JiraClient.__init__", return_value=None):
24
+ client = RaidJiraClient()
25
+ client.jira = Mock()
26
+ return client
27
+
28
+ @pytest.mark.parametrize(
29
+ ("zendesk_value", "should_be_in_jira"),
30
+ [
31
+ ("ZD-12345", True), # Valid value - should be sent to Jira
32
+ ("", False), # Empty string - should NOT be sent to Jira
33
+ (None, False), # None - should NOT be sent to Jira
34
+ ("0", True), # "0" is truthy in Python - should be sent
35
+ ],
36
+ )
37
+ def test_zendesk_field_mapping_edge_cases(
38
+ self, mock_jira_client, zendesk_value, should_be_in_jira
39
+ ):
40
+ """Test zendesk field mapping with various edge case values."""
41
+ mock_issue = Mock()
42
+ mock_issue.raw = {
43
+ "id": "12345",
44
+ "key": "TEST-123",
45
+ "fields": {
46
+ "summary": "Test",
47
+ "description": "Test",
48
+ "reporter": {"accountId": "reporter123"},
49
+ "issuetype": {"name": "Bug"},
50
+ },
51
+ }
52
+ mock_jira_client.jira.create_issue.return_value = mock_issue
53
+
54
+ # Call create_issue with zendesk value
55
+ result = mock_jira_client.create_issue(
56
+ issuetype="Bug",
57
+ summary="Test",
58
+ description="Test",
59
+ assignee=None,
60
+ reporter="test_reporter",
61
+ priority=1,
62
+ zendesk_ticket_id=zendesk_value,
63
+ )
64
+
65
+ # Check that the Jira API was called
66
+ assert mock_jira_client.jira.create_issue.called
67
+
68
+ # Verify customfield_10895 presence based on expected behavior
69
+ call_kwargs = mock_jira_client.jira.create_issue.call_args[1]
70
+
71
+ if should_be_in_jira:
72
+ assert "customfield_10895" in call_kwargs, (
73
+ f"customfield_10895 should be present for zendesk_value={zendesk_value!r}"
74
+ )
75
+ assert call_kwargs["customfield_10895"] == str(zendesk_value)
76
+ else:
77
+ assert "customfield_10895" not in call_kwargs, (
78
+ f"customfield_10895 should NOT be present for zendesk_value={zendesk_value!r}"
79
+ )
80
+
81
+ assert result["id"] == 12345
82
+
83
+ @pytest.mark.parametrize(
84
+ ("zendesk_value", "expected_param"),
85
+ [
86
+ ("ZD-12345", "ZD-12345"), # Valid value
87
+ ("", ""), # Empty string
88
+ (None, None), # None
89
+ ],
90
+ )
91
+ @patch("firefighter.raid.serializers.alert_slack_new_jira_ticket")
92
+ @patch("firefighter.raid.serializers.get_reporter_user_from_email")
93
+ @patch("firefighter.raid.serializers.jira_client")
94
+ def test_serializer_to_client_zendesk_flow(
95
+ self,
96
+ mock_jira_client,
97
+ mock_get_reporter,
98
+ mock_alert_slack,
99
+ zendesk_value,
100
+ expected_param,
101
+ ):
102
+ """Test that serializer correctly passes zendesk value to jira_client."""
103
+ # Setup mocks
104
+ user = UserFactory()
105
+ jira_user = JiraUser.objects.create(id="test-123", user=user)
106
+ mock_get_reporter.return_value = (user, jira_user, "manomano.com")
107
+ mock_alert_slack.return_value = None
108
+
109
+ mock_jira_client.create_issue.return_value = {
110
+ "id": "12345",
111
+ "key": "TEST-123",
112
+ "summary": "Test",
113
+ "reporter": jira_user,
114
+ }
115
+
116
+ # Create serializer and validated_data
117
+ serializer = LandbotIssueRequestSerializer()
118
+ validated_data = {
119
+ "reporter_email": "test@manomano.com",
120
+ "issue_type": "Incident",
121
+ "summary": "Test",
122
+ "description": "Test",
123
+ "labels": [],
124
+ "priority": 1,
125
+ "seller_contract_id": None,
126
+ "zoho": None,
127
+ "zendesk": zendesk_value,
128
+ "platform": "FR",
129
+ "incident_category": None,
130
+ "business_impact": None,
131
+ "environments": ["PRD"],
132
+ "suggested_team_routing": "TEAM1",
133
+ "project": "SBI",
134
+ "attachments": None,
135
+ }
136
+
137
+ # Call create
138
+ result = serializer.create(validated_data)
139
+
140
+ # Verify jira_client.create_issue was called
141
+ assert mock_jira_client.create_issue.called
142
+
143
+ # Check zendesk_ticket_id parameter
144
+ create_call = mock_jira_client.create_issue.call_args[1]
145
+ assert "zendesk_ticket_id" in create_call
146
+ assert create_call["zendesk_ticket_id"] == expected_param
147
+
148
+ assert isinstance(result, JiraTicket)
149
+ mock_alert_slack.assert_called_once()
150
+
151
+ @patch("firefighter.raid.serializers.alert_slack_new_jira_ticket")
152
+ @patch("firefighter.raid.serializers.get_reporter_user_from_email")
153
+ @patch("firefighter.raid.serializers.jira_client")
154
+ def test_serializer_without_zendesk_field(
155
+ self, mock_jira_client, mock_get_reporter, mock_alert_slack
156
+ ):
157
+ """Test that serializer works when zendesk field is not provided."""
158
+ # Setup mocks
159
+ user = UserFactory()
160
+ jira_user = JiraUser.objects.create(id="test-456", user=user)
161
+ mock_get_reporter.return_value = (user, jira_user, "manomano.com")
162
+ mock_alert_slack.return_value = None
163
+
164
+ mock_jira_client.create_issue.return_value = {
165
+ "id": "12346",
166
+ "key": "TEST-124",
167
+ "summary": "Test",
168
+ "reporter": jira_user,
169
+ }
170
+
171
+ serializer = LandbotIssueRequestSerializer()
172
+ validated_data = {
173
+ "reporter_email": "test@manomano.com",
174
+ "issue_type": "Incident",
175
+ "summary": "Test without zendesk",
176
+ "description": "Test",
177
+ "labels": [],
178
+ "priority": 1,
179
+ "seller_contract_id": None,
180
+ "zoho": None,
181
+ # zendesk field NOT provided
182
+ "platform": "FR",
183
+ "incident_category": None,
184
+ "business_impact": None,
185
+ "environments": ["PRD"],
186
+ "suggested_team_routing": "TEAM1",
187
+ "project": "SBI",
188
+ "attachments": None,
189
+ }
190
+
191
+ result = serializer.create(validated_data)
192
+
193
+ # Verify zendesk_ticket_id parameter is None
194
+ create_call = mock_jira_client.create_issue.call_args[1]
195
+ assert "zendesk_ticket_id" in create_call
196
+ assert create_call["zendesk_ticket_id"] is None
197
+
198
+ assert isinstance(result, JiraTicket)
@@ -0,0 +1,344 @@
1
+ """Tests for Slack Conversation tags usage across the application.
2
+
3
+ This module verifies that all documented conversation tags are properly used
4
+ and that the logic for finding and using tagged channels works correctly.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import pytest
10
+ from django.db import IntegrityError
11
+
12
+ from firefighter.incidents.enums import IncidentStatus
13
+ from firefighter.incidents.factories import (
14
+ IncidentCategoryFactory,
15
+ IncidentFactory,
16
+ UserFactory,
17
+ )
18
+ from firefighter.incidents.models.environment import Environment
19
+ from firefighter.incidents.models.priority import Priority
20
+ from firefighter.jira_app.models import JiraUser
21
+ from firefighter.raid.forms import get_internal_alert_conversations
22
+ from firefighter.raid.models import JiraTicket
23
+ from firefighter.slack.models.conversation import Conversation
24
+ from firefighter.slack.rules import (
25
+ should_publish_in_general_channel,
26
+ should_publish_in_it_deploy_channel,
27
+ )
28
+
29
+
30
+ @pytest.mark.django_db
31
+ class TestTechIncidentsTag:
32
+ """Test the tech_incidents tag usage for general incident announcements."""
33
+
34
+ @pytest.fixture
35
+ def tech_incidents_channel(self):
36
+ """Create tech_incidents channel."""
37
+ return Conversation.objects.create(
38
+ name="tech-incidents",
39
+ channel_id="C_TECH_INCIDENTS",
40
+ tag="tech_incidents",
41
+ )
42
+
43
+ @pytest.fixture
44
+ def p1_prd_incident(self):
45
+ """Create P1 incident in PRD."""
46
+ p1 = Priority.objects.get_or_create(value=1, defaults={"name": "P1"})[0]
47
+ prd = Environment.objects.get_or_create(value="PRD", defaults={"value": "PRD"})[
48
+ 0
49
+ ]
50
+ category = IncidentCategoryFactory()
51
+ return IncidentFactory(
52
+ priority=p1,
53
+ environment=prd,
54
+ incident_category=category,
55
+ status=IncidentStatus.OPEN,
56
+ private=False,
57
+ )
58
+
59
+ def test_tech_incidents_channel_can_be_found(
60
+ self, tech_incidents_channel # noqa: ARG002 - fixture creates channel in DB
61
+ ):
62
+ """Test that tech_incidents channel can be retrieved by tag."""
63
+ channel = Conversation.objects.get_or_none(tag="tech_incidents")
64
+ assert channel is not None
65
+ assert channel.name == "tech-incidents"
66
+ assert channel.tag == "tech_incidents"
67
+
68
+ def test_should_publish_p1_prd_incident_in_tech_incidents(self, p1_prd_incident):
69
+ """Test that P1 PRD incidents should be published to tech_incidents."""
70
+ should_publish = should_publish_in_general_channel(
71
+ p1_prd_incident, incident_update=None
72
+ )
73
+ assert should_publish is True
74
+
75
+ def test_should_not_publish_p4_in_tech_incidents(self):
76
+ """Test that P4 incidents should NOT be published to tech_incidents."""
77
+ p4 = Priority.objects.get_or_create(value=4, defaults={"name": "P4"})[0]
78
+ prd = Environment.objects.get_or_create(value="PRD", defaults={"value": "PRD"})[
79
+ 0
80
+ ]
81
+ category = IncidentCategoryFactory()
82
+ incident = IncidentFactory(
83
+ priority=p4,
84
+ environment=prd,
85
+ incident_category=category,
86
+ status=IncidentStatus.OPEN,
87
+ private=False,
88
+ )
89
+
90
+ should_publish = should_publish_in_general_channel(
91
+ incident, incident_update=None
92
+ )
93
+ assert should_publish is False
94
+
95
+ def test_should_not_publish_private_incident_in_tech_incidents(self):
96
+ """Test that private incidents should NOT be published to tech_incidents."""
97
+ p1 = Priority.objects.get_or_create(value=1, defaults={"name": "P1"})[0]
98
+ prd = Environment.objects.get_or_create(value="PRD", defaults={"value": "PRD"})[
99
+ 0
100
+ ]
101
+ category = IncidentCategoryFactory()
102
+ incident = IncidentFactory(
103
+ priority=p1,
104
+ environment=prd,
105
+ incident_category=category,
106
+ status=IncidentStatus.OPEN,
107
+ private=True, # Private incident
108
+ )
109
+
110
+ should_publish = should_publish_in_general_channel(
111
+ incident, incident_update=None
112
+ )
113
+ assert should_publish is False
114
+
115
+
116
+ @pytest.mark.django_db
117
+ class TestItDeployTag:
118
+ """Test the it_deploy tag usage for deployment warnings."""
119
+
120
+ @pytest.fixture
121
+ def it_deploy_channel(self):
122
+ """Create it_deploy channel."""
123
+ return Conversation.objects.create(
124
+ name="it-deploy", channel_id="C_IT_DEPLOY", tag="it_deploy"
125
+ )
126
+
127
+ @pytest.fixture
128
+ def deploy_warning_category(self):
129
+ """Create incident category with deploy_warning=True."""
130
+ return IncidentCategoryFactory(deploy_warning=True)
131
+
132
+ def test_it_deploy_channel_can_be_found(
133
+ self, it_deploy_channel # noqa: ARG002 - fixture creates channel in DB
134
+ ):
135
+ """Test that it_deploy channel can be retrieved by tag."""
136
+ channel = Conversation.objects.get_or_none(tag="it_deploy")
137
+ assert channel is not None
138
+ assert channel.name == "it-deploy"
139
+ assert channel.tag == "it_deploy"
140
+
141
+ def test_should_publish_p1_deploy_warning_in_it_deploy(
142
+ self, deploy_warning_category
143
+ ):
144
+ """Test that P1 incidents with deploy_warning should be published to it_deploy."""
145
+ p1 = Priority.objects.get_or_create(value=1, defaults={"name": "P1"})[0]
146
+ prd = Environment.objects.get_or_create(value="PRD", defaults={"value": "PRD"})[
147
+ 0
148
+ ]
149
+ incident = IncidentFactory(
150
+ priority=p1,
151
+ environment=prd,
152
+ incident_category=deploy_warning_category,
153
+ status=IncidentStatus.OPEN,
154
+ private=False,
155
+ )
156
+
157
+ should_publish = should_publish_in_it_deploy_channel(incident)
158
+ assert should_publish is True
159
+
160
+ def test_should_not_publish_p2_in_it_deploy(self, deploy_warning_category):
161
+ """Test that P2 incidents should NOT be published to it_deploy (P1 only)."""
162
+ p2 = Priority.objects.get_or_create(value=2, defaults={"name": "P2"})[0]
163
+ prd = Environment.objects.get_or_create(value="PRD", defaults={"value": "PRD"})[
164
+ 0
165
+ ]
166
+ incident = IncidentFactory(
167
+ priority=p2,
168
+ environment=prd,
169
+ incident_category=deploy_warning_category,
170
+ status=IncidentStatus.OPEN,
171
+ private=False,
172
+ )
173
+
174
+ should_publish = should_publish_in_it_deploy_channel(incident)
175
+ assert should_publish is False
176
+
177
+ def test_should_not_publish_p1_without_deploy_warning_in_it_deploy(self):
178
+ """Test that P1 without deploy_warning should NOT be published to it_deploy."""
179
+ p1 = Priority.objects.get_or_create(value=1, defaults={"name": "P1"})[0]
180
+ prd = Environment.objects.get_or_create(value="PRD", defaults={"value": "PRD"})[
181
+ 0
182
+ ]
183
+ category = IncidentCategoryFactory(deploy_warning=False)
184
+ incident = IncidentFactory(
185
+ priority=p1,
186
+ environment=prd,
187
+ incident_category=category,
188
+ status=IncidentStatus.OPEN,
189
+ private=False,
190
+ )
191
+
192
+ should_publish = should_publish_in_it_deploy_channel(incident)
193
+ assert should_publish is False
194
+
195
+
196
+ @pytest.mark.django_db
197
+ class TestInvitedForAllPublicP1Tag:
198
+ """Test the invited_for_all_public_p1 usergroup tag."""
199
+
200
+ @pytest.fixture
201
+ def p1_usergroup(self):
202
+ """Create P1 usergroup conversation (represents Slack usergroup)."""
203
+ return Conversation.objects.create(
204
+ name="leadership-p1",
205
+ channel_id="S_LEADERSHIP", # S prefix for usergroup
206
+ tag="invited_for_all_public_p1",
207
+ )
208
+
209
+ def test_p1_usergroup_can_be_found(
210
+ self, p1_usergroup # noqa: ARG002 - fixture creates usergroup in DB
211
+ ):
212
+ """Test that P1 usergroup can be retrieved by tag."""
213
+ usergroup = Conversation.objects.get_or_none(tag="invited_for_all_public_p1")
214
+ assert usergroup is not None
215
+ assert usergroup.tag == "invited_for_all_public_p1"
216
+
217
+ def test_p1_usergroup_tag_exists_in_code(
218
+ self, p1_usergroup # noqa: ARG002 - fixture creates usergroup in DB
219
+ ):
220
+ """Test that P1 usergroup tag is referenced in codebase."""
221
+ # Just verify the tag can be found - the actual Slack integration
222
+ # is tested in the get_users tests
223
+ usergroup = Conversation.objects.get_or_none(tag="invited_for_all_public_p1")
224
+ assert usergroup is not None
225
+
226
+
227
+ @pytest.mark.django_db
228
+ class TestDevFirefighterTag:
229
+ """Test the dev_firefighter tag for support channel."""
230
+
231
+ @pytest.fixture
232
+ def support_channel(self):
233
+ """Create dev_firefighter support channel."""
234
+ return Conversation.objects.create(
235
+ name="firefighter-support",
236
+ channel_id="C_SUPPORT",
237
+ tag="dev_firefighter",
238
+ )
239
+
240
+ def test_support_channel_can_be_found(
241
+ self, support_channel # noqa: ARG002 - fixture creates channel in DB
242
+ ):
243
+ """Test that support channel can be retrieved by tag."""
244
+ channel = Conversation.objects.get_or_none(tag="dev_firefighter")
245
+ assert channel is not None
246
+ assert channel.name == "firefighter-support"
247
+ assert channel.tag == "dev_firefighter"
248
+
249
+ def test_support_channel_used_in_templating(self, support_channel):
250
+ """Test that support channel tag is referenced in slack_templating module."""
251
+ # The support channel is retrieved in slack_templating.py:76
252
+ # Just verify it can be found
253
+ channel = Conversation.objects.get_or_none(tag="dev_firefighter")
254
+ assert channel is not None
255
+ assert channel == support_channel
256
+
257
+
258
+ @pytest.mark.django_db
259
+ class TestRaidAlertTags:
260
+ """Test RAID alert tags patterns (already tested in test_raid_alert_p4_p5.py).
261
+
262
+ These tests verify the tag patterns work correctly for P4-P5 incidents.
263
+ """
264
+
265
+ def test_raid_alert_sbi_normal_tag_format(self):
266
+ """Test that raid_alert__sbi_normal follows correct format."""
267
+ # Create channel
268
+ Conversation.objects.create(
269
+ name="incidents", channel_id="C_INC", tag="raid_alert__sbi_normal"
270
+ )
271
+
272
+ # Create ticket
273
+ jira_user = JiraUser.objects.create(id="test-jira", user=UserFactory())
274
+ ticket = JiraTicket.objects.create(
275
+ id=1,
276
+ key="SBI-1",
277
+ summary="Test",
278
+ project_key="SBI",
279
+ business_impact="N/A",
280
+ reporter=jira_user,
281
+ )
282
+
283
+ channels = list(get_internal_alert_conversations(ticket))
284
+ assert len(channels) == 1
285
+ assert channels[0].tag == "raid_alert__sbi_normal"
286
+
287
+ def test_raid_alert_sbi_high_tag_format(self):
288
+ """Test that raid_alert__sbi_high follows correct format."""
289
+ # Create channel
290
+ Conversation.objects.create(
291
+ name="incidents-high", channel_id="C_INC_H", tag="raid_alert__sbi_high"
292
+ )
293
+
294
+ # Create ticket with high impact
295
+ jira_user = JiraUser.objects.create(id="test-jira2", user=UserFactory())
296
+ ticket = JiraTicket.objects.create(
297
+ id=2,
298
+ key="SBI-2",
299
+ summary="Test",
300
+ project_key="SBI",
301
+ business_impact="High",
302
+ reporter=jira_user,
303
+ )
304
+
305
+ channels = list(get_internal_alert_conversations(ticket))
306
+ assert len(channels) == 1
307
+ assert channels[0].tag == "raid_alert__sbi_high"
308
+
309
+
310
+ @pytest.mark.django_db
311
+ class TestTagUniqueness:
312
+ """Test that tags are unique and constraints work."""
313
+
314
+ def test_cannot_create_duplicate_tags(self):
315
+ """Test that duplicate non-empty tags are rejected."""
316
+ Conversation.objects.create(
317
+ name="channel1", channel_id="C_001", tag="unique_tag"
318
+ )
319
+
320
+ with pytest.raises(IntegrityError):
321
+ Conversation.objects.create(
322
+ name="channel2", channel_id="C_002", tag="unique_tag"
323
+ )
324
+
325
+ def test_can_have_multiple_empty_tags(self):
326
+ """Test that multiple channels can have empty tags."""
327
+ conv1 = Conversation.objects.create(name="channel1", channel_id="C_001", tag="")
328
+ conv2 = Conversation.objects.create(name="channel2", channel_id="C_002", tag="")
329
+
330
+ assert conv1.tag == ""
331
+ assert conv2.tag == ""
332
+
333
+ def test_tag_case_sensitivity(self):
334
+ """Test that tags are case-sensitive."""
335
+ Conversation.objects.create(
336
+ name="channel1", channel_id="C_001", tag="tech_incidents"
337
+ )
338
+
339
+ # Different case should be allowed (but not recommended)
340
+ conv2 = Conversation.objects.create(
341
+ name="channel2", channel_id="C_002", tag="TECH_INCIDENTS"
342
+ )
343
+
344
+ assert conv2.tag == "TECH_INCIDENTS"
@@ -331,28 +331,26 @@ class TestUpdateStatusModal:
331
331
  trigger_incident_workflow.assert_not_called()
332
332
 
333
333
  @staticmethod
334
- def test_can_close_when_all_conditions_met(mocker: MockerFixture) -> None:
334
+ def test_can_close_when_all_conditions_met(mocker: MockerFixture, priority_factory, environment_factory) -> None:
335
335
  """Test that closing is allowed when all conditions are met for P3+ incidents."""
336
336
  # Create a user first
337
337
  user = UserFactory.build()
338
338
  user.save()
339
339
 
340
+ # Create P3 priority (needs_postmortem=False) and non-PRD environment
341
+ p3_priority = priority_factory(value=3, name="P3", needs_postmortem=False)
342
+ stg_environment = environment_factory(value="STG", name="Staging")
343
+
340
344
  # Create a P3+ incident in MITIGATED status with all conditions met
341
345
  incident = IncidentFactory.build(
342
346
  _status=IncidentStatus.MITIGATED,
343
347
  created_by=user,
348
+ priority=p3_priority,
349
+ environment=stg_environment,
344
350
  )
345
351
  # IMPORTANT: Save the incident so it has an ID for the form to reference
346
352
  incident.save()
347
353
 
348
- # Mock needs_postmortem to return False (P3+ incident)
349
- mocker.patch.object(
350
- type(incident),
351
- "needs_postmortem",
352
- new_callable=PropertyMock,
353
- return_value=False
354
- )
355
-
356
354
  # Mock can_be_closed to return True (all conditions met)
357
355
  mocker.patch.object(
358
356
  type(incident),