firefighter-incident 0.0.18__py3-none-any.whl → 0.0.20__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.
@@ -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
 
@@ -550,3 +553,58 @@ class TestGetInternalAlertConversations:
550
553
 
551
554
  # Then
552
555
  assert conversation in result
556
+
557
+
558
+ @pytest.mark.django_db
559
+ class TestAlertSlackNewJiraTicketSlackApiError:
560
+ """Test SlackApiError handling in alert_slack_new_jira_ticket."""
561
+
562
+ @pytest.fixture(autouse=True)
563
+ def setup(self):
564
+ """Set up test fixtures."""
565
+ self.user = UserFactory()
566
+ self.jira_user = JiraUser.objects.create(id="jira-slack-error", user=self.user)
567
+ self.jira_ticket = JiraTicket.objects.create(
568
+ id=88888,
569
+ key="SLACK-888",
570
+ summary="Slack error ticket",
571
+ reporter=self.jira_user,
572
+ )
573
+
574
+ @patch("firefighter.raid.forms.get_partner_alert_conversations")
575
+ @patch("firefighter.raid.forms.get_internal_alert_conversations")
576
+ @patch("firefighter.raid.forms.SlackMessageRaidCreatedIssue")
577
+ def test_alert_slack_new_jira_ticket_slack_api_error_on_channel_send(
578
+ self, mock_message_class, mock_get_internal, mock_get_partner, caplog
579
+ ):
580
+ """Test SlackApiError when sending to channel - should log exception and continue."""
581
+ # Given: Create a conversation that will fail to send
582
+ channel = Conversation.objects.create(
583
+ channel_id="C_FAIL_TEST",
584
+ name="fail-channel-test",
585
+ tag="raid_alert__test_fail",
586
+ )
587
+ mock_get_internal.return_value = Conversation.objects.filter(id=channel.id)
588
+ mock_get_partner.return_value = Conversation.objects.none()
589
+
590
+ # Mock message
591
+ mock_message = Mock()
592
+ mock_message_class.return_value = mock_message
593
+
594
+ # Mock channel.send_message_and_save to raise SlackApiError
595
+ with patch.object(
596
+ Conversation,
597
+ "send_message_and_save",
598
+ side_effect=SlackApiError(
599
+ message="channel_not_found",
600
+ response={"error": "channel_not_found"}
601
+ )
602
+ ):
603
+ # When
604
+ alert_slack_new_jira_ticket(self.jira_ticket)
605
+
606
+ # Then
607
+ # Should log exception about not being able to send
608
+ assert "Couldn't send message to channel" in caplog.text
609
+ assert str(self.jira_ticket.id) in caplog.text
610
+ # Function should continue and not raise
@@ -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"