regscale-cli 6.25.1.0__py3-none-any.whl → 6.27.0.0__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.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (146) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +19 -4
  4. regscale/core/app/internal/evidence.py +419 -2
  5. regscale/core/app/internal/login.py +0 -1
  6. regscale/core/app/utils/catalog_utils/common.py +1 -1
  7. regscale/dev/code_gen.py +24 -20
  8. regscale/integrations/commercial/jira.py +367 -126
  9. regscale/integrations/commercial/qualys/__init__.py +7 -8
  10. regscale/integrations/commercial/qualys/scanner.py +8 -3
  11. regscale/integrations/commercial/sicura/api.py +14 -13
  12. regscale/integrations/commercial/sicura/commands.py +8 -2
  13. regscale/integrations/commercial/sicura/scanner.py +49 -39
  14. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  15. regscale/integrations/commercial/synqly/assets.py +17 -0
  16. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  17. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  18. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  19. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  20. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  21. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  22. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  23. regscale/integrations/commercial/wizv2/click.py +64 -79
  24. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  25. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  26. regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
  27. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  28. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  29. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  30. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  31. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  33. regscale/integrations/commercial/wizv2/issue.py +1 -1
  34. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  35. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  36. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  37. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  38. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  39. regscale/integrations/commercial/wizv2/reports.py +1 -1
  40. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  41. regscale/integrations/commercial/wizv2/scanner.py +39 -99
  42. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  43. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  44. regscale/integrations/commercial/wizv2/variables.py +89 -3
  45. regscale/integrations/compliance_integration.py +60 -41
  46. regscale/integrations/control_matcher.py +377 -0
  47. regscale/integrations/due_date_handler.py +14 -8
  48. regscale/integrations/milestone_manager.py +291 -0
  49. regscale/integrations/public/__init__.py +1 -0
  50. regscale/integrations/public/cci_importer.py +37 -38
  51. regscale/integrations/public/fedramp/click.py +60 -2
  52. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  53. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  54. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  55. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  56. regscale/integrations/scanner_integration.py +277 -153
  57. regscale/models/integration_models/cisa_kev_data.json +282 -9
  58. regscale/models/integration_models/nexpose.py +36 -10
  59. regscale/models/integration_models/qualys.py +3 -4
  60. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  61. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  62. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  63. regscale/models/locking.py +12 -8
  64. regscale/models/platform.py +1 -2
  65. regscale/models/regscale_models/control_implementation.py +47 -22
  66. regscale/models/regscale_models/issue.py +256 -95
  67. regscale/models/regscale_models/milestone.py +1 -1
  68. regscale/models/regscale_models/regscale_model.py +6 -1
  69. regscale/templates/__init__.py +0 -0
  70. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  71. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
  72. tests/regscale/integrations/commercial/__init__.py +0 -0
  73. tests/regscale/integrations/commercial/conftest.py +28 -0
  74. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  75. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  76. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  77. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  78. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  79. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  80. tests/regscale/integrations/commercial/test_burp.py +48 -0
  81. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  82. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  83. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  84. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  85. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  86. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  87. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  88. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  89. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  90. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  91. tests/regscale/integrations/commercial/test_snow.py +423 -0
  92. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  93. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  94. tests/regscale/integrations/commercial/test_stig.py +33 -0
  95. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  96. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  97. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  98. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  99. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  100. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  101. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  102. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  103. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  104. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  105. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  106. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  107. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  108. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  109. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  110. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  111. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  112. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  113. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  114. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  115. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  116. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  117. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  118. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  119. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  120. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  121. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  122. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  123. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  124. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  125. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  126. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  127. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  128. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  129. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  130. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
  131. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  132. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  133. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  134. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  135. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  136. tests/regscale/integrations/public/test_fedramp.py +301 -0
  137. tests/regscale/integrations/test_control_matcher.py +1397 -0
  138. tests/regscale/integrations/test_control_matching.py +155 -0
  139. tests/regscale/integrations/test_milestone_manager.py +408 -0
  140. tests/regscale/models/test_issue.py +378 -1
  141. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  142. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  143. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  144. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  145. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  146. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2204 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Test for jira integration in RegScale CLI"""
4
+ # standard python imports
5
+ import contextlib
6
+ import os
7
+ import shutil
8
+ import tempfile
9
+ from unittest.mock import MagicMock, patch
10
+ from datetime import datetime, timedelta
11
+
12
+ import pytest
13
+ from rich.progress import Progress
14
+ from jira import JIRAError
15
+
16
+ from regscale.core.app.application import Application
17
+ from regscale.core.app.api import Api
18
+ from regscale.core.app.utils.app_utils import compute_hash, create_progress_object, get_current_datetime
19
+ from regscale.integrations.commercial.jira import (
20
+ _generate_jira_comment,
21
+ check_and_close_tasks,
22
+ create_and_update_regscale_issues,
23
+ create_issue_in_jira,
24
+ create_jira_client,
25
+ download_regscale_attachments_to_directory,
26
+ fetch_jira_objects,
27
+ get_regscale_data_and_attachments,
28
+ map_jira_due_date,
29
+ map_jira_to_regscale_issue,
30
+ sync_regscale_and_jira,
31
+ sync_regscale_objects_to_jira,
32
+ sync_regscale_to_jira,
33
+ task_and_attachments_sync,
34
+ create_regscale_task_from_jira,
35
+ create_and_update_regscale_tasks,
36
+ process_tasks_for_sync,
37
+ upload_files_to_jira,
38
+ upload_files_to_regscale,
39
+ validate_issue_type,
40
+ )
41
+ from regscale.models import File, Issue
42
+ from regscale.models.regscale_models.task import Task
43
+ from tests import CLITestFixture
44
+
45
+
46
+ class TestJira(CLITestFixture):
47
+ JIRA_PROJECT = "SNES"
48
+ PATH = "regscale.integrations.commercial.jira"
49
+ security_plan = None
50
+
51
+ @pytest.fixture(autouse=True)
52
+ def setup_ssp(self, create_security_plan):
53
+ self.security_plan = create_security_plan
54
+
55
+ @property
56
+ def PARENT_ID(self):
57
+ """Get the parent ID from the existing SSP"""
58
+ return self.security_plan.id
59
+
60
+ @property
61
+ def PARENT_MODULE(self):
62
+ """Get the parent module from the existing SSP"""
63
+ return self.security_plan.get_module_string()
64
+
65
+ @pytest.fixture
66
+ def jira_client(self):
67
+ """Setup jira client"""
68
+ return create_jira_client(
69
+ config=self.config,
70
+ )
71
+
72
+ @staticmethod
73
+ @pytest.fixture(params=[True, False])
74
+ def fetch_attachments(request):
75
+ """Pytest fixture that will run twice:
76
+ first time with a true value, second time with a false value"""
77
+ return request.param
78
+
79
+ @pytest.fixture
80
+ def jira_issues(self, jira_client, fetch_attachments):
81
+ """Fixture for fetching Jira issues and attachments"""
82
+ return fetch_jira_objects(jira_client=jira_client, jira_project=self.JIRA_PROJECT, jira_issue_type="Bug")
83
+
84
+ @pytest.fixture
85
+ def jira_tasks(self, jira_client, fetch_attachments):
86
+ """Fixture for fetching Jira tasks and attachments"""
87
+ return fetch_jira_objects(
88
+ jira_client=jira_client,
89
+ jira_project=self.JIRA_PROJECT,
90
+ jira_issue_type="Task",
91
+ sync_tasks_only=True,
92
+ )
93
+
94
+ @pytest.fixture
95
+ def get_jira_issue(self, jira_client):
96
+ """Fixture for creating a test Issue in Jira"""
97
+ jira_issue = jira_client.create_issue(
98
+ fields={
99
+ "project": {"key": self.JIRA_PROJECT},
100
+ "summary": f"{self.title_prefix} Jira Integration Test",
101
+ "description": "Test issue for integration testing",
102
+ "issuetype": {"name": "Bug"},
103
+ }
104
+ )
105
+ yield jira_issue
106
+ jira_issue.delete() # cleanup afterwards
107
+
108
+ @pytest.fixture
109
+ def get_jira_task(self, jira_client):
110
+ """Fixture for creating a test Task in Jira"""
111
+ jira_task = jira_client.create_issue(
112
+ fields={
113
+ "project": {"key": self.JIRA_PROJECT},
114
+ "summary": f"{self.title_prefix} Jira Integration Test",
115
+ "description": "Test task for integration testing",
116
+ "issuetype": {"name": "Task"},
117
+ }
118
+ )
119
+ yield jira_task
120
+ jira_task.delete() # cleanup afterwards
121
+
122
+ @pytest.fixture
123
+ def get_jira_issue_with_attachment(self, get_jira_issue, jira_client):
124
+ """Fixture for creating a test Issue in Jira with an attachment"""
125
+ issue = get_jira_issue
126
+ file_path = os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt")
127
+ with open(file_path, "rb") as f:
128
+ jira_client.add_attachment(issue=issue, filename="test_attachment.txt", attachment=f)
129
+ yield jira_client.issue(issue.id)
130
+
131
+ @pytest.fixture
132
+ def get_jira_task_with_attachment(self, get_jira_task, jira_client):
133
+ """Fixture for creating a test Task in Jira with an attachment"""
134
+ task = get_jira_task
135
+ file_path = os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt")
136
+ with open(file_path, "rb") as f:
137
+ jira_client.add_attachment(issue=task, filename="test_attachment.txt", attachment=f)
138
+ yield jira_client.issue(task.id)
139
+
140
+ @pytest.fixture
141
+ def regscale_issues_and_attachments(self, fetch_attachments, regscale_issue_and_attachment):
142
+ """Fixture for fetching RegScale issues and attachments"""
143
+ _ = regscale_issue_and_attachment
144
+
145
+ if fetch_attachments:
146
+ return Issue.get_objects_and_attachments_by_parent(
147
+ parent_id=self.PARENT_ID,
148
+ parent_module=self.PARENT_MODULE,
149
+ )
150
+ else:
151
+ return (
152
+ Issue.get_all_by_parent(
153
+ parent_id=self.PARENT_ID,
154
+ parent_module=self.PARENT_MODULE,
155
+ ),
156
+ [],
157
+ )
158
+
159
+ @pytest.fixture
160
+ def regscale_tasks_and_attachments(self, fetch_attachments, regscale_task_and_attachment):
161
+ """Fixture for fetching RegScale tasks and attachments"""
162
+ _ = regscale_task_and_attachment
163
+ if fetch_attachments:
164
+ return Task.get_objects_and_attachments_by_parent(
165
+ parent_id=self.PARENT_ID,
166
+ parent_module=self.PARENT_MODULE,
167
+ )
168
+ else:
169
+ return (
170
+ Task.get_all_by_parent(
171
+ parent_id=self.PARENT_ID,
172
+ parent_module=self.PARENT_MODULE,
173
+ ),
174
+ [],
175
+ )
176
+
177
+ @pytest.fixture
178
+ def regscale_issue_and_attachment(self, fetch_attachments):
179
+ """Fixture for creating RegScale issue and attachment"""
180
+ issue = Issue(
181
+ title=f"{self.title_prefix} Jira Issue Integration Test",
182
+ description="Security plan for Jira integration testing",
183
+ parentId=self.PARENT_ID,
184
+ parentModule=self.PARENT_MODULE,
185
+ dueDate=get_current_datetime(),
186
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
187
+ status="Open",
188
+ ).create()
189
+ if fetch_attachments:
190
+ File.upload_file_to_regscale(
191
+ file_name=os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt"),
192
+ parent_id=issue.id,
193
+ parent_module=issue.get_module_string(),
194
+ api=self.api,
195
+ )
196
+ return issue
197
+
198
+ @pytest.fixture
199
+ def regscale_task_and_attachment(self, fetch_attachments):
200
+ """Fixture for creating RegScale task and attachment"""
201
+ task = Task(
202
+ status="Backlog",
203
+ title=f"{self.title_prefix} Jira Task Integration Test",
204
+ description="Task for Jira integration testing",
205
+ parentId=self.PARENT_ID,
206
+ parentModule=self.PARENT_MODULE,
207
+ dueDate=get_current_datetime(),
208
+ ).create()
209
+ if fetch_attachments:
210
+ File.upload_file_to_regscale(
211
+ file_name=os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "image.png"),
212
+ parent_id=task.id,
213
+ parent_module=task.get_module_string(),
214
+ api=self.api,
215
+ )
216
+ return task
217
+
218
+ @pytest.fixture
219
+ def mock_job_progres_object(self):
220
+ """Mock job_progress object"""
221
+ with patch.object(self.PATH, "job_progress", new=create_progress_object()) as job_progress:
222
+ yield job_progress
223
+
224
+ def test_init(self):
225
+ """Test init file and config"""
226
+ self.verify_config(
227
+ [
228
+ "jiraUrl",
229
+ "jiraApiToken",
230
+ "jiraUserName",
231
+ ]
232
+ )
233
+
234
+ @patch(f"{PATH}.Task.get_objects_and_attachments_by_parent")
235
+ def test_get_regscale_data_and_attachments_sync_both(self, mock_get_objects_and_attachments_by_parent):
236
+ """Test getting RegScale data and attachments"""
237
+ mock_get_objects_and_attachments_by_parent.return_value = (["objects"], {"attachments": ["attachment"]})
238
+ regscale_issues, regscale_attachments = get_regscale_data_and_attachments(
239
+ parent_id=self.PARENT_ID,
240
+ parent_module=self.PARENT_MODULE,
241
+ sync_attachments=True,
242
+ sync_tasks_only=True,
243
+ )
244
+ assert regscale_issues == ["objects"]
245
+ assert regscale_attachments == {"attachments": ["attachment"]}
246
+
247
+ @patch(f"{PATH}.Task.get_all_by_parent")
248
+ def test_get_regscale_data_and_attachments_sync_tasks_only(self, mock_get_all_by_parent):
249
+ """Test getting RegScale data and no attachments"""
250
+ mock_get_all_by_parent.return_value = ["objects"]
251
+ regscale_issues, regscale_attachments = get_regscale_data_and_attachments(
252
+ parent_id=self.PARENT_ID,
253
+ parent_module=self.PARENT_MODULE,
254
+ sync_attachments=False,
255
+ sync_tasks_only=True,
256
+ )
257
+ assert regscale_issues == ["objects"]
258
+ assert regscale_attachments == []
259
+
260
+ @patch(f"{PATH}.Issue.get_objects_and_attachments_by_parent")
261
+ def test_get_regscale_data_and_attachments_sync_attachments_only(self, mock_get_objects_and_attachments_by_parent):
262
+ """Test getting RegScale attachments only"""
263
+ mock_get_objects_and_attachments_by_parent.return_value = (["objects"], {"attachments": ["attachment"]})
264
+ regscale_issues, regscale_attachments = get_regscale_data_and_attachments(
265
+ parent_id=self.PARENT_ID,
266
+ parent_module=self.PARENT_MODULE,
267
+ sync_attachments=True,
268
+ sync_tasks_only=False,
269
+ )
270
+ assert regscale_issues == ["objects"]
271
+ assert regscale_attachments == {"attachments": ["attachment"]}
272
+
273
+ @patch(f"{PATH}.Issue.get_all_by_parent")
274
+ def test_get_regscale_data_and_attachments_sync_issues_only(self, mock_get_all_by_parent):
275
+ """Test getting RegScale issues only"""
276
+ mock_get_all_by_parent.return_value = ["objects"]
277
+ regscale_issues, regscale_attachments = get_regscale_data_and_attachments(
278
+ parent_id=self.PARENT_ID,
279
+ parent_module=self.PARENT_MODULE,
280
+ sync_attachments=False,
281
+ sync_tasks_only=False,
282
+ )
283
+ assert regscale_issues == ["objects"]
284
+ assert regscale_attachments == []
285
+
286
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
287
+ def test_sync_regscale_and_jira(self, mock_check_license, fetch_attachments):
288
+ """Test the entire Jira & RegScale sync process"""
289
+ mock_check_license.return_value.config = self.config
290
+ # Add thread_manager to the mock Application
291
+ mock_check_license.return_value.thread_manager = MagicMock()
292
+ try:
293
+ sync_regscale_and_jira(
294
+ parent_id=self.PARENT_ID,
295
+ parent_module=self.PARENT_MODULE,
296
+ jira_project=self.JIRA_PROJECT,
297
+ jira_issue_type="Bug",
298
+ sync_attachments=fetch_attachments,
299
+ )
300
+ except Exception as e:
301
+ pytest.fail("Jira & RegScale sync failed: {}".format(e))
302
+
303
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
304
+ def test_sync_regscale_and_jira_tasks(self, mock_check_license, fetch_attachments):
305
+ """Test the entire Jira & RegScale task sync process"""
306
+ mock_check_license.return_value.config = self.config
307
+ # Add thread_manager to the mock Application
308
+ mock_check_license.return_value.thread_manager = MagicMock()
309
+ try:
310
+ sync_regscale_and_jira(
311
+ parent_id=self.PARENT_ID,
312
+ parent_module=self.PARENT_MODULE,
313
+ jira_project=self.JIRA_PROJECT,
314
+ jira_issue_type="Task",
315
+ sync_attachments=fetch_attachments,
316
+ sync_tasks_only=True,
317
+ )
318
+ except Exception as e:
319
+ pytest.fail("Jira & RegScale task sync failed: {}".format(e))
320
+
321
+ @patch(f"{PATH}.sync_regscale_objects_to_jira")
322
+ @patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
323
+ @patch(f"{PATH}.create_jira_client")
324
+ @patch(f"{PATH}.fetch_jira_objects", return_value=[])
325
+ @patch(f"{PATH}.get_regscale_data_and_attachments", return_value=([], {}))
326
+ @patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
327
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
328
+ def test_sync_regscale_and_jira_no_updates(
329
+ self,
330
+ mock_check_license,
331
+ mock_api,
332
+ mock_get_regscale_data_and_attachments,
333
+ mock_fetch_jira_objects,
334
+ mock_create_jira_client,
335
+ mock_sync_regscale_to_jira,
336
+ mock_sync_regscale_objects_to_jira,
337
+ fetch_attachments,
338
+ ):
339
+ """Test sync_regscale_and_jira without updates from either side"""
340
+ # mock jira client so we can check it was correctly used later
341
+ mock_jira_client = MagicMock()
342
+ mock_create_jira_client.return_value = mock_jira_client
343
+
344
+ sync_regscale_and_jira(
345
+ parent_id=self.PARENT_ID,
346
+ parent_module=self.PARENT_MODULE,
347
+ jira_project=self.JIRA_PROJECT,
348
+ jira_issue_type="Bug",
349
+ sync_attachments=fetch_attachments,
350
+ )
351
+
352
+ # check that we get the jira client correctly
353
+ mock_create_jira_client.assert_called_once()
354
+
355
+ # check that we correctly fetch objects from jira and regscale
356
+ mock_get_regscale_data_and_attachments.assert_called_once_with(
357
+ parent_id=self.PARENT_ID,
358
+ parent_module=self.PARENT_MODULE,
359
+ sync_attachments=fetch_attachments,
360
+ sync_tasks_only=False,
361
+ )
362
+ mock_fetch_jira_objects.assert_called_once_with(
363
+ jira_client=mock_jira_client,
364
+ jira_project=self.JIRA_PROJECT,
365
+ jql_str="project = 'SNES'",
366
+ jira_issue_type="Bug",
367
+ sync_tasks_only=False,
368
+ )
369
+
370
+ # check that no updates were made because we did not find any objects from jira/regscale
371
+ mock_sync_regscale_to_jira.assert_not_called()
372
+ mock_sync_regscale_objects_to_jira.assert_not_called()
373
+
374
+ @patch(f"{PATH}.sync_regscale_objects_to_jira")
375
+ @patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
376
+ @patch(f"{PATH}.create_jira_client")
377
+ @patch(f"{PATH}.fetch_jira_objects")
378
+ @patch(f"{PATH}.get_regscale_data_and_attachments")
379
+ @patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
380
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
381
+ def test_sync_regscale_and_jira_updates(
382
+ self,
383
+ mock_check_license,
384
+ mock_api,
385
+ mock_get_regscale_data_and_attachments,
386
+ mock_fetch_jira_objects,
387
+ mock_create_jira_client,
388
+ mock_sync_regscale_to_jira,
389
+ mock_sync_regscale_objects_to_jira,
390
+ fetch_attachments,
391
+ ):
392
+ """Test sync_regscale_and_jira with updates from both sides"""
393
+ # mock jira client so we can check it was correctly used later
394
+ mock_jira_client = MagicMock()
395
+ mock_create_jira_client.return_value = mock_jira_client
396
+
397
+ # Setup mock config with jiraCustomFields returning empty dict
398
+ mock_config = MagicMock()
399
+ mock_config.get.return_value = {}
400
+ mock_check_license.return_value.config = mock_config
401
+
402
+ # mock these so that we can control what objects were returned to check later
403
+ mock_fetch_jira_objects.return_value = MagicMock()
404
+ mock_get_regscale_data_and_attachments.return_value = (MagicMock(), MagicMock())
405
+
406
+ sync_regscale_and_jira(
407
+ parent_id=self.PARENT_ID,
408
+ parent_module=self.PARENT_MODULE,
409
+ jira_project=self.JIRA_PROJECT,
410
+ jira_issue_type="Bug",
411
+ sync_attachments=fetch_attachments,
412
+ )
413
+
414
+ # check that we get the jira client correctly
415
+ mock_create_jira_client.assert_called_once()
416
+
417
+ # check that we correctly fetch objects from jira and regscale
418
+ mock_get_regscale_data_and_attachments.assert_called_once_with(
419
+ parent_id=self.PARENT_ID,
420
+ parent_module=self.PARENT_MODULE,
421
+ sync_attachments=fetch_attachments,
422
+ sync_tasks_only=False,
423
+ )
424
+ mock_fetch_jira_objects.assert_called_once_with(
425
+ jira_client=mock_jira_client,
426
+ jira_project=self.JIRA_PROJECT,
427
+ jql_str="project = 'SNES'",
428
+ jira_issue_type="Bug",
429
+ sync_tasks_only=False,
430
+ )
431
+
432
+ # check that updates were made with correct objects
433
+ mock_sync_regscale_to_jira.assert_called_once_with(
434
+ regscale_objects=mock_get_regscale_data_and_attachments.return_value[0],
435
+ jira_client=mock_jira_client,
436
+ jira_project=self.JIRA_PROJECT,
437
+ jira_issue_type="Bug",
438
+ api=mock_api.return_value,
439
+ sync_attachments=fetch_attachments,
440
+ attachments=mock_get_regscale_data_and_attachments.return_value[1],
441
+ custom_fields={},
442
+ )
443
+ mock_sync_regscale_objects_to_jira.assert_called_once_with(
444
+ mock_fetch_jira_objects.return_value,
445
+ mock_get_regscale_data_and_attachments.return_value[0],
446
+ fetch_attachments,
447
+ mock_check_license.return_value,
448
+ self.PARENT_ID,
449
+ self.PARENT_MODULE,
450
+ False,
451
+ False,
452
+ )
453
+
454
+ @patch(f"{PATH}.sync_regscale_objects_to_jira")
455
+ @patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
456
+ @patch(f"{PATH}.create_jira_client")
457
+ @patch(f"{PATH}.fetch_jira_objects")
458
+ @patch(f"{PATH}.get_regscale_data_and_attachments")
459
+ @patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
460
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
461
+ def test_sync_regscale_and_jira_custom_jql(
462
+ self,
463
+ mock_check_license,
464
+ mock_api,
465
+ mock_get_regscale_data_and_attachments,
466
+ mock_fetch_jira_objects,
467
+ mock_create_jira_client,
468
+ mock_sync_regscale_to_jira,
469
+ mock_sync_regscale_objects_to_jira,
470
+ fetch_attachments,
471
+ ):
472
+ """Test sync_regscale_and_jira with custom JQL query"""
473
+ # mock jira client so we can check it was correctly used later
474
+ mock_jira_client = MagicMock()
475
+ mock_create_jira_client.return_value = mock_jira_client
476
+
477
+ # mock these so that we can control what objects were returned to check later
478
+ mock_fetch_jira_objects.return_value = []
479
+ mock_get_regscale_data_and_attachments.return_value = ([], {})
480
+
481
+ custom_jql = "project = SNES AND assignee = currentUser() AND status != Closed"
482
+
483
+ sync_regscale_and_jira(
484
+ parent_id=self.PARENT_ID,
485
+ parent_module=self.PARENT_MODULE,
486
+ jira_project=self.JIRA_PROJECT,
487
+ jira_issue_type="Bug",
488
+ sync_attachments=fetch_attachments,
489
+ jql=custom_jql,
490
+ )
491
+
492
+ # check that we get the jira client correctly
493
+ mock_create_jira_client.assert_called_once()
494
+
495
+ # check that we correctly fetch objects from jira and regscale
496
+ mock_get_regscale_data_and_attachments.assert_called_once_with(
497
+ parent_id=self.PARENT_ID,
498
+ parent_module=self.PARENT_MODULE,
499
+ sync_attachments=fetch_attachments,
500
+ sync_tasks_only=False,
501
+ )
502
+ # Verify that the custom JQL was used instead of the default
503
+ mock_fetch_jira_objects.assert_called_once_with(
504
+ jira_client=mock_jira_client,
505
+ jira_project=self.JIRA_PROJECT,
506
+ jql_str=custom_jql,
507
+ jira_issue_type="Bug",
508
+ sync_tasks_only=False,
509
+ )
510
+
511
+ @patch(f"{PATH}.sync_regscale_objects_to_jira")
512
+ @patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
513
+ @patch(f"{PATH}.create_jira_client")
514
+ @patch(f"{PATH}.fetch_jira_objects")
515
+ @patch(f"{PATH}.get_regscale_data_and_attachments")
516
+ @patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
517
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
518
+ def test_sync_regscale_and_jira_custom_jql_tasks(
519
+ self,
520
+ mock_check_license,
521
+ mock_api,
522
+ mock_get_regscale_data_and_attachments,
523
+ mock_fetch_jira_objects,
524
+ mock_create_jira_client,
525
+ mock_sync_regscale_to_jira,
526
+ mock_sync_regscale_objects_to_jira,
527
+ fetch_attachments,
528
+ ):
529
+ """Test sync_regscale_and_jira with custom JQL query for tasks"""
530
+ # mock jira client so we can check it was correctly used later
531
+ mock_jira_client = MagicMock()
532
+ mock_create_jira_client.return_value = mock_jira_client
533
+
534
+ # mock these so that we can control what objects were returned to check later
535
+ mock_fetch_jira_objects.return_value = []
536
+ mock_get_regscale_data_and_attachments.return_value = ([], {})
537
+
538
+ custom_jql = "project = SNES AND assignee = currentUser() AND issueType = Task"
539
+
540
+ sync_regscale_and_jira(
541
+ parent_id=self.PARENT_ID,
542
+ parent_module=self.PARENT_MODULE,
543
+ jira_project=self.JIRA_PROJECT,
544
+ jira_issue_type="Task",
545
+ sync_attachments=fetch_attachments,
546
+ sync_tasks_only=True,
547
+ jql=custom_jql,
548
+ )
549
+
550
+ # check that we get the jira client correctly
551
+ mock_create_jira_client.assert_called_once()
552
+
553
+ # check that we correctly fetch objects from jira and regscale
554
+ mock_get_regscale_data_and_attachments.assert_called_once_with(
555
+ parent_id=self.PARENT_ID,
556
+ parent_module=self.PARENT_MODULE,
557
+ sync_attachments=fetch_attachments,
558
+ sync_tasks_only=True,
559
+ )
560
+ # Verify that the custom JQL was used instead of the default task-specific JQL
561
+ mock_fetch_jira_objects.assert_called_once_with(
562
+ jira_client=mock_jira_client,
563
+ jira_project=self.JIRA_PROJECT,
564
+ jql_str=custom_jql,
565
+ jira_issue_type="Task",
566
+ sync_tasks_only=True,
567
+ )
568
+
569
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
570
+ def test_sync_regscale_objects_to_jira(
571
+ self, mock_check_license, fetch_attachments, get_jira_issue, regscale_issues_and_attachments
572
+ ):
573
+ """Test syncing RegScale objects to Jira"""
574
+ mock_check_license.return_value.config = self.config
575
+ try:
576
+ sync_regscale_objects_to_jira(
577
+ jira_issues=[get_jira_issue],
578
+ regscale_objects=regscale_issues_and_attachments[0],
579
+ sync_attachments=fetch_attachments,
580
+ app=self.app,
581
+ parent_id=self.PARENT_ID,
582
+ parent_module=self.PARENT_MODULE,
583
+ sync_tasks_only=False,
584
+ )
585
+ except Exception as e:
586
+ pytest.fail("Jira & RegScale task sync failed: {}".format(e))
587
+
588
+ @patch(f"{PATH}.create_jira_client")
589
+ @patch(f"{PATH}.create_and_update_regscale_issues")
590
+ @patch(f"{PATH}.create_and_update_regscale_tasks")
591
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
592
+ def test_sync_regscale_objects_to_jira_tasks_only(
593
+ self,
594
+ mock_check_license,
595
+ mock_create_and_update_regscale_tasks,
596
+ mock_create_and_update_regscale_issues,
597
+ mock_create_jira_client,
598
+ fetch_attachments,
599
+ ):
600
+ """Test syncing RegScale objects to Jira with sync_tasks_only=True"""
601
+ mock_check_license.return_value.config = self.config
602
+ mock_jira_client = MagicMock()
603
+ mock_create_jira_client.return_value = mock_jira_client
604
+ mock_create_and_update_regscale_tasks.return_value = (1, 0, 0)
605
+
606
+ sync_regscale_objects_to_jira(
607
+ MagicMock(),
608
+ MagicMock(),
609
+ fetch_attachments,
610
+ MagicMock(spec=Application),
611
+ self.PARENT_ID,
612
+ self.PARENT_MODULE,
613
+ True,
614
+ )
615
+
616
+ mock_create_and_update_regscale_tasks.assert_called_once()
617
+ mock_create_and_update_regscale_issues.assert_not_called()
618
+
619
+ @patch(f"{PATH}.create_jira_client")
620
+ @patch(f"{PATH}.create_and_update_regscale_tasks")
621
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
622
+ def test_sync_regscale_objects_to_jira_all(
623
+ self,
624
+ mock_check_license,
625
+ mock_create_and_update_regscale_tasks,
626
+ mock_create_jira_client,
627
+ fetch_attachments,
628
+ ):
629
+ """Test syncing RegScale objects to Jira with sync_tasks_only=False"""
630
+ mock_check_license.return_value.config = self.config
631
+ mock_jira_client = MagicMock()
632
+ mock_create_jira_client.return_value = mock_jira_client
633
+
634
+ # Create mock application with ThreadManager
635
+ mock_app = MagicMock(spec=Application)
636
+ mock_app.config = self.config
637
+ mock_thread_manager = MagicMock()
638
+ mock_app.thread_manager = mock_thread_manager
639
+
640
+ # Mock Jira issues
641
+ mock_jira_issues = [MagicMock(), MagicMock()]
642
+
643
+ sync_regscale_objects_to_jira(
644
+ mock_jira_issues,
645
+ MagicMock(),
646
+ fetch_attachments,
647
+ mock_app,
648
+ self.PARENT_ID,
649
+ self.PARENT_MODULE,
650
+ False,
651
+ )
652
+
653
+ mock_create_and_update_regscale_tasks.assert_not_called()
654
+ # Verify ThreadManager methods were called
655
+ mock_thread_manager.submit_tasks_from_list.assert_called_once()
656
+ mock_thread_manager.execute_and_verify.assert_called_once()
657
+
658
+ @patch(f"{PATH}.JIRA")
659
+ def test_create_jira_client_basic(self, mock_jira):
660
+ """Test creating a Jira client"""
661
+ conf = {
662
+ "jiraUrl": "https://example.com",
663
+ "jiraApiToken": "token",
664
+ "jiraUserName": "user",
665
+ }
666
+ _ = create_jira_client(conf, token_auth=False)
667
+ mock_jira.assert_called_once_with(basic_auth=("user", "token"), options={"server": "https://example.com"})
668
+
669
+ @patch(f"{PATH}.JIRA")
670
+ def test_create_jira_client_token(self, mock_jira):
671
+ """Test creating a Jira client with token auth"""
672
+ from regscale.integrations.variables import ScannerVariables
673
+
674
+ conf = {
675
+ "jiraUrl": "https://example.com",
676
+ "jiraApiToken": "token",
677
+ "jiraUserName": "user",
678
+ }
679
+ _ = create_jira_client(conf, token_auth=True)
680
+ mock_jira.assert_called_once_with(
681
+ token_auth="token", options={"server": "https://example.com", "verify": ScannerVariables.sslVerify}
682
+ )
683
+
684
+ @staticmethod
685
+ def test_jira_issues(jira_issues, fetch_attachments):
686
+ """Test fetching Jira issues and creating a Jira client"""
687
+ has_attachments = None
688
+ if fetch_attachments:
689
+ assert jira_issues is not None
690
+ assert True in [True if issue.fields.attachment else False for issue in jira_issues]
691
+ else:
692
+ assert jira_issues is not None
693
+ try:
694
+ # try to access the attachment attribute
695
+ has_attachments = [True if issue.fields.attachment else False for issue in jira_issues]
696
+ assert len(has_attachments) == len(jira_issues)
697
+ except AttributeError:
698
+ # if the attribute doesn't exist, then we know there are no attachments
699
+ assert has_attachments is None
700
+
701
+ @staticmethod
702
+ def test_jira_tasks(jira_tasks, fetch_attachments):
703
+ """Test fetching Jira tasks and creating a Jira client"""
704
+ has_attachments = None
705
+ if fetch_attachments:
706
+ assert jira_tasks is not None
707
+ assert True in [True if task.fields.attachment else False for task in jira_tasks]
708
+ else:
709
+ assert jira_tasks is not None
710
+ try:
711
+ # try to access the attachment attribute
712
+ has_attachments = [True if task.fields.attachment else False for task in jira_tasks]
713
+ assert len(has_attachments) == len(jira_tasks)
714
+ except AttributeError:
715
+ # if the attribute doesn't exist, then we know there are no attachments
716
+ assert has_attachments is None
717
+
718
+ @staticmethod
719
+ def test_fetch_regscale_issues_and_attachments(regscale_issues_and_attachments, fetch_attachments):
720
+ """Test fetching RegScale issues and attachments"""
721
+ issues, attachments = regscale_issues_and_attachments
722
+ assert issues is not None
723
+ if fetch_attachments:
724
+ assert attachments is not None
725
+ else:
726
+ assert attachments == []
727
+
728
+ @staticmethod
729
+ def test_fetch_regscale_tasks_and_attachments(regscale_tasks_and_attachments, fetch_attachments):
730
+ """Test fetching RegScale tasks and attachments"""
731
+ tasks, attachments = regscale_tasks_and_attachments
732
+ assert tasks is not None
733
+ if fetch_attachments:
734
+ assert attachments != []
735
+ else:
736
+ assert attachments == []
737
+
738
+ @pytest.mark.parametrize(
739
+ "due_date,priority,expected_days",
740
+ [
741
+ ("2024-12-31", "High", 7), # Has due date
742
+ (None, "High", 7), # No due date, high priority
743
+ (None, "Medium", 14), # No due date, medium priority
744
+ (None, "Low", 30), # No due date, low priority
745
+ (None, None, 14), # No due date, no priority (defaults to medium)
746
+ ],
747
+ )
748
+ @patch(f"{PATH}.datetime")
749
+ def test_map_jira_due_date(self, mock_datetime, due_date, priority, expected_days):
750
+ """Test mapping Jira due dates to RegScale format"""
751
+ # Set up mock datetime to return a fixed date
752
+ fixed_date = datetime(2024, 1, 1, 12, 0, 0)
753
+ mock_datetime.now.return_value = fixed_date
754
+ mock_datetime.timedelta = timedelta # Allow timedelta to work normally
755
+
756
+ # Create mock Jira issue
757
+ mock_issue = MagicMock()
758
+ mock_issue.fields.duedate = due_date
759
+ if priority:
760
+ mock_issue.fields.priority = MagicMock()
761
+ mock_issue.fields.priority.name = priority
762
+ else:
763
+ mock_issue.fields.priority = None
764
+
765
+ # Create mock config
766
+ mock_config = {"issues": {"jira": {"high": 7, "medium": 14, "low": 30}}}
767
+
768
+ result = map_jira_due_date(mock_issue, mock_config)
769
+
770
+ if due_date:
771
+ assert result == due_date
772
+ else:
773
+ # Calculate expected date using the same fixed date
774
+ expected_date = fixed_date + timedelta(days=expected_days)
775
+ result_date = datetime.strptime(result, "%Y-%m-%d %H:%M:%S")
776
+ assert result_date.date() == expected_date.date()
777
+
778
+ @pytest.mark.parametrize(
779
+ "status,expected_status",
780
+ [
781
+ ("Done", "Closed"),
782
+ ("In Progress", "Open"),
783
+ ("To Do", "Open"),
784
+ ],
785
+ )
786
+ def test_map_jira_to_regscale_issue(self, status, expected_status):
787
+ """Test mapping Jira issues to RegScale format"""
788
+ # Create mock Jira issue
789
+ issue_status = MagicMock()
790
+ issue_status.name = status
791
+ mock_issue = MagicMock(
792
+ key="TEST-123",
793
+ fields=MagicMock(
794
+ summary="Skipped task",
795
+ description="Skipped task description",
796
+ status=issue_status,
797
+ duedate=None,
798
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
799
+ ),
800
+ )
801
+ mock_issue.fields.priority = MagicMock()
802
+ mock_issue.fields.priority.name = "High"
803
+
804
+ # Create mock config
805
+ mock_config = {
806
+ "userId": "1", # Convert to string to match Optional[str] type
807
+ "issues": {"jira": {"status": "Open", "high": 7, "medium": 14, "low": 30}},
808
+ }
809
+
810
+ result = map_jira_to_regscale_issue(mock_issue, mock_config, 1, "issues")
811
+
812
+ # Verify the Issue object was created with correct attributes
813
+ assert isinstance(result, Issue)
814
+ assert result.title == "Skipped task"
815
+ assert "Skipped task description" in result.description
816
+ assert result.status == expected_status
817
+ assert result.jiraId == "TEST-123"
818
+ assert result.parentId == 1
819
+ assert result.parentModule == "issues"
820
+ assert result.dueDate is not None # Due date will be calculated based on priority
821
+ if status == "Done":
822
+ assert result.dateCompleted is not None
823
+ else:
824
+ assert result.dateCompleted is None
825
+
826
+ @pytest.mark.parametrize(
827
+ "status,expected_status,expected_percent_complete,expected_date_closed",
828
+ [
829
+ ("Done", "Closed", 100, True),
830
+ ("In Progress", "Open", None, False),
831
+ ("To Do", "Backlog", None, False),
832
+ ],
833
+ )
834
+ def test_create_regscale_task_from_jira(
835
+ self, status, expected_status, expected_percent_complete, expected_date_closed
836
+ ):
837
+ """Test creating RegScale tasks from Jira issues"""
838
+ # Create mock Jira issue
839
+ mock_issue = MagicMock()
840
+ mock_issue.fields.summary = "Test Task"
841
+ mock_issue.fields.description = "Test Description"
842
+ mock_issue.fields.status.name = status
843
+ mock_issue.fields.duedate = "2024-12-31"
844
+ mock_issue.fields.statuscategorychangedate = "2024-01-01T12:00:00.000Z"
845
+ mock_issue.key = "TEST-123"
846
+
847
+ # Create mock config
848
+ mock_config = {"issues": {"jira": {"medium": 14}}}
849
+
850
+ result = create_regscale_task_from_jira(mock_config, mock_issue, 1, "issues")
851
+
852
+ assert result.title == "Test Task"
853
+ assert result.description == "Test Description"
854
+ assert result.status == expected_status
855
+ assert result.dueDate == "2024-12-31"
856
+ assert result.parentId == 1
857
+ assert result.parentModule == "issues"
858
+ assert result.otherIdentifier == "TEST-123"
859
+ if expected_percent_complete:
860
+ assert result.percentComplete == expected_percent_complete
861
+ if expected_date_closed:
862
+ assert result.dateClosed is not None
863
+ else:
864
+ assert result.dateClosed is None
865
+
866
+ def test_check_and_close_tasks(self):
867
+ """Test checking and closing tasks"""
868
+ jira_titles = {"Testing1234"}
869
+ tasks = [
870
+ Task(
871
+ id=3,
872
+ title="Different Title",
873
+ status="Backlog",
874
+ dueDate=get_current_datetime(),
875
+ dateClosed="",
876
+ percentComplete=0,
877
+ ),
878
+ Task(
879
+ id=4,
880
+ title="Testing1234",
881
+ status="Backlog",
882
+ dueDate=get_current_datetime(),
883
+ dateClosed="",
884
+ percentComplete=0,
885
+ ),
886
+ ]
887
+
888
+ closed_tasks = check_and_close_tasks(tasks, set(jira_titles))
889
+ assert len(closed_tasks) == 1
890
+ assert closed_tasks[0].status == "Closed"
891
+ assert closed_tasks[0].percentComplete == 100
892
+
893
+ def test_process_tasks_for_sync(self):
894
+ """Test processing tasks for sync"""
895
+ todo_status = MagicMock()
896
+ todo_status.name = "to do"
897
+ in_progress_status = MagicMock()
898
+ in_progress_status.name = "in progress"
899
+ done_status = MagicMock()
900
+ done_status.name = "done"
901
+ # Create mock Jira issues
902
+ jira_tasks = [
903
+ MagicMock( # should be skipped (up to date - nothing happens)
904
+ key="JIRA-1",
905
+ fields=MagicMock(
906
+ summary="Skipped task",
907
+ description="Skipped task description",
908
+ status=todo_status,
909
+ duedate=None,
910
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
911
+ priority=None,
912
+ ),
913
+ ),
914
+ MagicMock( # should be inserted (task in jira but not regscale)
915
+ key="JIRA-2",
916
+ fields=MagicMock(
917
+ summary="New task",
918
+ description="New task description",
919
+ status=todo_status,
920
+ duedate=None,
921
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
922
+ priority=None,
923
+ ),
924
+ ),
925
+ MagicMock( # should be updated (task in both but out of sync)
926
+ key="JIRA-3",
927
+ fields=MagicMock(
928
+ summary="Existing task",
929
+ description="Existing task description",
930
+ status=in_progress_status,
931
+ duedate=None,
932
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
933
+ priority=None,
934
+ ),
935
+ ),
936
+ MagicMock( # should be updated
937
+ key="JIRA-4",
938
+ fields=MagicMock(
939
+ summary="Existing task",
940
+ description="Existing task description",
941
+ status=todo_status,
942
+ duedate=None,
943
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
944
+ priority=None,
945
+ ),
946
+ ),
947
+ MagicMock( # should be closed
948
+ key="JIRA-5",
949
+ fields=MagicMock(
950
+ summary="Existing task",
951
+ description="Existing task description",
952
+ status=done_status,
953
+ duedate=None,
954
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
955
+ priority=None,
956
+ ),
957
+ ),
958
+ MagicMock( # date to be updated
959
+ key="JIRA-6",
960
+ fields=MagicMock(
961
+ summary="Existing task",
962
+ description="Existing task description",
963
+ status=todo_status,
964
+ duedate="2024-12-31",
965
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
966
+ priority=None,
967
+ ),
968
+ ),
969
+ ]
970
+ regscale_tasks = [
971
+ Task( # matches JIRA-1 (should be skipped - up to date)
972
+ id=1,
973
+ title="Skipped task",
974
+ status="Backlog",
975
+ description="Skipped task description",
976
+ otherIdentifier="JIRA-1",
977
+ parentId=self.PARENT_ID,
978
+ parentModule=self.PARENT_MODULE,
979
+ dueDate=get_current_datetime(),
980
+ ),
981
+ Task( # matches JIRA-3 (should be updated - task in both but out of sync)
982
+ id=3,
983
+ title="Existing task",
984
+ status="Open",
985
+ description="Different description", # Different description to show sync needed
986
+ otherIdentifier="JIRA-3",
987
+ parentId=self.PARENT_ID,
988
+ parentModule=self.PARENT_MODULE,
989
+ dueDate=get_current_datetime(),
990
+ ),
991
+ Task( # matches JIRA-4 (should be updated - regscale closed but jira open)
992
+ id=4,
993
+ title="Existing task",
994
+ status="Closed", # Closed in RegScale but open in Jira
995
+ description="Existing task description",
996
+ otherIdentifier="JIRA-4",
997
+ parentId=self.PARENT_ID,
998
+ parentModule=self.PARENT_MODULE,
999
+ dueDate=get_current_datetime(),
1000
+ ),
1001
+ Task( # matches JIRA-5 (should be closed - jira closed but regscale open)
1002
+ id=5,
1003
+ title="Existing task",
1004
+ status="Open", # Open in RegScale but closed in Jira
1005
+ description="Existing task description",
1006
+ otherIdentifier="JIRA-5",
1007
+ parentId=self.PARENT_ID,
1008
+ parentModule=self.PARENT_MODULE,
1009
+ dueDate=get_current_datetime(),
1010
+ ),
1011
+ Task(
1012
+ id=6,
1013
+ title="New task",
1014
+ status="Backlog",
1015
+ description="Not in jira",
1016
+ otherIdentifier="JIRA-19",
1017
+ parentId=self.PARENT_ID,
1018
+ parentModule=self.PARENT_MODULE,
1019
+ ),
1020
+ Task(
1021
+ id=7,
1022
+ title="Existing task",
1023
+ status="Backlog",
1024
+ description="Existing task description",
1025
+ otherIdentifier="JIRA-6",
1026
+ parentId=self.PARENT_ID,
1027
+ parentModule=self.PARENT_MODULE,
1028
+ dueDate="2023-01-01",
1029
+ ),
1030
+ ]
1031
+
1032
+ progress = MagicMock(spec=Progress)
1033
+ progress_task = MagicMock()
1034
+
1035
+ insert_tasks, update_tasks, close_tasks = process_tasks_for_sync(
1036
+ config=self.config,
1037
+ jira_issues=jira_tasks,
1038
+ existing_tasks=regscale_tasks,
1039
+ parent_id=self.PARENT_ID,
1040
+ parent_module=self.PARENT_MODULE,
1041
+ progress=progress,
1042
+ progress_task=progress_task,
1043
+ )
1044
+
1045
+ # Assertions
1046
+ assert len(insert_tasks) == 1 # New task should be inserted
1047
+ assert len(update_tasks) == 3 # No updates needed
1048
+ assert len(close_tasks) == 1 # Existing task should be closed
1049
+
1050
+ @patch(f"{PATH}.process_tasks_for_sync")
1051
+ @patch(f"{PATH}.task_and_attachments_sync")
1052
+ @patch("regscale.core.app.api.Api")
1053
+ @patch(f"{PATH}.ThreadPoolExecutor")
1054
+ @patch(f"{PATH}.as_completed")
1055
+ def test_create_and_update_regscale_tasks(
1056
+ self, mock_as_completed, mock_thread_pool, mock_api, mock_task_sync, mock_process_tasks_for_sync
1057
+ ):
1058
+ """Test creating and updating RegScale tasks from Jira tasks"""
1059
+ # Setup mock tasks
1060
+ insert_tasks = [
1061
+ Task(
1062
+ id=1,
1063
+ title="New task",
1064
+ status="Backlog",
1065
+ description="New task description",
1066
+ otherIdentifier="JIRA-19",
1067
+ parentId=self.PARENT_ID,
1068
+ parentModule=self.PARENT_MODULE,
1069
+ )
1070
+ ]
1071
+ update_tasks = [
1072
+ Task(
1073
+ id=2,
1074
+ title="Existing task",
1075
+ status="Open",
1076
+ description="Existing task description",
1077
+ otherIdentifier="JIRA-3",
1078
+ parentId=self.PARENT_ID,
1079
+ parentModule=self.PARENT_MODULE,
1080
+ )
1081
+ ]
1082
+ close_tasks = [
1083
+ Task(
1084
+ id=3,
1085
+ title="Existing task",
1086
+ status="Closed",
1087
+ description="Existing task description",
1088
+ otherIdentifier="JIRA-5",
1089
+ parentId=self.PARENT_ID,
1090
+ parentModule=self.PARENT_MODULE,
1091
+ )
1092
+ ]
1093
+ mock_process_tasks_for_sync.return_value = (insert_tasks, update_tasks, close_tasks)
1094
+
1095
+ mock_api_instance = MagicMock()
1096
+ mock_api.return_value = mock_api_instance
1097
+ mock_api_instance.app.config = self.config
1098
+
1099
+ # Setup mock thread pool
1100
+ mock_executor = MagicMock()
1101
+ mock_thread_pool.return_value.__enter__.return_value = mock_executor
1102
+
1103
+ # Setup task_and_attachments_sync to return None
1104
+ mock_task_sync.return_value = None
1105
+
1106
+ # Make as_completed return an empty iterator
1107
+ mock_as_completed.return_value = []
1108
+
1109
+ inserted, updated, closed = create_and_update_regscale_tasks(
1110
+ jira_issues=[],
1111
+ existing_tasks=[],
1112
+ jira_client=MagicMock(),
1113
+ parent_id=self.PARENT_ID,
1114
+ parent_module=self.PARENT_MODULE,
1115
+ progress=MagicMock(spec=Progress),
1116
+ progress_task=MagicMock(),
1117
+ )
1118
+
1119
+ assert inserted == 1
1120
+ assert updated == 1
1121
+ assert closed == 1
1122
+
1123
+ mock_thread_pool.assert_called_once_with(max_workers=10)
1124
+ assert mock_executor.submit.call_count == 3
1125
+
1126
+ @patch(f"{PATH}.compare_files_for_dupes_and_upload")
1127
+ def test_tasks_and_attachments_sync_create(self, mock_compare_files_for_dupes_and_upload):
1128
+ """Test performing create operation on tasks in task_and_attachments_sync"""
1129
+ # check if operation fails
1130
+ mock_task = MagicMock()
1131
+ mock_task.create.return_value = None
1132
+ task_and_attachments_sync(
1133
+ operation="create",
1134
+ task=mock_task,
1135
+ jira_client=MagicMock(),
1136
+ api=MagicMock(),
1137
+ )
1138
+ mock_task.create.assert_called_once()
1139
+ mock_compare_files_for_dupes_and_upload.assert_not_called()
1140
+
1141
+ # check if operation is successful
1142
+ mock_task.create.reset_mock()
1143
+ mock_task.create.return_value = MagicMock()
1144
+ task_and_attachments_sync(
1145
+ operation="create",
1146
+ task=mock_task,
1147
+ jira_client=MagicMock(),
1148
+ api=MagicMock(),
1149
+ )
1150
+ mock_task.create.assert_called_once()
1151
+ mock_compare_files_for_dupes_and_upload.assert_called_once()
1152
+
1153
+ @pytest.mark.parametrize("operation", ["update", "close"])
1154
+ @patch(f"{PATH}.compare_files_for_dupes_and_upload")
1155
+ def test_tasks_and_attachments_sync_save(self, mock_compare_files_for_dupes_and_upload, operation):
1156
+ """Test performing save operation on tasks in task_and_attachments_sync"""
1157
+ # check if operation fails
1158
+ mock_task = MagicMock()
1159
+ mock_task.save.return_value = None
1160
+ task_and_attachments_sync(
1161
+ operation=operation,
1162
+ task=mock_task,
1163
+ jira_client=MagicMock(),
1164
+ api=MagicMock(),
1165
+ )
1166
+ mock_task.save.assert_called_once()
1167
+ mock_compare_files_for_dupes_and_upload.assert_not_called()
1168
+
1169
+ # check if operation is successful
1170
+ mock_task.save.reset_mock()
1171
+ mock_task.save.return_value = MagicMock()
1172
+ task_and_attachments_sync(
1173
+ operation=operation,
1174
+ task=mock_task,
1175
+ jira_client=MagicMock(),
1176
+ api=MagicMock(),
1177
+ )
1178
+ mock_task.save.assert_called_once()
1179
+ mock_compare_files_for_dupes_and_upload.assert_called_once()
1180
+
1181
+ @patch(f"{PATH}.compare_files_for_dupes_and_upload")
1182
+ @patch(f"{PATH}.map_jira_to_regscale_issue")
1183
+ @patch(f"{PATH}.Issue.save")
1184
+ @patch(f"{PATH}.job_progress", return_value=MagicMock(spec=Progress))
1185
+ def test_create_and_update_regscale_issues(
1186
+ self,
1187
+ mock_job_progress_object,
1188
+ mock_update_issue,
1189
+ mock_map_jira_to_regscale_issue,
1190
+ mock_compare_files_for_dupes_and_upload,
1191
+ fetch_attachments,
1192
+ ):
1193
+ """Test creating and updating RegScale issues from Jira issues"""
1194
+ open_status = MagicMock()
1195
+ open_status.name = "open"
1196
+ in_progress_status = MagicMock()
1197
+ in_progress_status.name = "in progress"
1198
+ closed_status = MagicMock()
1199
+ closed_status.name = "done"
1200
+
1201
+ highest_priority = MagicMock()
1202
+ highest_priority.name = "highest"
1203
+ high_priority = MagicMock()
1204
+ high_priority.name = "high"
1205
+ medium_priority = MagicMock()
1206
+ medium_priority.name = "medium"
1207
+ low_priority = MagicMock()
1208
+ low_priority.name = "low"
1209
+ lowest_priority = MagicMock()
1210
+ lowest_priority.name = "lowest"
1211
+
1212
+ # Create mock Jira issues
1213
+ jira_issue_1 = MagicMock( # should be updated (existing issue)
1214
+ key="JIRA-1",
1215
+ fields=MagicMock(
1216
+ summary="Skipped issue",
1217
+ description="Skipped issue description",
1218
+ status=open_status,
1219
+ duedate=None,
1220
+ priority=highest_priority,
1221
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1222
+ attachment=[MagicMock()], # Has attachments
1223
+ ),
1224
+ )
1225
+ jira_issue_2 = MagicMock( # should be inserted (issue in jira but not regscale)
1226
+ key="JIRA-2",
1227
+ fields=MagicMock(
1228
+ summary="New issue",
1229
+ description="New issue description",
1230
+ status=open_status,
1231
+ duedate=None,
1232
+ priority=medium_priority,
1233
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1234
+ attachment=None,
1235
+ ),
1236
+ )
1237
+ jira_issue_3 = MagicMock( # should be updated (issue in both but out of sync)
1238
+ key="JIRA-3",
1239
+ fields=MagicMock(
1240
+ summary="Existing issue",
1241
+ description="Existing issue description",
1242
+ status=in_progress_status,
1243
+ duedate=None,
1244
+ priority=low_priority,
1245
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1246
+ attachment=None, # No attachments
1247
+ ),
1248
+ )
1249
+ jira_issue_4 = MagicMock( # should be closed - counts as updated
1250
+ key="JIRA-4",
1251
+ fields=MagicMock(
1252
+ summary="Existing issue",
1253
+ description="Existing issue description",
1254
+ status=closed_status,
1255
+ duedate=None,
1256
+ priority=lowest_priority,
1257
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1258
+ attachment=None, # No attachments
1259
+ ),
1260
+ )
1261
+
1262
+ # Create RegScale issues
1263
+ regscale_issues = [
1264
+ Issue( # matches JIRA-1 (should be updated)
1265
+ id=1,
1266
+ title="Skipped issue",
1267
+ status="Open",
1268
+ description="Skipped issue description",
1269
+ jiraId="JIRA-1",
1270
+ parentId=self.PARENT_ID,
1271
+ parentModule=self.PARENT_MODULE,
1272
+ dueDate=get_current_datetime(),
1273
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1274
+ ),
1275
+ Issue( # matches JIRA-3 (should be updated - issue in both but out of sync)
1276
+ id=3,
1277
+ title="Existing issue",
1278
+ status="Open",
1279
+ description="Different description", # Different description to show sync needed
1280
+ jiraId="JIRA-3",
1281
+ parentId=self.PARENT_ID,
1282
+ parentModule=self.PARENT_MODULE,
1283
+ dueDate=get_current_datetime(),
1284
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1285
+ ),
1286
+ Issue( # matches JIRA-4 (should be closed - jira closed but regscale open)
1287
+ id=4,
1288
+ title="Existing issue",
1289
+ status="Open", # Open in RegScale but closed in Jira
1290
+ description="Existing issue description",
1291
+ jiraId="JIRA-4",
1292
+ parentId=self.PARENT_ID,
1293
+ parentModule=self.PARENT_MODULE,
1294
+ dueDate=get_current_datetime(),
1295
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1296
+ ),
1297
+ ]
1298
+
1299
+ # Create mock config with priority mappings from init.yaml
1300
+ config = {
1301
+ "issues": {"jira": {"highest": 7, "high": 30, "medium": 90, "low": 180, "lowest": 365, "status": "Open"}},
1302
+ "maxThreads": 4,
1303
+ "userId": "123e4567-e89b-12d3-a456-426614174000",
1304
+ }
1305
+ app = MagicMock()
1306
+ app.config = config
1307
+
1308
+ # Setup mock return values
1309
+ mock_update_issue.return_value = MagicMock()
1310
+
1311
+ # Mock the creation of a new issue
1312
+ created_issue_mock = MagicMock()
1313
+ created_issue_mock.id = 2
1314
+ created_issue_mock.create.return_value = created_issue_mock
1315
+ mock_map_jira_to_regscale_issue.return_value = created_issue_mock
1316
+
1317
+ with mock_job_progress_object as job_progress:
1318
+ test_task = job_progress.add_task(
1319
+ description="Processing issues",
1320
+ total=4,
1321
+ visible=False,
1322
+ )
1323
+
1324
+ # Test the function with each Jira issue individually (as ThreadManager would call it)
1325
+ # JIRA-1: existing issue with matching jiraId and attachments
1326
+ create_and_update_regscale_issues(
1327
+ jira_issue_1,
1328
+ regscale_issues,
1329
+ False,
1330
+ fetch_attachments,
1331
+ MagicMock(),
1332
+ app,
1333
+ self.PARENT_ID,
1334
+ self.PARENT_MODULE,
1335
+ test_task,
1336
+ job_progress,
1337
+ )
1338
+
1339
+ # JIRA-2: new issue (not in regscale_issues)
1340
+ create_and_update_regscale_issues(
1341
+ jira_issue_2,
1342
+ regscale_issues,
1343
+ False,
1344
+ fetch_attachments,
1345
+ MagicMock(),
1346
+ app,
1347
+ self.PARENT_ID,
1348
+ self.PARENT_MODULE,
1349
+ test_task,
1350
+ job_progress,
1351
+ )
1352
+
1353
+ # JIRA-3: existing issue to be updated
1354
+ create_and_update_regscale_issues(
1355
+ jira_issue_3,
1356
+ regscale_issues,
1357
+ False,
1358
+ fetch_attachments,
1359
+ MagicMock(),
1360
+ app,
1361
+ self.PARENT_ID,
1362
+ self.PARENT_MODULE,
1363
+ test_task,
1364
+ job_progress,
1365
+ )
1366
+
1367
+ # JIRA-4: existing issue to be closed
1368
+ create_and_update_regscale_issues(
1369
+ jira_issue_4,
1370
+ regscale_issues,
1371
+ False,
1372
+ fetch_attachments,
1373
+ MagicMock(),
1374
+ app,
1375
+ self.PARENT_ID,
1376
+ self.PARENT_MODULE,
1377
+ test_task,
1378
+ job_progress,
1379
+ )
1380
+
1381
+ # Verify update_issue was called 3 times (JIRA-1, JIRA-3, JIRA-4)
1382
+ assert mock_update_issue.call_count == 3
1383
+ # Verify map_jira_to_regscale_issue was called once for JIRA-2 (new issue)
1384
+ assert mock_map_jira_to_regscale_issue.call_count == 1
1385
+ # Verify attachment handling
1386
+ if fetch_attachments:
1387
+ # Only JIRA-1 has attachments in fields.attachment
1388
+ assert mock_compare_files_for_dupes_and_upload.call_count == 1
1389
+ else:
1390
+ assert mock_compare_files_for_dupes_and_upload.call_count == 0
1391
+
1392
+ @patch(f"{PATH}.create_issue_in_jira")
1393
+ def test_sync_regscale_issues_to_jira(self, mock_create_issue_in_jira, fetch_attachments):
1394
+ """Test inserting Regscale issues into jira if they do not exist"""
1395
+
1396
+ # Create RegScale issues
1397
+ regscale_objects = [
1398
+ Issue(
1399
+ id=1,
1400
+ title="Test Issue",
1401
+ status="Open",
1402
+ description="This is a test issue",
1403
+ dueDate=get_current_datetime(),
1404
+ parentId=self.PARENT_ID,
1405
+ parentModule=self.PARENT_MODULE,
1406
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1407
+ ),
1408
+ Issue(
1409
+ id=3,
1410
+ title="Test Issue with Jira ID",
1411
+ status="Open",
1412
+ description="This is a test issue with Jira ID",
1413
+ dueDate=get_current_datetime(),
1414
+ parentId=self.PARENT_ID,
1415
+ parentModule=self.PARENT_MODULE,
1416
+ jiraId="JIRA-3",
1417
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1418
+ ),
1419
+ ]
1420
+
1421
+ # Create mock Jira issues
1422
+ open_status = MagicMock()
1423
+ open_status.name = "open"
1424
+
1425
+ returned_jira_issues = [
1426
+ MagicMock(
1427
+ key="JIRA-1",
1428
+ fields=MagicMock(
1429
+ summary="Test Issue",
1430
+ description="This is a test issue",
1431
+ status=open_status,
1432
+ duedate=None,
1433
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1434
+ attachment=None,
1435
+ ),
1436
+ )
1437
+ ]
1438
+
1439
+ mock_create_issue_in_jira.side_effect = returned_jira_issues
1440
+
1441
+ new_regscale_objects = sync_regscale_to_jira(
1442
+ regscale_objects=regscale_objects,
1443
+ jira_client=MagicMock(),
1444
+ jira_project=self.JIRA_PROJECT,
1445
+ jira_issue_type="Issue", # Using Issue type for issues
1446
+ sync_attachments=fetch_attachments,
1447
+ attachments={},
1448
+ api=MagicMock(),
1449
+ )
1450
+
1451
+ assert len(new_regscale_objects) == 1 # Only one new issue should be created
1452
+ assert mock_create_issue_in_jira.call_count == 1 # Should only be called once for the issue without Jira ID
1453
+ assert new_regscale_objects[0].jiraId == "JIRA-1"
1454
+
1455
+ @patch(f"{PATH}.create_issue_in_jira")
1456
+ def test_sync_regscale_tasks_to_jira(self, mock_create_issue_in_jira, fetch_attachments):
1457
+ """Test inserting Regscale tasks into jira if they do not exist"""
1458
+
1459
+ # Create RegScale tasks
1460
+ regscale_objects = [
1461
+ Task(
1462
+ id=2,
1463
+ title="Test Task",
1464
+ status="Backlog",
1465
+ description="This is a test task",
1466
+ dueDate=get_current_datetime(),
1467
+ parentId=self.PARENT_ID,
1468
+ parentModule=self.PARENT_MODULE,
1469
+ ),
1470
+ Task(
1471
+ id=4,
1472
+ title="Test Task with Other ID",
1473
+ status="Backlog",
1474
+ description="This is a test task with other ID",
1475
+ dueDate=get_current_datetime(),
1476
+ parentId=self.PARENT_ID,
1477
+ parentModule=self.PARENT_MODULE,
1478
+ otherIdentifier="JIRA-4",
1479
+ ),
1480
+ ]
1481
+
1482
+ # Create mock Jira issues
1483
+ todo_status = MagicMock()
1484
+ todo_status.name = "to do"
1485
+
1486
+ returned_jira_issues = [
1487
+ MagicMock(
1488
+ key="JIRA-2",
1489
+ fields=MagicMock(
1490
+ summary="Test Task",
1491
+ description="This is a test task",
1492
+ status=todo_status,
1493
+ duedate=None,
1494
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
1495
+ attachment=None,
1496
+ ),
1497
+ )
1498
+ ]
1499
+
1500
+ mock_create_issue_in_jira.side_effect = returned_jira_issues
1501
+
1502
+ new_regscale_objects = sync_regscale_to_jira(
1503
+ regscale_objects=regscale_objects,
1504
+ jira_client=MagicMock(),
1505
+ jira_project=self.JIRA_PROJECT,
1506
+ jira_issue_type="Task", # Using Task type for tasks
1507
+ sync_attachments=fetch_attachments,
1508
+ attachments={},
1509
+ api=MagicMock(),
1510
+ )
1511
+
1512
+ assert len(new_regscale_objects) == 1 # Only one new task should be created
1513
+ assert mock_create_issue_in_jira.call_count == 1 # Should only be called once for the task without Jira ID
1514
+ assert new_regscale_objects[0].otherIdentifier == "JIRA-2"
1515
+
1516
+ def test_generate_jira_comment(self):
1517
+ """Test generating a Jira comment from a RegScale issue"""
1518
+ # Create issue with mix of included and excluded fields
1519
+ issue = Issue(
1520
+ id=1,
1521
+ title="Test Issue",
1522
+ status="Open",
1523
+ description="Test description",
1524
+ createdById="excluded-1",
1525
+ lastUpdatedById="excluded-2",
1526
+ issueOwnerId="excluded-3",
1527
+ assignedToId="excluded-4",
1528
+ uuid="excluded-5",
1529
+ jiraId="JIRA-123",
1530
+ severityLevel="High",
1531
+ dueDate="2023-12-31",
1532
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1533
+ )
1534
+
1535
+ comment = _generate_jira_comment(issue)
1536
+
1537
+ # Verify excluded fields are not in comment
1538
+ assert "createdById" not in comment
1539
+ assert "lastUpdatedById" not in comment
1540
+ assert "issueOwnerId" not in comment
1541
+ assert "assignedToId" not in comment
1542
+ assert "uuid" not in comment
1543
+
1544
+ # Verify included fields are in comment
1545
+ assert "**jiraId:** JIRA-123" in comment
1546
+ assert "**severityLevel:** High" in comment
1547
+ assert "**dueDate:** 2023-12-31" in comment
1548
+ assert "**title:** Test Issue" in comment
1549
+ assert "**status:** Open" in comment
1550
+ assert "**description:** Test description" in comment
1551
+
1552
+ def test_generate_jira_comment_task(self):
1553
+ """Test generating a Jira comment from a RegScale task"""
1554
+ # Create task with mix of included and excluded fields
1555
+ task = Task(
1556
+ id=1,
1557
+ title="Test Task",
1558
+ status="Backlog",
1559
+ description="Test task description",
1560
+ createdById="excluded-1",
1561
+ lastUpdatedById="excluded-2",
1562
+ assignedToId="excluded-3",
1563
+ uuid="excluded-4",
1564
+ otherIdentifier="JIRA-123",
1565
+ percentComplete=50,
1566
+ dueDate="2023-12-31",
1567
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1568
+ )
1569
+
1570
+ comment = _generate_jira_comment(task)
1571
+
1572
+ # Verify excluded fields are not in comment
1573
+ assert "createdById" not in comment
1574
+ assert "lastUpdatedById" not in comment
1575
+ assert "assignedToId" not in comment
1576
+ assert "uuid" not in comment
1577
+
1578
+ # Verify included fields are in comment
1579
+ assert "**otherIdentifier:** JIRA-123" in comment
1580
+ assert "**percentComplete:** 50" in comment
1581
+ assert "**dueDate:** 2023-12-31" in comment
1582
+ assert "**title:** Test Task" in comment
1583
+ assert "**status:** Backlog" in comment
1584
+ assert "**description:** Test task description" in comment
1585
+
1586
+ def test_create_issue_in_jira(self, regscale_issues_and_attachments, jira_client, fetch_attachments):
1587
+ """Test creating an issue in Jira"""
1588
+ issues, attachments = regscale_issues_and_attachments
1589
+ for issue in issues:
1590
+ jira_issue = create_issue_in_jira(
1591
+ regscale_object=issue,
1592
+ jira_client=jira_client,
1593
+ jira_project=self.JIRA_PROJECT,
1594
+ issue_type="Bug",
1595
+ add_attachments=fetch_attachments,
1596
+ attachments=attachments,
1597
+ api=self.api,
1598
+ )
1599
+
1600
+ assert jira_issue is not None
1601
+ assert jira_issue.key is not None
1602
+ assert jira_issue.fields.summary == issue.title
1603
+ assert issue.description in jira_issue.fields.description
1604
+
1605
+ jira_issue.delete() # cleanup issue in jira
1606
+
1607
+ def test_create_task_in_jira(self, regscale_tasks_and_attachments, jira_client, fetch_attachments):
1608
+ """Test creating a task in Jira"""
1609
+ tasks, attachments = regscale_tasks_and_attachments
1610
+ for task in tasks:
1611
+ jira_task = create_issue_in_jira(
1612
+ regscale_object=task,
1613
+ jira_client=jira_client,
1614
+ jira_project=self.JIRA_PROJECT,
1615
+ issue_type="Task",
1616
+ add_attachments=fetch_attachments,
1617
+ attachments=attachments,
1618
+ api=self.api,
1619
+ )
1620
+
1621
+ assert jira_task is not None
1622
+ assert jira_task.key is not None
1623
+ assert jira_task.fields.summary == task.title
1624
+ assert task.description in jira_task.fields.description
1625
+
1626
+ jira_task.delete() # cleanup task in jira
1627
+
1628
+ def test_create_issue_in_jira_error(self):
1629
+ """Test that we exit when Jira API call fails"""
1630
+ # Create a mock Jira client that will raise an error
1631
+ mock_jira_client = MagicMock()
1632
+ mock_jira_client.create_issue.side_effect = JIRAError("Test error")
1633
+
1634
+ mock_regscale = MagicMock()
1635
+ mock_regscale.get_module_string.return_value = "issues"
1636
+ mock_regscale.id = 1
1637
+
1638
+ mock_api = MagicMock()
1639
+ mock_api.config = {"domain": "https://test.regscale.com"}
1640
+
1641
+ with pytest.raises(SystemExit) as e:
1642
+ create_issue_in_jira(
1643
+ regscale_object=mock_regscale,
1644
+ jira_client=mock_jira_client,
1645
+ jira_project=self.JIRA_PROJECT,
1646
+ issue_type="Bug",
1647
+ add_attachments=True,
1648
+ attachments={},
1649
+ api=mock_api,
1650
+ )
1651
+ assert e.value.code == 1
1652
+ assert e.type == SystemExit
1653
+
1654
+ def test_upload_files_to_jira(self, jira_client):
1655
+ """Test uploading files to Jira"""
1656
+ # create a jira issue to upload attachment to
1657
+ jira_issue = jira_client.create_issue(
1658
+ project=self.JIRA_PROJECT,
1659
+ summary="Test Issue",
1660
+ description="Test Description",
1661
+ issuetype={"name": "Bug"},
1662
+ )
1663
+
1664
+ # create regscale issue to link to
1665
+ reg_issue = Issue(
1666
+ id=1,
1667
+ title="Test Issue",
1668
+ description="Test Description",
1669
+ parentId=self.PARENT_ID,
1670
+ parentModule=self.PARENT_MODULE,
1671
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1672
+ )
1673
+
1674
+ # setup file hashes for upload
1675
+ file_path = os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt")
1676
+ with open(file_path, "rb") as file:
1677
+ reg_hashes = {compute_hash(file): file_path}
1678
+ jira_hashes = {}
1679
+ uploaded_attachments = []
1680
+
1681
+ upload_files_to_jira(
1682
+ jira_hashes,
1683
+ reg_hashes,
1684
+ jira_issue,
1685
+ reg_issue,
1686
+ jira_client,
1687
+ uploaded_attachments,
1688
+ )
1689
+ assert uploaded_attachments == [file_path]
1690
+ check_issue = jira_client.issue(jira_issue.key)
1691
+ assert len(check_issue.fields.attachment) == 1
1692
+ assert check_issue.fields.attachment[0].size > 0
1693
+ assert check_issue.fields.attachment[0].created is not None
1694
+
1695
+ # Clean up
1696
+ jira_issue.delete()
1697
+
1698
+ def test_upload_files_to_jira_duplicates(self):
1699
+ """Test that we don't upload duplicates"""
1700
+ jira_hashes = {"dummyhash": "dummy/path/file.txt"}
1701
+ reg_hashes = {"dummyhash": "dummy/path/file.txt"}
1702
+ jira_issue = MagicMock()
1703
+ reg_issue = MagicMock()
1704
+ jira_client = MagicMock()
1705
+ uploaded_attachments = []
1706
+
1707
+ upload_files_to_jira(jira_hashes, reg_hashes, jira_issue, reg_issue, jira_client, uploaded_attachments)
1708
+
1709
+ assert uploaded_attachments == []
1710
+ jira_client.add_attachment.assert_not_called()
1711
+
1712
+ @pytest.mark.parametrize("error_type", [JIRAError, TypeError])
1713
+ @patch(f"{PATH}.open")
1714
+ def test_upload_files_to_jira_error(self, mock_open, error_type):
1715
+ """Test that uploads aren't made when errors are encountered"""
1716
+ # Setup mock file
1717
+ mock_file = MagicMock()
1718
+ mock_file.read.return_value = b"test content"
1719
+ mock_open.return_value.__enter__.return_value = mock_file
1720
+
1721
+ # Setup test data
1722
+ file_path = "dummy/path/file.txt"
1723
+ reg_hashes = {"dummyhash": file_path}
1724
+ jira_hashes = {}
1725
+ jira_issue = MagicMock()
1726
+ reg_issue = MagicMock()
1727
+ jira_client = MagicMock()
1728
+ jira_client.add_attachment.side_effect = error_type("Test error")
1729
+ uploaded_attachments = []
1730
+
1731
+ upload_files_to_jira(jira_hashes, reg_hashes, jira_issue, reg_issue, jira_client, uploaded_attachments)
1732
+
1733
+ assert uploaded_attachments == []
1734
+ jira_client.add_attachment.assert_called_once()
1735
+ mock_file.read.assert_called_once()
1736
+
1737
+ def test_upload_files_to_regscale(self):
1738
+ """Test uploading files to RegScale"""
1739
+ tmp = Issue(
1740
+ id=1,
1741
+ title="Test Issue",
1742
+ description="Test Description",
1743
+ parentId=self.PARENT_ID,
1744
+ parentModule=self.PARENT_MODULE,
1745
+ dueDate=get_current_datetime(),
1746
+ status="Open",
1747
+ identification=f"{self.title_prefix} Jira Issue Integration Test",
1748
+ )
1749
+ reg_issue = tmp.create()
1750
+
1751
+ file_path = os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt")
1752
+ with open(file_path, "rb") as file:
1753
+ jira_hashes = {compute_hash(file): file_path}
1754
+ reg_hashes = {}
1755
+ uploaded_attachments = []
1756
+
1757
+ upload_files_to_regscale(jira_hashes, reg_hashes, reg_issue, self.api, uploaded_attachments)
1758
+
1759
+ assert uploaded_attachments == [file_path]
1760
+ check_issues, attachments = Issue.get_objects_and_attachments_by_parent(
1761
+ parent_id=self.PARENT_ID, parent_module=self.PARENT_MODULE
1762
+ )
1763
+ assert reg_issue in check_issues
1764
+ assert len(attachments[reg_issue.id]) == 1
1765
+
1766
+ @patch(f"{PATH}.File.upload_file_to_regscale", return_value=None)
1767
+ def test_upload_files_to_regscale_duplicates(self, mock_upload_file_to_regscale):
1768
+ """Test that we don't upload duplicate attachments to regscale"""
1769
+ jira_hashes = {"dummyhash": "dummy/path/file.txt"}
1770
+ reg_hashes = {"dummyhash": "dummy/path/file.txt"}
1771
+ reg_issue = MagicMock()
1772
+ uploaded_attachments = []
1773
+
1774
+ upload_files_to_regscale(jira_hashes, reg_hashes, reg_issue, MagicMock(), uploaded_attachments)
1775
+
1776
+ assert uploaded_attachments == []
1777
+ mock_upload_file_to_regscale.assert_not_called()
1778
+
1779
+ @patch(f"{PATH}.File.upload_file_to_regscale", return_value=None)
1780
+ @patch(f"{PATH}.open")
1781
+ def test_upload_files_to_regscale_error(self, mock_open, mock_upload_file_to_regscale):
1782
+ """Test when the uploads are unsuccessful"""
1783
+ mock_file = MagicMock()
1784
+ mock_file.read.return_value = b"test content"
1785
+ mock_open.return_value.__enter__.return_value = mock_file
1786
+
1787
+ # Setup test data
1788
+ file_path = "dummy/path/file.txt"
1789
+ jira_hashes = {"dummyhash": file_path}
1790
+ reg_hashes = {}
1791
+ reg_issue = MagicMock()
1792
+ uploaded_attachments = []
1793
+ api = MagicMock()
1794
+
1795
+ upload_files_to_regscale(jira_hashes, reg_hashes, reg_issue, api, uploaded_attachments)
1796
+
1797
+ assert uploaded_attachments == []
1798
+ mock_upload_file_to_regscale.assert_called_once()
1799
+
1800
+ def test_validate_issue_type(self):
1801
+ """Test validating the issue type"""
1802
+ jira_client = MagicMock()
1803
+ issue_type_bug = MagicMock()
1804
+ issue_type_bug.name = "Bug"
1805
+ issue_type_task = MagicMock()
1806
+ issue_type_task.name = "Task"
1807
+ jira_client.issue_types.return_value = [issue_type_bug, issue_type_task]
1808
+ assert validate_issue_type(jira_client, "Bug") is True
1809
+ assert validate_issue_type(jira_client, "Task") is True
1810
+ with pytest.raises(SystemExit) as e:
1811
+ validate_issue_type(jira_client, "Invalid")
1812
+ assert e.value.code == 1
1813
+ assert e.type == SystemExit
1814
+
1815
+ def test_download_issue_attachments(self, regscale_issues_and_attachments, get_jira_issue_with_attachment):
1816
+ """Test downloading attachments from Jira and RegScale issues"""
1817
+ issues, attachments = regscale_issues_and_attachments
1818
+ jira_issue = get_jira_issue_with_attachment
1819
+ if attachments:
1820
+ with tempfile.TemporaryDirectory() as tmpdir:
1821
+ download_regscale_attachments_to_directory(
1822
+ directory=tmpdir,
1823
+ jira_issue=jira_issue,
1824
+ regscale_object=issues[0],
1825
+ api=self.api,
1826
+ )
1827
+ assert os.path.exists(tmpdir) is True
1828
+ with tempfile.TemporaryDirectory() as tmpdir:
1829
+ download_regscale_attachments_to_directory(
1830
+ directory=tmpdir,
1831
+ jira_issue=jira_issue,
1832
+ regscale_object=issues[0],
1833
+ api=self.api,
1834
+ )
1835
+ assert os.path.exists(tmpdir) is True
1836
+
1837
+ def test_download_task_attachments(self, regscale_tasks_and_attachments, get_jira_task_with_attachment):
1838
+ """Test downloading attachments from Jira and RegScale tasks"""
1839
+ tasks, attachments = regscale_tasks_and_attachments
1840
+ jira_task = get_jira_task_with_attachment
1841
+ if attachments:
1842
+ with tempfile.TemporaryDirectory() as tmpdir:
1843
+ download_regscale_attachments_to_directory(
1844
+ directory=tmpdir,
1845
+ jira_issue=jira_task,
1846
+ regscale_object=tasks[0],
1847
+ api=self.api,
1848
+ )
1849
+ assert os.path.exists(tmpdir) is True
1850
+ with tempfile.TemporaryDirectory() as tmpdir:
1851
+ download_regscale_attachments_to_directory(
1852
+ directory=tmpdir,
1853
+ jira_issue=jira_task,
1854
+ regscale_object=tasks[0],
1855
+ api=self.api,
1856
+ )
1857
+ assert os.path.exists(tmpdir) is True
1858
+
1859
+ @patch(f"{PATH}.sync_regscale_objects_to_jira")
1860
+ @patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
1861
+ @patch(f"{PATH}.create_jira_client")
1862
+ @patch(f"{PATH}.fetch_jira_objects")
1863
+ @patch(f"{PATH}.get_regscale_data_and_attachments")
1864
+ @patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
1865
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
1866
+ def test_sync_regscale_and_jira_with_poams_true(
1867
+ self,
1868
+ mock_check_license,
1869
+ mock_api,
1870
+ mock_get_regscale_data_and_attachments,
1871
+ mock_fetch_jira_objects,
1872
+ mock_create_jira_client,
1873
+ mock_sync_regscale_to_jira,
1874
+ mock_sync_regscale_objects_to_jira,
1875
+ fetch_attachments,
1876
+ ):
1877
+ """Test sync_regscale_and_jira with use_poams=True"""
1878
+ # Setup mocks
1879
+ mock_jira_client = MagicMock()
1880
+ mock_create_jira_client.return_value = mock_jira_client
1881
+ mock_fetch_jira_objects.return_value = [MagicMock()]
1882
+ mock_get_regscale_data_and_attachments.return_value = ([MagicMock()], MagicMock())
1883
+
1884
+ # Call function with use_poams=True
1885
+ sync_regscale_and_jira(
1886
+ parent_id=self.PARENT_ID,
1887
+ parent_module=self.PARENT_MODULE,
1888
+ jira_project=self.JIRA_PROJECT,
1889
+ jira_issue_type="Bug",
1890
+ sync_attachments=fetch_attachments,
1891
+ use_poams=True,
1892
+ )
1893
+
1894
+ # Verify sync_regscale_objects_to_jira was called with use_poams=True
1895
+ mock_sync_regscale_objects_to_jira.assert_called_once()
1896
+ call_args = mock_sync_regscale_objects_to_jira.call_args
1897
+ assert call_args[0][7] is True # use_poams is the 8th positional argument
1898
+
1899
+ @patch(f"{PATH}.sync_regscale_objects_to_jira")
1900
+ @patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
1901
+ @patch(f"{PATH}.create_jira_client")
1902
+ @patch(f"{PATH}.fetch_jira_objects")
1903
+ @patch(f"{PATH}.get_regscale_data_and_attachments")
1904
+ @patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
1905
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
1906
+ def test_sync_regscale_and_jira_with_poams_false(
1907
+ self,
1908
+ mock_check_license,
1909
+ mock_api,
1910
+ mock_get_regscale_data_and_attachments,
1911
+ mock_fetch_jira_objects,
1912
+ mock_create_jira_client,
1913
+ mock_sync_regscale_to_jira,
1914
+ mock_sync_regscale_objects_to_jira,
1915
+ fetch_attachments,
1916
+ ):
1917
+ """Test sync_regscale_and_jira with use_poams=False (default)"""
1918
+ # Setup mocks
1919
+ mock_jira_client = MagicMock()
1920
+ mock_create_jira_client.return_value = mock_jira_client
1921
+ mock_fetch_jira_objects.return_value = [MagicMock()]
1922
+ mock_get_regscale_data_and_attachments.return_value = ([MagicMock()], MagicMock())
1923
+
1924
+ # Call function with use_poams=False (default)
1925
+ sync_regscale_and_jira(
1926
+ parent_id=self.PARENT_ID,
1927
+ parent_module=self.PARENT_MODULE,
1928
+ jira_project=self.JIRA_PROJECT,
1929
+ jira_issue_type="Bug",
1930
+ sync_attachments=fetch_attachments,
1931
+ use_poams=False,
1932
+ )
1933
+
1934
+ # Verify sync_regscale_objects_to_jira was called with use_poams=False
1935
+ mock_sync_regscale_objects_to_jira.assert_called_once()
1936
+ call_args = mock_sync_regscale_objects_to_jira.call_args
1937
+ assert call_args[0][7] is False # use_poams is the 8th positional argument
1938
+
1939
+ @patch(f"{PATH}.create_jira_client")
1940
+ @patch(f"{PATH}.create_and_update_regscale_issues")
1941
+ @patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
1942
+ def test_sync_regscale_objects_to_jira_with_poams(
1943
+ self,
1944
+ mock_check_license,
1945
+ mock_create_and_update_regscale_issues,
1946
+ mock_create_jira_client,
1947
+ fetch_attachments,
1948
+ ):
1949
+ """Test sync_regscale_objects_to_jira passes use_poams to create_and_update_regscale_issues"""
1950
+ mock_check_license.return_value.config = self.config
1951
+ mock_jira_client = MagicMock()
1952
+ mock_create_jira_client.return_value = mock_jira_client
1953
+
1954
+ # Create mock application with ThreadManager
1955
+ mock_app = MagicMock(spec=Application)
1956
+ mock_app.config = self.config
1957
+ mock_thread_manager = MagicMock()
1958
+ mock_app.thread_manager = mock_thread_manager
1959
+
1960
+ # Mock Jira issues
1961
+ mock_jira_issues = [MagicMock(), MagicMock()]
1962
+ mock_regscale_objects = [MagicMock()]
1963
+
1964
+ # Test with use_poams=True
1965
+ sync_regscale_objects_to_jira(
1966
+ mock_jira_issues,
1967
+ mock_regscale_objects,
1968
+ fetch_attachments,
1969
+ mock_app,
1970
+ self.PARENT_ID,
1971
+ self.PARENT_MODULE,
1972
+ False, # sync_tasks_only
1973
+ True, # use_poams
1974
+ )
1975
+
1976
+ # Verify ThreadManager was called with correct parameters
1977
+ mock_thread_manager.submit_tasks_from_list.assert_called_once()
1978
+ call_args = mock_thread_manager.submit_tasks_from_list.call_args[0]
1979
+ # use_poams is the 4th argument (index 3) after function, jira_issues, and regscale_objects
1980
+ assert call_args[3] is True
1981
+
1982
+ @patch(f"{PATH}.map_jira_to_regscale_issue")
1983
+ def test_map_jira_to_regscale_issue_with_poam_true(self, mock_map_jira_to_regscale_issue):
1984
+ """Test map_jira_to_regscale_issue sets isPoam=True when is_poam=True"""
1985
+ # Create mock Jira issue
1986
+ mock_issue = MagicMock()
1987
+ mock_issue.fields.summary = "Test Issue"
1988
+ mock_issue.fields.description = "Test Description"
1989
+ mock_issue.fields.status.name = "Open"
1990
+ mock_issue.fields.priority.name = "High"
1991
+ mock_issue.fields.duedate = None
1992
+ mock_issue.key = "TEST-123"
1993
+
1994
+ # Create mock config
1995
+ mock_config = {
1996
+ "userId": "1",
1997
+ "issues": {"jira": {"status": "Open", "high": 7, "medium": 14, "low": 30}},
1998
+ }
1999
+
2000
+ # Call the actual function with is_poam=True
2001
+ result = map_jira_to_regscale_issue(
2002
+ jira_issue=mock_issue,
2003
+ config=mock_config,
2004
+ parent_id=self.PARENT_ID,
2005
+ parent_module=self.PARENT_MODULE,
2006
+ is_poam=True,
2007
+ )
2008
+
2009
+ # Verify the Issue object was created with isPoam=True
2010
+ assert isinstance(result, Issue)
2011
+ assert result.isPoam is True
2012
+ assert result.title == "Test Issue"
2013
+ assert result.jiraId == "TEST-123"
2014
+
2015
+ @patch(f"{PATH}.map_jira_to_regscale_issue")
2016
+ def test_map_jira_to_regscale_issue_with_poam_false(self, mock_map_jira_to_regscale_issue):
2017
+ """Test map_jira_to_regscale_issue sets isPoam=False when is_poam=False"""
2018
+ # Create mock Jira issue
2019
+ mock_issue = MagicMock()
2020
+ mock_issue.fields.summary = "Test Issue"
2021
+ mock_issue.fields.description = "Test Description"
2022
+ mock_issue.fields.status.name = "Open"
2023
+ mock_issue.fields.priority.name = "High"
2024
+ mock_issue.fields.duedate = None
2025
+ mock_issue.key = "TEST-123"
2026
+
2027
+ # Create mock config
2028
+ mock_config = {
2029
+ "userId": "1",
2030
+ "issues": {"jira": {"status": "Open", "high": 7, "medium": 14, "low": 30}},
2031
+ }
2032
+
2033
+ # Call the actual function with is_poam=False
2034
+ result = map_jira_to_regscale_issue(
2035
+ jira_issue=mock_issue,
2036
+ config=mock_config,
2037
+ parent_id=self.PARENT_ID,
2038
+ parent_module=self.PARENT_MODULE,
2039
+ is_poam=False,
2040
+ )
2041
+
2042
+ # Verify the Issue object was created with isPoam=False
2043
+ assert isinstance(result, Issue)
2044
+ assert result.isPoam is False
2045
+ assert result.title == "Test Issue"
2046
+ assert result.jiraId == "TEST-123"
2047
+
2048
+ @patch(f"{PATH}.compare_files_for_dupes_and_upload")
2049
+ @patch(f"{PATH}.map_jira_to_regscale_issue")
2050
+ @patch(f"{PATH}.Issue.update_issue")
2051
+ @patch(f"{PATH}.job_progress", return_value=MagicMock(spec=Progress))
2052
+ def test_create_and_update_regscale_issues_sets_ispoam_on_new_issue(
2053
+ self,
2054
+ mock_job_progress_object,
2055
+ mock_update_issue,
2056
+ mock_map_jira_to_regscale_issue,
2057
+ mock_compare_files_for_dupes_and_upload,
2058
+ ):
2059
+ """Test that create_and_update_regscale_issues sets isPoam on newly created issues"""
2060
+ # Create mock Jira issue
2061
+ open_status = MagicMock()
2062
+ open_status.name = "open"
2063
+ high_priority = MagicMock()
2064
+ high_priority.name = "high"
2065
+
2066
+ jira_issue = MagicMock(
2067
+ key="JIRA-NEW",
2068
+ fields=MagicMock(
2069
+ summary="New Issue",
2070
+ description="New issue description",
2071
+ status=open_status,
2072
+ duedate=None,
2073
+ priority=high_priority,
2074
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
2075
+ attachment=None,
2076
+ ),
2077
+ )
2078
+
2079
+ # Create empty RegScale issues list (no existing issues)
2080
+ regscale_issues = []
2081
+
2082
+ # Create mock config
2083
+ config = {
2084
+ "issues": {"jira": {"highest": 7, "high": 30, "medium": 90, "low": 180, "lowest": 365, "status": "Open"}},
2085
+ "maxThreads": 4,
2086
+ "userId": "123e4567-e89b-12d3-a456-426614174000",
2087
+ }
2088
+ app = MagicMock()
2089
+ app.config = config
2090
+
2091
+ # Mock the creation of a new issue
2092
+ created_issue_mock = MagicMock()
2093
+ created_issue_mock.id = 999
2094
+ created_issue_mock.create.return_value = created_issue_mock
2095
+ mock_map_jira_to_regscale_issue.return_value = created_issue_mock
2096
+
2097
+ with mock_job_progress_object as job_progress:
2098
+ test_task = job_progress.add_task(
2099
+ description="Processing issues",
2100
+ total=1,
2101
+ visible=False,
2102
+ )
2103
+
2104
+ # Call with use_poams=True
2105
+ create_and_update_regscale_issues(
2106
+ jira_issue,
2107
+ regscale_issues,
2108
+ True, # use_poams
2109
+ False, # add_attachments
2110
+ MagicMock(),
2111
+ app,
2112
+ self.PARENT_ID,
2113
+ self.PARENT_MODULE,
2114
+ test_task,
2115
+ job_progress,
2116
+ )
2117
+
2118
+ # Verify map_jira_to_regscale_issue was called with is_poam=True
2119
+ mock_map_jira_to_regscale_issue.assert_called_once()
2120
+ call_kwargs = mock_map_jira_to_regscale_issue.call_args[1]
2121
+ assert call_kwargs["is_poam"] is True
2122
+
2123
+ @patch(f"{PATH}.compare_files_for_dupes_and_upload")
2124
+ @patch(f"{PATH}.map_jira_to_regscale_issue")
2125
+ @patch(f"{PATH}.Issue.save")
2126
+ @patch(f"{PATH}.job_progress", return_value=MagicMock(spec=Progress))
2127
+ def test_create_and_update_regscale_issues_sets_ispoam_on_existing_issue(
2128
+ self,
2129
+ mock_job_progress_object,
2130
+ mock_save,
2131
+ mock_map_jira_to_regscale_issue,
2132
+ mock_compare_files_for_dupes_and_upload,
2133
+ ):
2134
+ """Test that create_and_update_regscale_issues sets isPoam on existing issues"""
2135
+ # Create mock Jira issue
2136
+ open_status = MagicMock()
2137
+ open_status.name = "open"
2138
+ high_priority = MagicMock()
2139
+ high_priority.name = "high"
2140
+
2141
+ jira_issue = MagicMock(
2142
+ key="JIRA-1",
2143
+ fields=MagicMock(
2144
+ summary="Existing Issue",
2145
+ description="Existing issue description",
2146
+ status=open_status,
2147
+ duedate=None,
2148
+ priority=high_priority,
2149
+ statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
2150
+ attachment=None,
2151
+ ),
2152
+ )
2153
+
2154
+ # Create existing RegScale issue (using MagicMock to avoid actual creation)
2155
+ existing_issue = MagicMock(spec=Issue)
2156
+ existing_issue.jiraId = "JIRA-1"
2157
+ existing_issue.isPoam = False # Initially not a POAM
2158
+ existing_issue.id = 1
2159
+ existing_issue.title = "Existing Issue"
2160
+ regscale_issues = [existing_issue]
2161
+
2162
+ # Create mock config
2163
+ config = {
2164
+ "issues": {"jira": {"highest": 7, "high": 30, "medium": 90, "low": 180, "lowest": 365, "status": "Open"}},
2165
+ "maxThreads": 4,
2166
+ "userId": "123e4567-e89b-12d3-a456-426614174000",
2167
+ "jiraCustomFields": {},
2168
+ }
2169
+ app = MagicMock()
2170
+ app.config = config
2171
+
2172
+ # Setup mock return values
2173
+ mock_save.return_value = MagicMock()
2174
+
2175
+ with mock_job_progress_object as job_progress:
2176
+ test_task = job_progress.add_task(
2177
+ description="Processing issues",
2178
+ total=1,
2179
+ visible=False,
2180
+ )
2181
+
2182
+ # Call with use_poams=True
2183
+ create_and_update_regscale_issues(
2184
+ jira_issue,
2185
+ regscale_issues,
2186
+ True, # use_poams
2187
+ False, # add_attachments
2188
+ MagicMock(),
2189
+ app,
2190
+ self.PARENT_ID,
2191
+ self.PARENT_MODULE,
2192
+ test_task,
2193
+ job_progress,
2194
+ )
2195
+
2196
+ # Verify the existing issue had isPoam set to True
2197
+ assert existing_issue.isPoam is True
2198
+
2199
+ @staticmethod
2200
+ def teardown_class(cls):
2201
+ """Remove test data"""
2202
+ with contextlib.suppress(FileNotFoundError):
2203
+ shutil.rmtree("./artifacts")
2204
+ assert not os.path.exists("./artifacts")