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.
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.20'
32
+ __version_tuple__ = version_tuple = (0, 0, 20)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -71,7 +71,7 @@ class SelectImpactForm(forms.Form):
71
71
  if impact_name.impact_type.name == "Business Impact":
72
72
  impact_value = impact_name.value
73
73
 
74
- return LevelChoices(impact_value).label if impact_value else None
74
+ return str(LevelChoices(impact_value).label) if impact_value else None
75
75
 
76
76
  def save(self, incident: HasImpactProtocol) -> None:
77
77
  """Save the impact choices to the incident."""
@@ -254,14 +254,24 @@ class JiraClient:
254
254
  ValueError: Empty issue id
255
255
 
256
256
  Returns:
257
- list(JiraAPIUser): List of Jira users object
257
+ list(JiraAPIUser): List of Jira users object, or empty list if ticket doesn't exist
258
258
  """
259
- watchers = self.jira.watchers(jira_issue_id).raw.get("watchers")
260
- if len(watchers) == 0:
261
- logger.warning(
262
- "Watchers not found for jira_account_id '%s'.", jira_issue_id
263
- )
264
- return watchers
259
+ try:
260
+ watchers = self.jira.watchers(jira_issue_id).raw.get("watchers")
261
+ except exceptions.JIRAError as e:
262
+ if e.status_code == 404:
263
+ logger.warning(
264
+ "Jira ticket %s not found or no permission to access it. Cannot fetch watchers.",
265
+ jira_issue_id,
266
+ )
267
+ return []
268
+ raise
269
+ else:
270
+ if len(watchers) == 0:
271
+ logger.debug(
272
+ "No watchers found for jira_issue_id '%s'.", jira_issue_id
273
+ )
274
+ return watchers
265
275
 
266
276
  @staticmethod
267
277
  def _create_user_from_jira_info(
@@ -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.20
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=8FWqMenH0vIxIRfUQxFCzTnv2dsyjwSoAhVLD84lkZ4,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
@@ -141,7 +141,7 @@ firefighter/incidents/forms/close_incident.py,sha256=wyLIlNXx6eU183SkR8H--k9YEOV
141
141
  firefighter/incidents/forms/closure_reason.py,sha256=rwWC9Ks8iuuelCv2oqSpGUjL13yogpdbWHdG2yM23Rc,1564
142
142
  firefighter/incidents/forms/create_incident.py,sha256=cm5EWIvkJ1BZ-JfRJrh4TAE2wYYLV694gQ3MRIkcrGQ,2764
143
143
  firefighter/incidents/forms/edit.py,sha256=RxjmYpSeVo9Xbrs09hbmKO6siy3-PusKqg1VV5xAVr4,1051
144
- firefighter/incidents/forms/select_impact.py,sha256=jLbzVj4UeUGwOYYa5P92PXkEu1J_6H43UATZYzDgSLY,4630
144
+ firefighter/incidents/forms/select_impact.py,sha256=pH7neqP3d4Bxol4FuizD0Zcp6OP5wtvbhh1M3DjyDVA,4635
145
145
  firefighter/incidents/forms/unified_incident.py,sha256=3xDB3IFJVwrRe9C_G52SjQ9-Xeqe1ivAGb0e8xtXJaY,19564
146
146
  firefighter/incidents/forms/update_key_events.py,sha256=1Xmnxe5OgZqLFS2HmMzQm3VGFPQipsdrLgKSwdh-fKc,4441
147
147
  firefighter/incidents/forms/update_roles.py,sha256=Q26UPfwAj-8N23RNZLQkvmHGnS1_j_X5KQWjJmPjMKY,3635
@@ -271,7 +271,7 @@ firefighter/incidents/views/users/details.py,sha256=xqDUEMorplAdSJNQ2vcNzntKKn6U
271
271
  firefighter/jira_app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
272
272
  firefighter/jira_app/admin.py,sha256=YywHfi1dlpyyW8XPYvjtOW3fbTZGm5bKXvHbDkr0D_8,702
273
273
  firefighter/jira_app/apps.py,sha256=W6iM_e2X-dQ4YSeDPuFPM4wp-zIgpWsEiehtng0fk48,348
274
- firefighter/jira_app/client.py,sha256=pf0uqs6ccLKy0wCEIcP9FtKrsgTRkpipA9J6Mkj9p2I,16140
274
+ firefighter/jira_app/client.py,sha256=vT0Oj8W1QJabUCA_Gkgg2ZarxgAjBL_krdP2EkaJN84,16535
275
275
  firefighter/jira_app/models.py,sha256=m-ay74zeI9zMyKuTBfN7W8DPDc9zPmiDESC8mnGBjR4,1741
276
276
  firefighter/jira_app/types.py,sha256=Ukak1U1EhcH2jQPN-UoEL6AMZ-kzPsQ8c7FUr7GmahE,956
277
277
  firefighter/jira_app/utils.py,sha256=3xuzr8viZCBm6j2J9oFzA4bUvVW8TN1DOdlpbruJ_TE,3443
@@ -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
@@ -463,18 +463,24 @@ firefighter_tests/test_incidents/test_models/test_migrations/test_incident_migra
463
463
  firefighter_tests/test_incidents/test_utils/test_date_utils.py,sha256=ogP7qOEwItL4YGI5gbQPVssOS9ilwiuZC8OrT2qngBY,6568
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
+ firefighter_tests/test_jira_app/__init__.py,sha256=JxZ3v-0kiHOoO-N3kR8NHTmD8tEvuEYKW1GX_S1ZLMY,33
467
+ firefighter_tests/test_jira_app/conftest.py,sha256=HmZd7EBZgng-rb3kIaB14TPVMixMG4YEvnShVqgjodE,545
468
+ firefighter_tests/test_jira_app/test_jira_client_watchers.py,sha256=IBrFjwhOP0rfm58BBq339CySxjdJkPYjGmISC4oQhZc,4803
466
469
  firefighter_tests/test_raid/conftest.py,sha256=i_TOquYIMLDyVQ97uqxTqPJszVz4qq7L_Q7YJxTuS1o,4090
467
- firefighter_tests/test_raid/test_raid_client.py,sha256=8iRBR4bKewPPbtz33hCLOHiZDTELPQBOrQb7EdNDS4c,21227
470
+ firefighter_tests/test_raid/test_raid_alert_p4_p5.py,sha256=rz9orbt1E1vJ5POQyVZ6-SEPvqB55-xhwIWHicdfgDg,9356
471
+ firefighter_tests/test_raid/test_raid_client.py,sha256=KTqELERpWno7XhF9LpabpxkHoJiWWrryUg5LHi5Yfjo,22456
468
472
  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
473
+ firefighter_tests/test_raid/test_raid_forms.py,sha256=8hiXftYPO_lY0heKHqoreUW2s8AcedUme48wTq4hwNE,21931
470
474
  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
475
+ firefighter_tests/test_raid/test_raid_serializers.py,sha256=xkMK9npvDMAr2pUpP4mFbLszrR6qyTzsJRd-uNOzEhM,22560
472
476
  firefighter_tests/test_raid/test_raid_service.py,sha256=AqVyrRjW2tr0sfbXS4lGlJ7mcxB2ACEXAR8Bv0pXnj0,16755
473
477
  firefighter_tests/test_raid/test_raid_signals.py,sha256=twNxB3NQs58s8ZcP-wsZXG7iTHR4yKFC9x-zpq1ZuAo,7002
474
478
  firefighter_tests/test_raid/test_raid_transitions.py,sha256=mtmMKwukxmZSM-R619BQ3Z_2AB-qY6imvDgUF0A3_tw,4784
475
479
  firefighter_tests/test_raid/test_raid_utils.py,sha256=i6JBwim1G-qynwxprNZekxl9K7Vis4FFvNkw3wT2jTM,1016
476
480
  firefighter_tests/test_raid/test_raid_views.py,sha256=paAhh4k2EDlmG1ehwNhMuYIhr1okqrvM7xlkaTAo2V0,6825
481
+ firefighter_tests/test_raid/test_zendesk_integration.py,sha256=RtHIKWog_YZ6g-uZSr37k5XjAGe0c8WEeBWqkt1ZNqk,7113
477
482
  firefighter_tests/test_slack/conftest.py,sha256=MCg04JFQ0iBeYUN_moEvvH4PVA3wE48Q6LkrIw7Bic0,2015
483
+ firefighter_tests/test_slack/test_conversation_tags.py,sha256=nNqTZRRBfF6Z4wpFSYZ83DH7_mR9z9F9LAHSq8AHFN0,12744
478
484
  firefighter_tests/test_slack/test_signals_downgrade.py,sha256=mgl4H5vwr2kImf6g4IZbhv7YEPmMzbYSaVr8E6taL88,5420
479
485
  firefighter_tests/test_slack/test_slack_utils.py,sha256=9PLobMNXh3xDyFuwzcQFpKJhe4j__sIgf_WRHIpANJw,3957
480
486
  firefighter_tests/test_slack/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -491,10 +497,10 @@ firefighter_tests/test_slack/views/modals/test_open.py,sha256=IzgG9le5NN_CvltehA
491
497
  firefighter_tests/test_slack/views/modals/test_opening_unified.py,sha256=OejtLyc_mehav2TDaLzUnhilMNvhCzc6T4FodCqfQPk,17406
492
498
  firefighter_tests/test_slack/views/modals/test_send_sos.py,sha256=_rE6jD-gOzcGyhlY0R9GzlGtPx65oOOguJYdENgxtLc,1289
493
499
  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
500
+ firefighter_tests/test_slack/views/modals/test_update_status.py,sha256=OVGqVSeim7aancl0RnGzqwM21Qs3ZVFlaryZiOWi_wM,55748
495
501
  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,,
502
+ firefighter_incident-0.0.20.dist-info/METADATA,sha256=yI3gWfRG__v2fGUUA1H0zC2W4LpRJzv4xkjSq1RNufY,5541
503
+ firefighter_incident-0.0.20.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
504
+ firefighter_incident-0.0.20.dist-info/entry_points.txt,sha256=c13meJbv7YNmYz7MipMOQwzQ5IeFOPXUBYAJ44XMQsM,61
505
+ firefighter_incident-0.0.20.dist-info/licenses/LICENSE,sha256=krRiGp-a9-1nH1bWpBEdxyTKLhjLmn6DMVVoIb0zF90,1087
506
+ firefighter_incident-0.0.20.dist-info/RECORD,,
@@ -0,0 +1 @@
1
+ """Tests for jira_app module."""
@@ -0,0 +1,24 @@
1
+ """Fixtures for jira_app tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import Mock, patch
6
+
7
+ import pytest
8
+
9
+ from firefighter.jira_app.client import JiraClient
10
+
11
+
12
+ @pytest.fixture
13
+ def mock_jira_api():
14
+ """Create a mock JIRA API object."""
15
+ return Mock()
16
+
17
+
18
+ @pytest.fixture
19
+ def jira_client(mock_jira_api):
20
+ """Create a JiraClient with mocked JIRA API."""
21
+ with patch("firefighter.jira_app.client.JIRA", return_value=mock_jira_api):
22
+ client = JiraClient()
23
+ client.jira = mock_jira_api
24
+ return client
@@ -0,0 +1,135 @@
1
+ """Tests for JiraClient.get_watchers_from_jira_ticket method."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import Mock
6
+
7
+ import pytest
8
+ from jira.exceptions import JIRAError
9
+
10
+
11
+ @pytest.mark.django_db
12
+ class TestGetWatchersFromJiraTicket:
13
+ """Test get_watchers_from_jira_ticket method."""
14
+
15
+ def test_get_watchers_success_with_watchers(self, jira_client, mock_jira_api):
16
+ """Test successful retrieval of watchers when watchers exist."""
17
+ # Given
18
+ mock_watchers_response = Mock()
19
+ mock_watchers_response.raw = {
20
+ "watchers": [
21
+ {"accountId": "user1", "displayName": "User One"},
22
+ {"accountId": "user2", "displayName": "User Two"},
23
+ ]
24
+ }
25
+ mock_jira_api.watchers.return_value = mock_watchers_response
26
+
27
+ # When
28
+ result = jira_client.get_watchers_from_jira_ticket(12345)
29
+
30
+ # Then
31
+ mock_jira_api.watchers.assert_called_once_with(12345)
32
+ assert len(result) == 2
33
+ assert result[0]["accountId"] == "user1"
34
+ assert result[1]["accountId"] == "user2"
35
+
36
+ def test_get_watchers_success_empty_list(
37
+ self, jira_client, mock_jira_api, caplog
38
+ ):
39
+ """Test successful retrieval when no watchers exist."""
40
+ # Given
41
+ mock_watchers_response = Mock()
42
+ mock_watchers_response.raw = {"watchers": []}
43
+ mock_jira_api.watchers.return_value = mock_watchers_response
44
+
45
+ # When
46
+ result = jira_client.get_watchers_from_jira_ticket(12345)
47
+
48
+ # Then
49
+ mock_jira_api.watchers.assert_called_once_with(12345)
50
+ assert result == []
51
+ # Should log debug message
52
+ assert "No watchers found for jira_issue_id '12345'" in caplog.text
53
+
54
+ def test_get_watchers_404_ticket_not_found(
55
+ self, jira_client, mock_jira_api, caplog
56
+ ):
57
+ """Test handling of 404 error when ticket doesn't exist."""
58
+ # Given
59
+ jira_error = JIRAError(
60
+ status_code=404,
61
+ text="Issue does not exist or you do not have permission to see it.",
62
+ url="https://jira.example.com/rest/api/2/issue/404295/watchers",
63
+ )
64
+ mock_jira_api.watchers.side_effect = jira_error
65
+
66
+ # When
67
+ result = jira_client.get_watchers_from_jira_ticket(404295)
68
+
69
+ # Then
70
+ mock_jira_api.watchers.assert_called_once_with(404295)
71
+ assert result == []
72
+ # Should log warning
73
+ assert (
74
+ "Jira ticket 404295 not found or no permission to access it"
75
+ in caplog.text
76
+ )
77
+
78
+ def test_get_watchers_404_no_permission(self, jira_client, mock_jira_api, caplog):
79
+ """Test handling of 404 error when bot has no permission."""
80
+ # Given
81
+ jira_error = JIRAError(
82
+ status_code=404,
83
+ text="Issue does not exist or you do not have permission to see it.",
84
+ url="https://jira.example.com/rest/api/2/issue/999999/watchers",
85
+ )
86
+ mock_jira_api.watchers.side_effect = jira_error
87
+
88
+ # When
89
+ result = jira_client.get_watchers_from_jira_ticket("999999")
90
+
91
+ # Then
92
+ assert result == []
93
+ assert "not found or no permission" in caplog.text
94
+
95
+ def test_get_watchers_other_jira_error_raised(self, jira_client, mock_jira_api):
96
+ """Test that non-404 JIRA errors are re-raised."""
97
+ # Given
98
+ jira_error = JIRAError(
99
+ status_code=500, text="Internal Server Error", url="https://jira.example.com"
100
+ )
101
+ mock_jira_api.watchers.side_effect = jira_error
102
+
103
+ # When / Then
104
+ with pytest.raises(JIRAError) as exc_info:
105
+ jira_client.get_watchers_from_jira_ticket(12345)
106
+
107
+ assert exc_info.value.status_code == 500
108
+
109
+ def test_get_watchers_403_error_raised(self, jira_client, mock_jira_api):
110
+ """Test that 403 (Forbidden) errors are re-raised."""
111
+ # Given
112
+ jira_error = JIRAError(
113
+ status_code=403, text="Forbidden", url="https://jira.example.com"
114
+ )
115
+ mock_jira_api.watchers.side_effect = jira_error
116
+
117
+ # When / Then
118
+ with pytest.raises(JIRAError) as exc_info:
119
+ jira_client.get_watchers_from_jira_ticket(12345)
120
+
121
+ assert exc_info.value.status_code == 403
122
+
123
+ def test_get_watchers_with_string_id(self, jira_client, mock_jira_api):
124
+ """Test get_watchers with string issue ID."""
125
+ # Given
126
+ mock_watchers_response = Mock()
127
+ mock_watchers_response.raw = {"watchers": [{"accountId": "user1"}]}
128
+ mock_jira_api.watchers.return_value = mock_watchers_response
129
+
130
+ # When
131
+ result = jira_client.get_watchers_from_jira_ticket("INCIDENT-123")
132
+
133
+ # Then
134
+ mock_jira_api.watchers.assert_called_once_with("INCIDENT-123")
135
+ assert len(result) == 1
@@ -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 = {