regscale-cli 6.25.1.0__py3-none-any.whl → 6.26.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.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/application.py +18 -3
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/integrations/commercial/sicura/api.py +14 -13
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +49 -39
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/wizv2/click.py +26 -26
- regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
- regscale/integrations/commercial/wizv2/scanner.py +3 -3
- regscale/integrations/compliance_integration.py +67 -2
- regscale/integrations/control_matcher.py +358 -0
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +1 -0
- regscale/integrations/public/cci_importer.py +37 -38
- regscale/integrations/public/fedramp/click.py +60 -2
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +150 -96
- regscale/models/integration_models/cisa_kev_data.json +154 -4
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +46 -21
- regscale/models/regscale_models/issue.py +256 -94
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/regscale_model.py +6 -1
- regscale/templates/__init__.py +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +80 -33
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3731 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +1814 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +350 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1469 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/test_control_matcher.py +1314 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_issue.py +378 -1
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1814 @@
|
|
|
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
|
+
update_regscale_issues,
|
|
35
|
+
create_regscale_task_from_jira,
|
|
36
|
+
create_and_update_regscale_tasks,
|
|
37
|
+
process_tasks_for_sync,
|
|
38
|
+
upload_files_to_jira,
|
|
39
|
+
upload_files_to_regscale,
|
|
40
|
+
validate_issue_type,
|
|
41
|
+
)
|
|
42
|
+
from regscale.models import File, Issue, SecurityPlan
|
|
43
|
+
from regscale.models.regscale_models.task import Task
|
|
44
|
+
from tests import CLITestFixture
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestJira(CLITestFixture):
|
|
48
|
+
JIRA_PROJECT = "SNES"
|
|
49
|
+
PATH = "regscale.integrations.commercial.jira"
|
|
50
|
+
security_plan = None
|
|
51
|
+
|
|
52
|
+
@pytest.fixture(autouse=True)
|
|
53
|
+
def setup_ssp(self, create_security_plan):
|
|
54
|
+
self.security_plan = create_security_plan
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def PARENT_ID(self):
|
|
58
|
+
"""Get the parent ID from the existing SSP"""
|
|
59
|
+
return self.security_plan.id
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def PARENT_MODULE(self):
|
|
63
|
+
"""Get the parent module from the existing SSP"""
|
|
64
|
+
return self.security_plan.get_module_string()
|
|
65
|
+
|
|
66
|
+
@pytest.fixture
|
|
67
|
+
def jira_client(self):
|
|
68
|
+
"""Setup jira client"""
|
|
69
|
+
return create_jira_client(
|
|
70
|
+
config=self.config,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
@pytest.fixture(params=[True, False])
|
|
75
|
+
def fetch_attachments(request):
|
|
76
|
+
"""Pytest fixture that will run twice:
|
|
77
|
+
first time with a true value, second time with a false value"""
|
|
78
|
+
return request.param
|
|
79
|
+
|
|
80
|
+
@pytest.fixture
|
|
81
|
+
def jira_issues(self, jira_client, fetch_attachments):
|
|
82
|
+
"""Fixture for fetching Jira issues and attachments"""
|
|
83
|
+
return fetch_jira_objects(jira_client=jira_client, jira_project=self.JIRA_PROJECT, jira_issue_type="Bug")
|
|
84
|
+
|
|
85
|
+
@pytest.fixture
|
|
86
|
+
def jira_tasks(self, jira_client, fetch_attachments):
|
|
87
|
+
"""Fixture for fetching Jira tasks and attachments"""
|
|
88
|
+
return fetch_jira_objects(
|
|
89
|
+
jira_client=jira_client,
|
|
90
|
+
jira_project=self.JIRA_PROJECT,
|
|
91
|
+
jira_issue_type="Task",
|
|
92
|
+
sync_tasks_only=True,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@pytest.fixture
|
|
96
|
+
def get_jira_issue(self, jira_client):
|
|
97
|
+
"""Fixture for creating a test Issue in Jira"""
|
|
98
|
+
jira_issue = jira_client.create_issue(
|
|
99
|
+
fields={
|
|
100
|
+
"project": {"key": self.JIRA_PROJECT},
|
|
101
|
+
"summary": f"{self.title_prefix} Jira Integration Test",
|
|
102
|
+
"description": "Test issue for integration testing",
|
|
103
|
+
"issuetype": {"name": "Bug"},
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
yield jira_issue
|
|
107
|
+
jira_issue.delete() # cleanup afterwards
|
|
108
|
+
|
|
109
|
+
@pytest.fixture
|
|
110
|
+
def get_jira_task(self, jira_client):
|
|
111
|
+
"""Fixture for creating a test Task in Jira"""
|
|
112
|
+
jira_task = jira_client.create_issue(
|
|
113
|
+
fields={
|
|
114
|
+
"project": {"key": self.JIRA_PROJECT},
|
|
115
|
+
"summary": f"{self.title_prefix} Jira Integration Test",
|
|
116
|
+
"description": "Test task for integration testing",
|
|
117
|
+
"issuetype": {"name": "Task"},
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
yield jira_task
|
|
121
|
+
jira_task.delete() # cleanup afterwards
|
|
122
|
+
|
|
123
|
+
@pytest.fixture
|
|
124
|
+
def get_jira_issue_with_attachment(self, get_jira_issue, jira_client):
|
|
125
|
+
"""Fixture for creating a test Issue in Jira with an attachment"""
|
|
126
|
+
issue = get_jira_issue
|
|
127
|
+
file_path = os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt")
|
|
128
|
+
with open(file_path, "rb") as f:
|
|
129
|
+
jira_client.add_attachment(issue=issue, filename="test_attachment.txt", attachment=f)
|
|
130
|
+
yield jira_client.issue(issue.id)
|
|
131
|
+
|
|
132
|
+
@pytest.fixture
|
|
133
|
+
def get_jira_task_with_attachment(self, get_jira_task, jira_client):
|
|
134
|
+
"""Fixture for creating a test Task in Jira with an attachment"""
|
|
135
|
+
task = get_jira_task
|
|
136
|
+
file_path = os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt")
|
|
137
|
+
with open(file_path, "rb") as f:
|
|
138
|
+
jira_client.add_attachment(issue=task, filename="test_attachment.txt", attachment=f)
|
|
139
|
+
yield jira_client.issue(task.id)
|
|
140
|
+
|
|
141
|
+
@pytest.fixture
|
|
142
|
+
def regscale_issues_and_attachments(self, fetch_attachments, regscale_issue_and_attachment):
|
|
143
|
+
"""Fixture for fetching RegScale issues and attachments"""
|
|
144
|
+
_ = regscale_issue_and_attachment
|
|
145
|
+
|
|
146
|
+
if fetch_attachments:
|
|
147
|
+
return Issue.get_objects_and_attachments_by_parent(
|
|
148
|
+
parent_id=self.PARENT_ID,
|
|
149
|
+
parent_module=self.PARENT_MODULE,
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
return (
|
|
153
|
+
Issue.get_all_by_parent(
|
|
154
|
+
parent_id=self.PARENT_ID,
|
|
155
|
+
parent_module=self.PARENT_MODULE,
|
|
156
|
+
),
|
|
157
|
+
[],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@pytest.fixture
|
|
161
|
+
def regscale_tasks_and_attachments(self, fetch_attachments, regscale_task_and_attachment):
|
|
162
|
+
"""Fixture for fetching RegScale tasks and attachments"""
|
|
163
|
+
_ = regscale_task_and_attachment
|
|
164
|
+
if fetch_attachments:
|
|
165
|
+
return Task.get_objects_and_attachments_by_parent(
|
|
166
|
+
parent_id=self.PARENT_ID,
|
|
167
|
+
parent_module=self.PARENT_MODULE,
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
return (
|
|
171
|
+
Task.get_all_by_parent(
|
|
172
|
+
parent_id=self.PARENT_ID,
|
|
173
|
+
parent_module=self.PARENT_MODULE,
|
|
174
|
+
),
|
|
175
|
+
[],
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@pytest.fixture
|
|
179
|
+
def regscale_issue_and_attachment(self, fetch_attachments):
|
|
180
|
+
"""Fixture for creating RegScale issue and attachment"""
|
|
181
|
+
issue = Issue(
|
|
182
|
+
title=f"{self.title_prefix} Jira Issue Integration Test",
|
|
183
|
+
description="Security plan for Jira integration testing",
|
|
184
|
+
parentId=self.PARENT_ID,
|
|
185
|
+
parentModule=self.PARENT_MODULE,
|
|
186
|
+
dueDate=get_current_datetime(),
|
|
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
|
+
try:
|
|
291
|
+
sync_regscale_and_jira(
|
|
292
|
+
parent_id=self.PARENT_ID,
|
|
293
|
+
parent_module=self.PARENT_MODULE,
|
|
294
|
+
jira_project=self.JIRA_PROJECT,
|
|
295
|
+
jira_issue_type="Bug",
|
|
296
|
+
sync_attachments=fetch_attachments,
|
|
297
|
+
)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
pytest.fail("Jira & RegScale sync failed: {}".format(e))
|
|
300
|
+
|
|
301
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
302
|
+
def test_sync_regscale_and_jira_tasks(self, mock_check_license, fetch_attachments):
|
|
303
|
+
"""Test the entire Jira & RegScale task sync process"""
|
|
304
|
+
mock_check_license.return_value.config = self.config
|
|
305
|
+
try:
|
|
306
|
+
sync_regscale_and_jira(
|
|
307
|
+
parent_id=self.PARENT_ID,
|
|
308
|
+
parent_module=self.PARENT_MODULE,
|
|
309
|
+
jira_project=self.JIRA_PROJECT,
|
|
310
|
+
jira_issue_type="Task",
|
|
311
|
+
sync_attachments=fetch_attachments,
|
|
312
|
+
sync_tasks_only=True,
|
|
313
|
+
)
|
|
314
|
+
except Exception as e:
|
|
315
|
+
pytest.fail("Jira & RegScale task sync failed: {}".format(e))
|
|
316
|
+
|
|
317
|
+
@patch(f"{PATH}.sync_regscale_objects_to_jira")
|
|
318
|
+
@patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
|
|
319
|
+
@patch(f"{PATH}.create_jira_client")
|
|
320
|
+
@patch(f"{PATH}.fetch_jira_objects", return_value=[])
|
|
321
|
+
@patch(f"{PATH}.get_regscale_data_and_attachments", return_value=([], {}))
|
|
322
|
+
@patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
|
|
323
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
324
|
+
def test_sync_regscale_and_jira_no_updates(
|
|
325
|
+
self,
|
|
326
|
+
mock_check_license,
|
|
327
|
+
mock_api,
|
|
328
|
+
mock_get_regscale_data_and_attachments,
|
|
329
|
+
mock_fetch_jira_objects,
|
|
330
|
+
mock_create_jira_client,
|
|
331
|
+
mock_sync_regscale_to_jira,
|
|
332
|
+
mock_sync_regscale_objects_to_jira,
|
|
333
|
+
fetch_attachments,
|
|
334
|
+
):
|
|
335
|
+
"""Test sync_regscale_and_jira without updates from either side"""
|
|
336
|
+
# mock jira client so we can check it was correctly used later
|
|
337
|
+
mock_jira_client = MagicMock()
|
|
338
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
339
|
+
|
|
340
|
+
sync_regscale_and_jira(
|
|
341
|
+
parent_id=self.PARENT_ID,
|
|
342
|
+
parent_module=self.PARENT_MODULE,
|
|
343
|
+
jira_project=self.JIRA_PROJECT,
|
|
344
|
+
jira_issue_type="Bug",
|
|
345
|
+
sync_attachments=fetch_attachments,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# check that we get the jira client correctly
|
|
349
|
+
mock_create_jira_client.assert_called_once()
|
|
350
|
+
|
|
351
|
+
# check that we correctly fetch objects from jira and regscale
|
|
352
|
+
mock_get_regscale_data_and_attachments.assert_called_once_with(
|
|
353
|
+
parent_id=self.PARENT_ID,
|
|
354
|
+
parent_module=self.PARENT_MODULE,
|
|
355
|
+
sync_attachments=fetch_attachments,
|
|
356
|
+
sync_tasks_only=False,
|
|
357
|
+
)
|
|
358
|
+
mock_fetch_jira_objects.assert_called_once_with(
|
|
359
|
+
jira_client=mock_jira_client,
|
|
360
|
+
jira_project=self.JIRA_PROJECT,
|
|
361
|
+
jql_str="project = SNES",
|
|
362
|
+
jira_issue_type="Bug",
|
|
363
|
+
sync_tasks_only=False,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# check that no updates were made because we did not find any objects from jira/regscale
|
|
367
|
+
mock_sync_regscale_to_jira.assert_not_called()
|
|
368
|
+
mock_sync_regscale_objects_to_jira.assert_not_called()
|
|
369
|
+
|
|
370
|
+
@patch(f"{PATH}.sync_regscale_objects_to_jira")
|
|
371
|
+
@patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
|
|
372
|
+
@patch(f"{PATH}.create_jira_client")
|
|
373
|
+
@patch(f"{PATH}.fetch_jira_objects")
|
|
374
|
+
@patch(f"{PATH}.get_regscale_data_and_attachments")
|
|
375
|
+
@patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
|
|
376
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
377
|
+
def test_sync_regscale_and_jira_updates(
|
|
378
|
+
self,
|
|
379
|
+
mock_check_license,
|
|
380
|
+
mock_api,
|
|
381
|
+
mock_get_regscale_data_and_attachments,
|
|
382
|
+
mock_fetch_jira_objects,
|
|
383
|
+
mock_create_jira_client,
|
|
384
|
+
mock_sync_regscale_to_jira,
|
|
385
|
+
mock_sync_regscale_objects_to_jira,
|
|
386
|
+
fetch_attachments,
|
|
387
|
+
):
|
|
388
|
+
"""Test sync_regscale_and_jira with updates from both sides"""
|
|
389
|
+
# mock jira client so we can check it was correctly used later
|
|
390
|
+
mock_jira_client = MagicMock()
|
|
391
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
392
|
+
|
|
393
|
+
# mock these so that we can control what objects were returned to check later
|
|
394
|
+
mock_fetch_jira_objects.return_value = MagicMock()
|
|
395
|
+
mock_get_regscale_data_and_attachments.return_value = (MagicMock(), MagicMock())
|
|
396
|
+
|
|
397
|
+
sync_regscale_and_jira(
|
|
398
|
+
parent_id=self.PARENT_ID,
|
|
399
|
+
parent_module=self.PARENT_MODULE,
|
|
400
|
+
jira_project=self.JIRA_PROJECT,
|
|
401
|
+
jira_issue_type="Bug",
|
|
402
|
+
sync_attachments=fetch_attachments,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# check that we get the jira client correctly
|
|
406
|
+
mock_create_jira_client.assert_called_once()
|
|
407
|
+
|
|
408
|
+
# check that we correctly fetch objects from jira and regscale
|
|
409
|
+
mock_get_regscale_data_and_attachments.assert_called_once_with(
|
|
410
|
+
parent_id=self.PARENT_ID,
|
|
411
|
+
parent_module=self.PARENT_MODULE,
|
|
412
|
+
sync_attachments=fetch_attachments,
|
|
413
|
+
sync_tasks_only=False,
|
|
414
|
+
)
|
|
415
|
+
mock_fetch_jira_objects.assert_called_once_with(
|
|
416
|
+
jira_client=mock_jira_client,
|
|
417
|
+
jira_project=self.JIRA_PROJECT,
|
|
418
|
+
jql_str="project = SNES",
|
|
419
|
+
jira_issue_type="Bug",
|
|
420
|
+
sync_tasks_only=False,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# check that updates were made with correct objects
|
|
424
|
+
mock_sync_regscale_to_jira.assert_called_once_with(
|
|
425
|
+
regscale_objects=mock_get_regscale_data_and_attachments.return_value[0],
|
|
426
|
+
jira_client=mock_jira_client,
|
|
427
|
+
jira_project=self.JIRA_PROJECT,
|
|
428
|
+
jira_issue_type="Bug",
|
|
429
|
+
api=mock_api.return_value,
|
|
430
|
+
sync_attachments=fetch_attachments,
|
|
431
|
+
attachments=mock_get_regscale_data_and_attachments.return_value[1],
|
|
432
|
+
)
|
|
433
|
+
mock_sync_regscale_objects_to_jira.assert_called_once_with(
|
|
434
|
+
mock_fetch_jira_objects.return_value,
|
|
435
|
+
mock_get_regscale_data_and_attachments.return_value[0],
|
|
436
|
+
fetch_attachments,
|
|
437
|
+
mock_check_license.return_value,
|
|
438
|
+
self.PARENT_ID,
|
|
439
|
+
self.PARENT_MODULE,
|
|
440
|
+
False,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
@patch(f"{PATH}.sync_regscale_objects_to_jira")
|
|
444
|
+
@patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
|
|
445
|
+
@patch(f"{PATH}.create_jira_client")
|
|
446
|
+
@patch(f"{PATH}.fetch_jira_objects")
|
|
447
|
+
@patch(f"{PATH}.get_regscale_data_and_attachments")
|
|
448
|
+
@patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
|
|
449
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
450
|
+
def test_sync_regscale_and_jira_custom_jql(
|
|
451
|
+
self,
|
|
452
|
+
mock_check_license,
|
|
453
|
+
mock_api,
|
|
454
|
+
mock_get_regscale_data_and_attachments,
|
|
455
|
+
mock_fetch_jira_objects,
|
|
456
|
+
mock_create_jira_client,
|
|
457
|
+
mock_sync_regscale_to_jira,
|
|
458
|
+
mock_sync_regscale_objects_to_jira,
|
|
459
|
+
fetch_attachments,
|
|
460
|
+
):
|
|
461
|
+
"""Test sync_regscale_and_jira with custom JQL query"""
|
|
462
|
+
# mock jira client so we can check it was correctly used later
|
|
463
|
+
mock_jira_client = MagicMock()
|
|
464
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
465
|
+
|
|
466
|
+
# mock these so that we can control what objects were returned to check later
|
|
467
|
+
mock_fetch_jira_objects.return_value = []
|
|
468
|
+
mock_get_regscale_data_and_attachments.return_value = ([], {})
|
|
469
|
+
|
|
470
|
+
custom_jql = "project = SNES AND assignee = currentUser() AND status != Closed"
|
|
471
|
+
|
|
472
|
+
sync_regscale_and_jira(
|
|
473
|
+
parent_id=self.PARENT_ID,
|
|
474
|
+
parent_module=self.PARENT_MODULE,
|
|
475
|
+
jira_project=self.JIRA_PROJECT,
|
|
476
|
+
jira_issue_type="Bug",
|
|
477
|
+
sync_attachments=fetch_attachments,
|
|
478
|
+
jql=custom_jql,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# check that we get the jira client correctly
|
|
482
|
+
mock_create_jira_client.assert_called_once()
|
|
483
|
+
|
|
484
|
+
# check that we correctly fetch objects from jira and regscale
|
|
485
|
+
mock_get_regscale_data_and_attachments.assert_called_once_with(
|
|
486
|
+
parent_id=self.PARENT_ID,
|
|
487
|
+
parent_module=self.PARENT_MODULE,
|
|
488
|
+
sync_attachments=fetch_attachments,
|
|
489
|
+
sync_tasks_only=False,
|
|
490
|
+
)
|
|
491
|
+
# Verify that the custom JQL was used instead of the default
|
|
492
|
+
mock_fetch_jira_objects.assert_called_once_with(
|
|
493
|
+
jira_client=mock_jira_client,
|
|
494
|
+
jira_project=self.JIRA_PROJECT,
|
|
495
|
+
jql_str=custom_jql,
|
|
496
|
+
jira_issue_type="Bug",
|
|
497
|
+
sync_tasks_only=False,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
@patch(f"{PATH}.sync_regscale_objects_to_jira")
|
|
501
|
+
@patch(f"{PATH}.sync_regscale_to_jira", return_value=[])
|
|
502
|
+
@patch(f"{PATH}.create_jira_client")
|
|
503
|
+
@patch(f"{PATH}.fetch_jira_objects")
|
|
504
|
+
@patch(f"{PATH}.get_regscale_data_and_attachments")
|
|
505
|
+
@patch(f"{PATH}.Api", return_value=MagicMock(spec=Api))
|
|
506
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
507
|
+
def test_sync_regscale_and_jira_custom_jql_tasks(
|
|
508
|
+
self,
|
|
509
|
+
mock_check_license,
|
|
510
|
+
mock_api,
|
|
511
|
+
mock_get_regscale_data_and_attachments,
|
|
512
|
+
mock_fetch_jira_objects,
|
|
513
|
+
mock_create_jira_client,
|
|
514
|
+
mock_sync_regscale_to_jira,
|
|
515
|
+
mock_sync_regscale_objects_to_jira,
|
|
516
|
+
fetch_attachments,
|
|
517
|
+
):
|
|
518
|
+
"""Test sync_regscale_and_jira with custom JQL query for tasks"""
|
|
519
|
+
# mock jira client so we can check it was correctly used later
|
|
520
|
+
mock_jira_client = MagicMock()
|
|
521
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
522
|
+
|
|
523
|
+
# mock these so that we can control what objects were returned to check later
|
|
524
|
+
mock_fetch_jira_objects.return_value = []
|
|
525
|
+
mock_get_regscale_data_and_attachments.return_value = ([], {})
|
|
526
|
+
|
|
527
|
+
custom_jql = "project = SNES AND assignee = currentUser() AND issueType = Task"
|
|
528
|
+
|
|
529
|
+
sync_regscale_and_jira(
|
|
530
|
+
parent_id=self.PARENT_ID,
|
|
531
|
+
parent_module=self.PARENT_MODULE,
|
|
532
|
+
jira_project=self.JIRA_PROJECT,
|
|
533
|
+
jira_issue_type="Task",
|
|
534
|
+
sync_attachments=fetch_attachments,
|
|
535
|
+
sync_tasks_only=True,
|
|
536
|
+
jql=custom_jql,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# check that we get the jira client correctly
|
|
540
|
+
mock_create_jira_client.assert_called_once()
|
|
541
|
+
|
|
542
|
+
# check that we correctly fetch objects from jira and regscale
|
|
543
|
+
mock_get_regscale_data_and_attachments.assert_called_once_with(
|
|
544
|
+
parent_id=self.PARENT_ID,
|
|
545
|
+
parent_module=self.PARENT_MODULE,
|
|
546
|
+
sync_attachments=fetch_attachments,
|
|
547
|
+
sync_tasks_only=True,
|
|
548
|
+
)
|
|
549
|
+
# Verify that the custom JQL was used instead of the default task-specific JQL
|
|
550
|
+
mock_fetch_jira_objects.assert_called_once_with(
|
|
551
|
+
jira_client=mock_jira_client,
|
|
552
|
+
jira_project=self.JIRA_PROJECT,
|
|
553
|
+
jql_str=custom_jql,
|
|
554
|
+
jira_issue_type="Task",
|
|
555
|
+
sync_tasks_only=True,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
559
|
+
def test_sync_regscale_objects_to_jira(
|
|
560
|
+
self, mock_check_license, fetch_attachments, get_jira_issue, regscale_issues_and_attachments
|
|
561
|
+
):
|
|
562
|
+
"""Test syncing RegScale objects to Jira"""
|
|
563
|
+
mock_check_license.return_value.config = self.config
|
|
564
|
+
try:
|
|
565
|
+
sync_regscale_objects_to_jira(
|
|
566
|
+
jira_issues=[get_jira_issue],
|
|
567
|
+
regscale_objects=regscale_issues_and_attachments[0],
|
|
568
|
+
sync_attachments=fetch_attachments,
|
|
569
|
+
app=self.app,
|
|
570
|
+
parent_id=self.PARENT_ID,
|
|
571
|
+
parent_module=self.PARENT_MODULE,
|
|
572
|
+
sync_tasks_only=False,
|
|
573
|
+
)
|
|
574
|
+
except Exception as e:
|
|
575
|
+
pytest.fail("Jira & RegScale task sync failed: {}".format(e))
|
|
576
|
+
|
|
577
|
+
@patch(f"{PATH}.create_jira_client")
|
|
578
|
+
@patch(f"{PATH}.create_and_update_regscale_issues")
|
|
579
|
+
@patch(f"{PATH}.create_and_update_regscale_tasks")
|
|
580
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
581
|
+
def test_sync_regscale_objects_to_jira_tasks_only(
|
|
582
|
+
self,
|
|
583
|
+
mock_check_license,
|
|
584
|
+
mock_create_and_update_regscale_tasks,
|
|
585
|
+
mock_create_and_update_regscale_issues,
|
|
586
|
+
mock_create_jira_client,
|
|
587
|
+
fetch_attachments,
|
|
588
|
+
):
|
|
589
|
+
"""Test syncing RegScale objects to Jira with sync_tasks_only=True"""
|
|
590
|
+
mock_check_license.return_value.config = self.config
|
|
591
|
+
mock_jira_client = MagicMock()
|
|
592
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
593
|
+
mock_create_and_update_regscale_tasks.return_value = (1, 0, 0)
|
|
594
|
+
|
|
595
|
+
sync_regscale_objects_to_jira(
|
|
596
|
+
MagicMock(),
|
|
597
|
+
MagicMock(),
|
|
598
|
+
fetch_attachments,
|
|
599
|
+
MagicMock(spec=Application),
|
|
600
|
+
self.PARENT_ID,
|
|
601
|
+
self.PARENT_MODULE,
|
|
602
|
+
True,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
mock_create_and_update_regscale_tasks.assert_called_once()
|
|
606
|
+
mock_create_and_update_regscale_issues.assert_not_called()
|
|
607
|
+
|
|
608
|
+
@patch(f"{PATH}.create_jira_client")
|
|
609
|
+
@patch(f"{PATH}.create_threads")
|
|
610
|
+
@patch(f"{PATH}.create_and_update_regscale_tasks")
|
|
611
|
+
@patch(f"{PATH}.check_license", return_value=MagicMock(spec=Application))
|
|
612
|
+
def test_sync_regscale_objects_to_jira_all(
|
|
613
|
+
self,
|
|
614
|
+
mock_check_license,
|
|
615
|
+
mock_create_and_update_regscale_tasks,
|
|
616
|
+
mock_create_threads,
|
|
617
|
+
mock_create_jira_client,
|
|
618
|
+
fetch_attachments,
|
|
619
|
+
):
|
|
620
|
+
"""Test syncing RegScale objects to Jira with sync_tasks_only=False"""
|
|
621
|
+
mock_check_license.return_value.config = self.config
|
|
622
|
+
mock_jira_client = MagicMock()
|
|
623
|
+
mock_create_jira_client.return_value = mock_jira_client
|
|
624
|
+
|
|
625
|
+
sync_regscale_objects_to_jira(
|
|
626
|
+
MagicMock(),
|
|
627
|
+
MagicMock(),
|
|
628
|
+
fetch_attachments,
|
|
629
|
+
MagicMock(spec=Application),
|
|
630
|
+
self.PARENT_ID,
|
|
631
|
+
self.PARENT_MODULE,
|
|
632
|
+
False,
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
mock_create_and_update_regscale_tasks.assert_not_called()
|
|
636
|
+
mock_create_threads.assert_called_once()
|
|
637
|
+
|
|
638
|
+
@patch(f"{PATH}.JIRA")
|
|
639
|
+
def test_create_jira_client_basic(self, mock_jira):
|
|
640
|
+
"""Test creating a Jira client"""
|
|
641
|
+
conf = {
|
|
642
|
+
"jiraUrl": "https://example.com",
|
|
643
|
+
"jiraApiToken": "token",
|
|
644
|
+
"jiraUserName": "user",
|
|
645
|
+
}
|
|
646
|
+
_ = create_jira_client(conf, token_auth=False)
|
|
647
|
+
mock_jira.assert_called_once_with(basic_auth=("user", "token"), options={"server": "https://example.com"})
|
|
648
|
+
|
|
649
|
+
@patch(f"{PATH}.JIRA")
|
|
650
|
+
def test_create_jira_client_token(self, mock_jira):
|
|
651
|
+
"""Test creating a Jira client with token auth"""
|
|
652
|
+
from regscale.integrations.variables import ScannerVariables
|
|
653
|
+
|
|
654
|
+
conf = {
|
|
655
|
+
"jiraUrl": "https://example.com",
|
|
656
|
+
"jiraApiToken": "token",
|
|
657
|
+
"jiraUserName": "user",
|
|
658
|
+
}
|
|
659
|
+
_ = create_jira_client(conf, token_auth=True)
|
|
660
|
+
mock_jira.assert_called_once_with(
|
|
661
|
+
token_auth="token", options={"server": "https://example.com", "verify": ScannerVariables.sslVerify}
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
@staticmethod
|
|
665
|
+
def test_jira_issues(jira_issues, fetch_attachments):
|
|
666
|
+
"""Test fetching Jira issues and creating a Jira client"""
|
|
667
|
+
has_attachments = None
|
|
668
|
+
if fetch_attachments:
|
|
669
|
+
assert jira_issues is not None
|
|
670
|
+
assert True in [True if issue.fields.attachment else False for issue in jira_issues]
|
|
671
|
+
else:
|
|
672
|
+
assert jira_issues is not None
|
|
673
|
+
try:
|
|
674
|
+
# try to access the attachment attribute
|
|
675
|
+
has_attachments = [True if issue.fields.attachment else False for issue in jira_issues]
|
|
676
|
+
assert len(has_attachments) == len(jira_issues)
|
|
677
|
+
except AttributeError:
|
|
678
|
+
# if the attribute doesn't exist, then we know there are no attachments
|
|
679
|
+
assert has_attachments is None
|
|
680
|
+
|
|
681
|
+
@staticmethod
|
|
682
|
+
def test_jira_tasks(jira_tasks, fetch_attachments):
|
|
683
|
+
"""Test fetching Jira tasks and creating a Jira client"""
|
|
684
|
+
has_attachments = None
|
|
685
|
+
if fetch_attachments:
|
|
686
|
+
assert jira_tasks is not None
|
|
687
|
+
assert True in [True if task.fields.attachment else False for task in jira_tasks]
|
|
688
|
+
else:
|
|
689
|
+
assert jira_tasks is not None
|
|
690
|
+
try:
|
|
691
|
+
# try to access the attachment attribute
|
|
692
|
+
has_attachments = [True if task.fields.attachment else False for task in jira_tasks]
|
|
693
|
+
assert len(has_attachments) == len(jira_tasks)
|
|
694
|
+
except AttributeError:
|
|
695
|
+
# if the attribute doesn't exist, then we know there are no attachments
|
|
696
|
+
assert has_attachments is None
|
|
697
|
+
|
|
698
|
+
@staticmethod
|
|
699
|
+
def test_fetch_regscale_issues_and_attachments(regscale_issues_and_attachments, fetch_attachments):
|
|
700
|
+
"""Test fetching RegScale issues and attachments"""
|
|
701
|
+
issues, attachments = regscale_issues_and_attachments
|
|
702
|
+
assert issues is not None
|
|
703
|
+
if fetch_attachments:
|
|
704
|
+
assert attachments is not None
|
|
705
|
+
else:
|
|
706
|
+
assert attachments == []
|
|
707
|
+
|
|
708
|
+
@staticmethod
|
|
709
|
+
def test_fetch_regscale_tasks_and_attachments(regscale_tasks_and_attachments, fetch_attachments):
|
|
710
|
+
"""Test fetching RegScale tasks and attachments"""
|
|
711
|
+
tasks, attachments = regscale_tasks_and_attachments
|
|
712
|
+
assert tasks is not None
|
|
713
|
+
if fetch_attachments:
|
|
714
|
+
assert attachments != []
|
|
715
|
+
else:
|
|
716
|
+
assert attachments == []
|
|
717
|
+
|
|
718
|
+
@pytest.mark.parametrize(
|
|
719
|
+
"due_date,priority,expected_days",
|
|
720
|
+
[
|
|
721
|
+
("2024-12-31", "High", 7), # Has due date
|
|
722
|
+
(None, "High", 7), # No due date, high priority
|
|
723
|
+
(None, "Medium", 14), # No due date, medium priority
|
|
724
|
+
(None, "Low", 30), # No due date, low priority
|
|
725
|
+
(None, None, 14), # No due date, no priority (defaults to medium)
|
|
726
|
+
],
|
|
727
|
+
)
|
|
728
|
+
@patch(f"{PATH}.datetime")
|
|
729
|
+
def test_map_jira_due_date(self, mock_datetime, due_date, priority, expected_days):
|
|
730
|
+
"""Test mapping Jira due dates to RegScale format"""
|
|
731
|
+
# Set up mock datetime to return a fixed date
|
|
732
|
+
fixed_date = datetime(2024, 1, 1, 12, 0, 0)
|
|
733
|
+
mock_datetime.now.return_value = fixed_date
|
|
734
|
+
mock_datetime.timedelta = timedelta # Allow timedelta to work normally
|
|
735
|
+
|
|
736
|
+
# Create mock Jira issue
|
|
737
|
+
mock_issue = MagicMock()
|
|
738
|
+
mock_issue.fields.duedate = due_date
|
|
739
|
+
if priority:
|
|
740
|
+
mock_issue.fields.priority = MagicMock()
|
|
741
|
+
mock_issue.fields.priority.name = priority
|
|
742
|
+
else:
|
|
743
|
+
mock_issue.fields.priority = None
|
|
744
|
+
|
|
745
|
+
# Create mock config
|
|
746
|
+
mock_config = {"issues": {"jira": {"high": 7, "medium": 14, "low": 30}}}
|
|
747
|
+
|
|
748
|
+
result = map_jira_due_date(mock_issue, mock_config)
|
|
749
|
+
|
|
750
|
+
if due_date:
|
|
751
|
+
assert result == due_date
|
|
752
|
+
else:
|
|
753
|
+
# Calculate expected date using the same fixed date
|
|
754
|
+
expected_date = fixed_date + timedelta(days=expected_days)
|
|
755
|
+
result_date = datetime.strptime(result, "%Y-%m-%d %H:%M:%S")
|
|
756
|
+
assert result_date.date() == expected_date.date()
|
|
757
|
+
|
|
758
|
+
@pytest.mark.parametrize(
|
|
759
|
+
"status,expected_status",
|
|
760
|
+
[
|
|
761
|
+
("Done", "Closed"),
|
|
762
|
+
("In Progress", "Open"),
|
|
763
|
+
("To Do", "Open"),
|
|
764
|
+
],
|
|
765
|
+
)
|
|
766
|
+
def test_map_jira_to_regscale_issue(self, status, expected_status):
|
|
767
|
+
"""Test mapping Jira issues to RegScale format"""
|
|
768
|
+
# Create mock Jira issue
|
|
769
|
+
issue_status = MagicMock()
|
|
770
|
+
issue_status.name = status
|
|
771
|
+
mock_issue = MagicMock(
|
|
772
|
+
key="TEST-123",
|
|
773
|
+
fields=MagicMock(
|
|
774
|
+
summary="Skipped task",
|
|
775
|
+
description="Skipped task description",
|
|
776
|
+
status=issue_status,
|
|
777
|
+
duedate=None,
|
|
778
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
779
|
+
),
|
|
780
|
+
)
|
|
781
|
+
mock_issue.fields.priority = MagicMock()
|
|
782
|
+
mock_issue.fields.priority.name = "High"
|
|
783
|
+
|
|
784
|
+
# Create mock config
|
|
785
|
+
mock_config = {
|
|
786
|
+
"userId": "1", # Convert to string to match Optional[str] type
|
|
787
|
+
"issues": {"jira": {"status": "Open", "high": 7, "medium": 14, "low": 30}},
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
result = map_jira_to_regscale_issue(mock_issue, mock_config, 1, "issues")
|
|
791
|
+
|
|
792
|
+
# Verify the Issue object was created with correct attributes
|
|
793
|
+
assert isinstance(result, Issue)
|
|
794
|
+
assert result.title == "Skipped task"
|
|
795
|
+
assert "Skipped task description" in result.description
|
|
796
|
+
assert result.status == expected_status
|
|
797
|
+
assert result.jiraId == "TEST-123"
|
|
798
|
+
assert result.parentId == 1
|
|
799
|
+
assert result.parentModule == "issues"
|
|
800
|
+
assert result.dueDate is not None # Due date will be calculated based on priority
|
|
801
|
+
if status == "Done":
|
|
802
|
+
assert result.dateCompleted is not None
|
|
803
|
+
else:
|
|
804
|
+
assert result.dateCompleted is None
|
|
805
|
+
|
|
806
|
+
@pytest.mark.parametrize(
|
|
807
|
+
"status,expected_status,expected_percent_complete,expected_date_closed",
|
|
808
|
+
[
|
|
809
|
+
("Done", "Closed", 100, True),
|
|
810
|
+
("In Progress", "Open", None, False),
|
|
811
|
+
("To Do", "Backlog", None, False),
|
|
812
|
+
],
|
|
813
|
+
)
|
|
814
|
+
def test_create_regscale_task_from_jira(
|
|
815
|
+
self, status, expected_status, expected_percent_complete, expected_date_closed
|
|
816
|
+
):
|
|
817
|
+
"""Test creating RegScale tasks from Jira issues"""
|
|
818
|
+
# Create mock Jira issue
|
|
819
|
+
mock_issue = MagicMock()
|
|
820
|
+
mock_issue.fields.summary = "Test Task"
|
|
821
|
+
mock_issue.fields.description = "Test Description"
|
|
822
|
+
mock_issue.fields.status.name = status
|
|
823
|
+
mock_issue.fields.duedate = "2024-12-31"
|
|
824
|
+
mock_issue.fields.statuscategorychangedate = "2024-01-01T12:00:00.000Z"
|
|
825
|
+
mock_issue.key = "TEST-123"
|
|
826
|
+
|
|
827
|
+
# Create mock config
|
|
828
|
+
mock_config = {"issues": {"jira": {"medium": 14}}}
|
|
829
|
+
|
|
830
|
+
result = create_regscale_task_from_jira(mock_config, mock_issue, 1, "issues")
|
|
831
|
+
|
|
832
|
+
assert result.title == "Test Task"
|
|
833
|
+
assert result.description == "Test Description"
|
|
834
|
+
assert result.status == expected_status
|
|
835
|
+
assert result.dueDate == "2024-12-31"
|
|
836
|
+
assert result.parentId == 1
|
|
837
|
+
assert result.parentModule == "issues"
|
|
838
|
+
assert result.otherIdentifier == "TEST-123"
|
|
839
|
+
if expected_percent_complete:
|
|
840
|
+
assert result.percentComplete == expected_percent_complete
|
|
841
|
+
if expected_date_closed:
|
|
842
|
+
assert result.dateClosed is not None
|
|
843
|
+
else:
|
|
844
|
+
assert result.dateClosed is None
|
|
845
|
+
|
|
846
|
+
def test_check_and_close_tasks(self):
|
|
847
|
+
"""Test checking and closing tasks"""
|
|
848
|
+
jira_titles = {"Testing1234"}
|
|
849
|
+
tasks = [
|
|
850
|
+
Task(
|
|
851
|
+
id=3,
|
|
852
|
+
title="Different Title",
|
|
853
|
+
status="Backlog",
|
|
854
|
+
dueDate=get_current_datetime(),
|
|
855
|
+
dateClosed="",
|
|
856
|
+
percentComplete=0,
|
|
857
|
+
),
|
|
858
|
+
Task(
|
|
859
|
+
id=4,
|
|
860
|
+
title="Testing1234",
|
|
861
|
+
status="Backlog",
|
|
862
|
+
dueDate=get_current_datetime(),
|
|
863
|
+
dateClosed="",
|
|
864
|
+
percentComplete=0,
|
|
865
|
+
),
|
|
866
|
+
]
|
|
867
|
+
|
|
868
|
+
closed_tasks = check_and_close_tasks(tasks, set(jira_titles))
|
|
869
|
+
assert len(closed_tasks) == 1
|
|
870
|
+
assert closed_tasks[0].status == "Closed"
|
|
871
|
+
assert closed_tasks[0].percentComplete == 100
|
|
872
|
+
|
|
873
|
+
def test_process_tasks_for_sync(self):
|
|
874
|
+
"""Test processing tasks for sync"""
|
|
875
|
+
todo_status = MagicMock()
|
|
876
|
+
todo_status.name = "to do"
|
|
877
|
+
in_progress_status = MagicMock()
|
|
878
|
+
in_progress_status.name = "in progress"
|
|
879
|
+
done_status = MagicMock()
|
|
880
|
+
done_status.name = "done"
|
|
881
|
+
# Create mock Jira issues
|
|
882
|
+
jira_tasks = [
|
|
883
|
+
MagicMock( # should be skipped (up to date - nothing happens)
|
|
884
|
+
key="JIRA-1",
|
|
885
|
+
fields=MagicMock(
|
|
886
|
+
summary="Skipped task",
|
|
887
|
+
description="Skipped task description",
|
|
888
|
+
status=todo_status,
|
|
889
|
+
duedate=None,
|
|
890
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
891
|
+
priority=None,
|
|
892
|
+
),
|
|
893
|
+
),
|
|
894
|
+
MagicMock( # should be inserted (task in jira but not regscale)
|
|
895
|
+
key="JIRA-2",
|
|
896
|
+
fields=MagicMock(
|
|
897
|
+
summary="New task",
|
|
898
|
+
description="New task description",
|
|
899
|
+
status=todo_status,
|
|
900
|
+
duedate=None,
|
|
901
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
902
|
+
priority=None,
|
|
903
|
+
),
|
|
904
|
+
),
|
|
905
|
+
MagicMock( # should be updated (task in both but out of sync)
|
|
906
|
+
key="JIRA-3",
|
|
907
|
+
fields=MagicMock(
|
|
908
|
+
summary="Existing task",
|
|
909
|
+
description="Existing task description",
|
|
910
|
+
status=in_progress_status,
|
|
911
|
+
duedate=None,
|
|
912
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
913
|
+
priority=None,
|
|
914
|
+
),
|
|
915
|
+
),
|
|
916
|
+
MagicMock( # should be updated
|
|
917
|
+
key="JIRA-4",
|
|
918
|
+
fields=MagicMock(
|
|
919
|
+
summary="Existing task",
|
|
920
|
+
description="Existing task description",
|
|
921
|
+
status=todo_status,
|
|
922
|
+
duedate=None,
|
|
923
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
924
|
+
priority=None,
|
|
925
|
+
),
|
|
926
|
+
),
|
|
927
|
+
MagicMock( # should be closed
|
|
928
|
+
key="JIRA-5",
|
|
929
|
+
fields=MagicMock(
|
|
930
|
+
summary="Existing task",
|
|
931
|
+
description="Existing task description",
|
|
932
|
+
status=done_status,
|
|
933
|
+
duedate=None,
|
|
934
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
935
|
+
priority=None,
|
|
936
|
+
),
|
|
937
|
+
),
|
|
938
|
+
MagicMock( # date to be updated
|
|
939
|
+
key="JIRA-6",
|
|
940
|
+
fields=MagicMock(
|
|
941
|
+
summary="Existing task",
|
|
942
|
+
description="Existing task description",
|
|
943
|
+
status=todo_status,
|
|
944
|
+
duedate="2024-12-31",
|
|
945
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
946
|
+
priority=None,
|
|
947
|
+
),
|
|
948
|
+
),
|
|
949
|
+
]
|
|
950
|
+
regscale_tasks = [
|
|
951
|
+
Task( # matches JIRA-1 (should be skipped - up to date)
|
|
952
|
+
id=1,
|
|
953
|
+
title="Skipped task",
|
|
954
|
+
status="Backlog",
|
|
955
|
+
description="Skipped task description",
|
|
956
|
+
otherIdentifier="JIRA-1",
|
|
957
|
+
parentId=self.PARENT_ID,
|
|
958
|
+
parentModule=self.PARENT_MODULE,
|
|
959
|
+
dueDate=get_current_datetime(),
|
|
960
|
+
),
|
|
961
|
+
Task( # matches JIRA-3 (should be updated - task in both but out of sync)
|
|
962
|
+
id=3,
|
|
963
|
+
title="Existing task",
|
|
964
|
+
status="Open",
|
|
965
|
+
description="Different description", # Different description to show sync needed
|
|
966
|
+
otherIdentifier="JIRA-3",
|
|
967
|
+
parentId=self.PARENT_ID,
|
|
968
|
+
parentModule=self.PARENT_MODULE,
|
|
969
|
+
dueDate=get_current_datetime(),
|
|
970
|
+
),
|
|
971
|
+
Task( # matches JIRA-4 (should be updated - regscale closed but jira open)
|
|
972
|
+
id=4,
|
|
973
|
+
title="Existing task",
|
|
974
|
+
status="Closed", # Closed in RegScale but open in Jira
|
|
975
|
+
description="Existing task description",
|
|
976
|
+
otherIdentifier="JIRA-4",
|
|
977
|
+
parentId=self.PARENT_ID,
|
|
978
|
+
parentModule=self.PARENT_MODULE,
|
|
979
|
+
dueDate=get_current_datetime(),
|
|
980
|
+
),
|
|
981
|
+
Task( # matches JIRA-5 (should be closed - jira closed but regscale open)
|
|
982
|
+
id=5,
|
|
983
|
+
title="Existing task",
|
|
984
|
+
status="Open", # Open in RegScale but closed in Jira
|
|
985
|
+
description="Existing task description",
|
|
986
|
+
otherIdentifier="JIRA-5",
|
|
987
|
+
parentId=self.PARENT_ID,
|
|
988
|
+
parentModule=self.PARENT_MODULE,
|
|
989
|
+
dueDate=get_current_datetime(),
|
|
990
|
+
),
|
|
991
|
+
Task(
|
|
992
|
+
id=6,
|
|
993
|
+
title="New task",
|
|
994
|
+
status="Backlog",
|
|
995
|
+
description="Not in jira",
|
|
996
|
+
otherIdentifier="JIRA-19",
|
|
997
|
+
parentId=self.PARENT_ID,
|
|
998
|
+
parentModule=self.PARENT_MODULE,
|
|
999
|
+
),
|
|
1000
|
+
Task(
|
|
1001
|
+
id=7,
|
|
1002
|
+
title="Existing task",
|
|
1003
|
+
status="Backlog",
|
|
1004
|
+
description="Existing task description",
|
|
1005
|
+
otherIdentifier="JIRA-6",
|
|
1006
|
+
parentId=self.PARENT_ID,
|
|
1007
|
+
parentModule=self.PARENT_MODULE,
|
|
1008
|
+
dueDate="2023-01-01",
|
|
1009
|
+
),
|
|
1010
|
+
]
|
|
1011
|
+
|
|
1012
|
+
progress = MagicMock(spec=Progress)
|
|
1013
|
+
progress_task = MagicMock()
|
|
1014
|
+
|
|
1015
|
+
insert_tasks, update_tasks, close_tasks = process_tasks_for_sync(
|
|
1016
|
+
config=self.config,
|
|
1017
|
+
jira_issues=jira_tasks,
|
|
1018
|
+
existing_tasks=regscale_tasks,
|
|
1019
|
+
parent_id=self.PARENT_ID,
|
|
1020
|
+
parent_module=self.PARENT_MODULE,
|
|
1021
|
+
progress=progress,
|
|
1022
|
+
progress_task=progress_task,
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
# Assertions
|
|
1026
|
+
assert len(insert_tasks) == 1 # New task should be inserted
|
|
1027
|
+
assert len(update_tasks) == 3 # No updates needed
|
|
1028
|
+
assert len(close_tasks) == 1 # Existing task should be closed
|
|
1029
|
+
|
|
1030
|
+
@patch(f"{PATH}.process_tasks_for_sync")
|
|
1031
|
+
@patch(f"{PATH}.task_and_attachments_sync")
|
|
1032
|
+
@patch("regscale.core.app.api.Api")
|
|
1033
|
+
@patch(f"{PATH}.ThreadPoolExecutor")
|
|
1034
|
+
@patch(f"{PATH}.as_completed")
|
|
1035
|
+
def test_create_and_update_regscale_tasks(
|
|
1036
|
+
self, mock_as_completed, mock_thread_pool, mock_api, mock_task_sync, mock_process_tasks_for_sync
|
|
1037
|
+
):
|
|
1038
|
+
"""Test creating and updating RegScale tasks from Jira tasks"""
|
|
1039
|
+
# Setup mock tasks
|
|
1040
|
+
insert_tasks = [
|
|
1041
|
+
Task(
|
|
1042
|
+
id=1,
|
|
1043
|
+
title="New task",
|
|
1044
|
+
status="Backlog",
|
|
1045
|
+
description="New task description",
|
|
1046
|
+
otherIdentifier="JIRA-19",
|
|
1047
|
+
parentId=self.PARENT_ID,
|
|
1048
|
+
parentModule=self.PARENT_MODULE,
|
|
1049
|
+
)
|
|
1050
|
+
]
|
|
1051
|
+
update_tasks = [
|
|
1052
|
+
Task(
|
|
1053
|
+
id=2,
|
|
1054
|
+
title="Existing task",
|
|
1055
|
+
status="Open",
|
|
1056
|
+
description="Existing task description",
|
|
1057
|
+
otherIdentifier="JIRA-3",
|
|
1058
|
+
parentId=self.PARENT_ID,
|
|
1059
|
+
parentModule=self.PARENT_MODULE,
|
|
1060
|
+
)
|
|
1061
|
+
]
|
|
1062
|
+
close_tasks = [
|
|
1063
|
+
Task(
|
|
1064
|
+
id=3,
|
|
1065
|
+
title="Existing task",
|
|
1066
|
+
status="Closed",
|
|
1067
|
+
description="Existing task description",
|
|
1068
|
+
otherIdentifier="JIRA-5",
|
|
1069
|
+
parentId=self.PARENT_ID,
|
|
1070
|
+
parentModule=self.PARENT_MODULE,
|
|
1071
|
+
)
|
|
1072
|
+
]
|
|
1073
|
+
mock_process_tasks_for_sync.return_value = (insert_tasks, update_tasks, close_tasks)
|
|
1074
|
+
|
|
1075
|
+
mock_api_instance = MagicMock()
|
|
1076
|
+
mock_api.return_value = mock_api_instance
|
|
1077
|
+
mock_api_instance.app.config = self.config
|
|
1078
|
+
|
|
1079
|
+
# Setup mock thread pool
|
|
1080
|
+
mock_executor = MagicMock()
|
|
1081
|
+
mock_thread_pool.return_value.__enter__.return_value = mock_executor
|
|
1082
|
+
|
|
1083
|
+
# Setup task_and_attachments_sync to return None
|
|
1084
|
+
mock_task_sync.return_value = None
|
|
1085
|
+
|
|
1086
|
+
# Make as_completed return an empty iterator
|
|
1087
|
+
mock_as_completed.return_value = []
|
|
1088
|
+
|
|
1089
|
+
inserted, updated, closed = create_and_update_regscale_tasks(
|
|
1090
|
+
jira_issues=[],
|
|
1091
|
+
existing_tasks=[],
|
|
1092
|
+
jira_client=MagicMock(),
|
|
1093
|
+
parent_id=self.PARENT_ID,
|
|
1094
|
+
parent_module=self.PARENT_MODULE,
|
|
1095
|
+
progress=MagicMock(spec=Progress),
|
|
1096
|
+
progress_task=MagicMock(),
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
assert inserted == 1
|
|
1100
|
+
assert updated == 1
|
|
1101
|
+
assert closed == 1
|
|
1102
|
+
|
|
1103
|
+
mock_thread_pool.assert_called_once_with(max_workers=10)
|
|
1104
|
+
assert mock_executor.submit.call_count == 3
|
|
1105
|
+
|
|
1106
|
+
@patch(f"{PATH}.compare_files_for_dupes_and_upload")
|
|
1107
|
+
def test_tasks_and_attachments_sync_create(self, mock_compare_files_for_dupes_and_upload):
|
|
1108
|
+
"""Test performing create operation on tasks in task_and_attachments_sync"""
|
|
1109
|
+
# check if operation fails
|
|
1110
|
+
mock_task = MagicMock()
|
|
1111
|
+
mock_task.create.return_value = None
|
|
1112
|
+
task_and_attachments_sync(
|
|
1113
|
+
operation="create",
|
|
1114
|
+
task=mock_task,
|
|
1115
|
+
jira_client=MagicMock(),
|
|
1116
|
+
api=MagicMock(),
|
|
1117
|
+
)
|
|
1118
|
+
mock_task.create.assert_called_once()
|
|
1119
|
+
mock_compare_files_for_dupes_and_upload.assert_not_called()
|
|
1120
|
+
|
|
1121
|
+
# check if operation is successful
|
|
1122
|
+
mock_task.create.reset_mock()
|
|
1123
|
+
mock_task.create.return_value = MagicMock()
|
|
1124
|
+
task_and_attachments_sync(
|
|
1125
|
+
operation="create",
|
|
1126
|
+
task=mock_task,
|
|
1127
|
+
jira_client=MagicMock(),
|
|
1128
|
+
api=MagicMock(),
|
|
1129
|
+
)
|
|
1130
|
+
mock_task.create.assert_called_once()
|
|
1131
|
+
mock_compare_files_for_dupes_and_upload.assert_called_once()
|
|
1132
|
+
|
|
1133
|
+
@pytest.mark.parametrize("operation", ["update", "close"])
|
|
1134
|
+
@patch(f"{PATH}.compare_files_for_dupes_and_upload")
|
|
1135
|
+
def test_tasks_and_attachments_sync_save(self, mock_compare_files_for_dupes_and_upload, operation):
|
|
1136
|
+
"""Test performing save operation on tasks in task_and_attachments_sync"""
|
|
1137
|
+
# check if operation fails
|
|
1138
|
+
mock_task = MagicMock()
|
|
1139
|
+
mock_task.save.return_value = None
|
|
1140
|
+
task_and_attachments_sync(
|
|
1141
|
+
operation=operation,
|
|
1142
|
+
task=mock_task,
|
|
1143
|
+
jira_client=MagicMock(),
|
|
1144
|
+
api=MagicMock(),
|
|
1145
|
+
)
|
|
1146
|
+
mock_task.save.assert_called_once()
|
|
1147
|
+
mock_compare_files_for_dupes_and_upload.assert_not_called()
|
|
1148
|
+
|
|
1149
|
+
# check if operation is successful
|
|
1150
|
+
mock_task.save.reset_mock()
|
|
1151
|
+
mock_task.save.return_value = MagicMock()
|
|
1152
|
+
task_and_attachments_sync(
|
|
1153
|
+
operation=operation,
|
|
1154
|
+
task=mock_task,
|
|
1155
|
+
jira_client=MagicMock(),
|
|
1156
|
+
api=MagicMock(),
|
|
1157
|
+
)
|
|
1158
|
+
mock_task.save.assert_called_once()
|
|
1159
|
+
mock_compare_files_for_dupes_and_upload.assert_called_once()
|
|
1160
|
+
|
|
1161
|
+
@patch(f"{PATH}.compare_files_for_dupes_and_upload")
|
|
1162
|
+
@patch(f"{PATH}.Issue.insert_issue")
|
|
1163
|
+
@patch(f"{PATH}.Issue.update_issue")
|
|
1164
|
+
@patch(f"{PATH}.job_progress", return_value=MagicMock(spec=Progress))
|
|
1165
|
+
def test_create_and_update_regscale_issues(
|
|
1166
|
+
self,
|
|
1167
|
+
mock_job_progress_object,
|
|
1168
|
+
mock_update_issue,
|
|
1169
|
+
mock_insert_issue,
|
|
1170
|
+
mock_compare_files_for_dupes_and_upload,
|
|
1171
|
+
fetch_attachments,
|
|
1172
|
+
):
|
|
1173
|
+
"""Test creating and updating RegScale issues from Jira issues"""
|
|
1174
|
+
open_status = MagicMock()
|
|
1175
|
+
open_status.name = "open"
|
|
1176
|
+
in_progress_status = MagicMock()
|
|
1177
|
+
in_progress_status.name = "in progress"
|
|
1178
|
+
closed_status = MagicMock()
|
|
1179
|
+
closed_status.name = "done"
|
|
1180
|
+
|
|
1181
|
+
highest_priority = MagicMock()
|
|
1182
|
+
highest_priority.name = "highest"
|
|
1183
|
+
high_priority = MagicMock()
|
|
1184
|
+
high_priority.name = "high"
|
|
1185
|
+
medium_priority = MagicMock()
|
|
1186
|
+
medium_priority.name = "medium"
|
|
1187
|
+
low_priority = MagicMock()
|
|
1188
|
+
low_priority.name = "low"
|
|
1189
|
+
lowest_priority = MagicMock()
|
|
1190
|
+
lowest_priority.name = "lowest"
|
|
1191
|
+
|
|
1192
|
+
# Create mock Jira issues
|
|
1193
|
+
jira_issues = [
|
|
1194
|
+
MagicMock( # should be skipped (up to date - nothing happens) - counts as updated
|
|
1195
|
+
key="JIRA-1",
|
|
1196
|
+
fields=MagicMock(
|
|
1197
|
+
summary="Skipped issue",
|
|
1198
|
+
description="Skipped issue description",
|
|
1199
|
+
status=open_status,
|
|
1200
|
+
duedate=None,
|
|
1201
|
+
priority=highest_priority,
|
|
1202
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
1203
|
+
attachment=MagicMock(),
|
|
1204
|
+
),
|
|
1205
|
+
),
|
|
1206
|
+
MagicMock( # should be inserted (issue in jira but not regscale)
|
|
1207
|
+
key="JIRA-2",
|
|
1208
|
+
fields=MagicMock(
|
|
1209
|
+
summary="New issue",
|
|
1210
|
+
description="New issue description",
|
|
1211
|
+
status=open_status,
|
|
1212
|
+
duedate=None,
|
|
1213
|
+
priority=medium_priority,
|
|
1214
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
1215
|
+
attachment=None,
|
|
1216
|
+
),
|
|
1217
|
+
),
|
|
1218
|
+
MagicMock( # should be updated (issue in both but out of sync)
|
|
1219
|
+
key="JIRA-3",
|
|
1220
|
+
fields=MagicMock(
|
|
1221
|
+
summary="Existing issue",
|
|
1222
|
+
description="Existing issue description",
|
|
1223
|
+
status=in_progress_status,
|
|
1224
|
+
duedate=None,
|
|
1225
|
+
priority=low_priority,
|
|
1226
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
1227
|
+
attachment=None, # No attachments
|
|
1228
|
+
),
|
|
1229
|
+
),
|
|
1230
|
+
MagicMock( # should be closed - counts as updated
|
|
1231
|
+
key="JIRA-4",
|
|
1232
|
+
fields=MagicMock(
|
|
1233
|
+
summary="Existing issue",
|
|
1234
|
+
description="Existing issue description",
|
|
1235
|
+
status=closed_status,
|
|
1236
|
+
duedate=None,
|
|
1237
|
+
priority=lowest_priority,
|
|
1238
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
1239
|
+
attachment=None, # No attachments
|
|
1240
|
+
),
|
|
1241
|
+
),
|
|
1242
|
+
]
|
|
1243
|
+
|
|
1244
|
+
# Create RegScale issues
|
|
1245
|
+
regscale_issues = [
|
|
1246
|
+
Issue( # matches JIRA-1 (should be skipped - up to date)
|
|
1247
|
+
id=1,
|
|
1248
|
+
title="Skipped issue",
|
|
1249
|
+
status="Open",
|
|
1250
|
+
description="Skipped issue description",
|
|
1251
|
+
jiraId="JIRA-1",
|
|
1252
|
+
parentId=self.PARENT_ID,
|
|
1253
|
+
parentModule=self.PARENT_MODULE,
|
|
1254
|
+
dueDate=get_current_datetime(),
|
|
1255
|
+
),
|
|
1256
|
+
Issue( # matches JIRA-3 (should be updated - issue in both but out of sync)
|
|
1257
|
+
id=3,
|
|
1258
|
+
title="Existing issue",
|
|
1259
|
+
status="Open",
|
|
1260
|
+
description="Different description", # Different description to show sync needed
|
|
1261
|
+
jiraId="JIRA-3",
|
|
1262
|
+
parentId=self.PARENT_ID,
|
|
1263
|
+
parentModule=self.PARENT_MODULE,
|
|
1264
|
+
dueDate=get_current_datetime(),
|
|
1265
|
+
),
|
|
1266
|
+
Issue( # matches JIRA-4 (should be closed - jira closed but regscale open)
|
|
1267
|
+
id=4,
|
|
1268
|
+
title="Existing issue",
|
|
1269
|
+
status="Open", # Open in RegScale but closed in Jira
|
|
1270
|
+
description="Existing issue description",
|
|
1271
|
+
jiraId="JIRA-4",
|
|
1272
|
+
parentId=self.PARENT_ID,
|
|
1273
|
+
parentModule=self.PARENT_MODULE,
|
|
1274
|
+
dueDate=get_current_datetime(),
|
|
1275
|
+
),
|
|
1276
|
+
]
|
|
1277
|
+
|
|
1278
|
+
# Create mock config with priority mappings from init.yaml
|
|
1279
|
+
config = {
|
|
1280
|
+
"issues": {"jira": {"highest": 7, "high": 30, "medium": 90, "low": 180, "lowest": 365, "status": "Open"}},
|
|
1281
|
+
"maxThreads": 4, # Set to match number of issues to process
|
|
1282
|
+
"userId": "123e4567-e89b-12d3-a456-426614174000", # Add a fake UUID for userId for issue owner
|
|
1283
|
+
}
|
|
1284
|
+
app = MagicMock()
|
|
1285
|
+
app.config = config
|
|
1286
|
+
|
|
1287
|
+
# Setup mock return values
|
|
1288
|
+
mock_update_issue.return_value = MagicMock()
|
|
1289
|
+
mock_insert_issue.return_value = MagicMock()
|
|
1290
|
+
|
|
1291
|
+
with mock_job_progress_object as job_progress:
|
|
1292
|
+
test_task = job_progress.add_task(
|
|
1293
|
+
description="Processing issues",
|
|
1294
|
+
total=len(jira_issues),
|
|
1295
|
+
visible=False,
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
args = (
|
|
1299
|
+
jira_issues, # Pass all issues at once
|
|
1300
|
+
regscale_issues,
|
|
1301
|
+
fetch_attachments,
|
|
1302
|
+
MagicMock(),
|
|
1303
|
+
app,
|
|
1304
|
+
self.PARENT_ID,
|
|
1305
|
+
self.PARENT_MODULE,
|
|
1306
|
+
test_task,
|
|
1307
|
+
job_progress,
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
# Test each thread
|
|
1311
|
+
for thread in range(4):
|
|
1312
|
+
create_and_update_regscale_issues(
|
|
1313
|
+
args=args,
|
|
1314
|
+
thread=thread,
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
assert mock_update_issue.call_count == 3 # JIRA-3, JIRA-4, and JIRA-1
|
|
1318
|
+
assert mock_insert_issue.call_count == 1 # JIRA-2
|
|
1319
|
+
if fetch_attachments:
|
|
1320
|
+
assert mock_compare_files_for_dupes_and_upload.call_count == 1 # JIRA-2
|
|
1321
|
+
else:
|
|
1322
|
+
assert mock_compare_files_for_dupes_and_upload.call_count == 0
|
|
1323
|
+
|
|
1324
|
+
@patch(f"{PATH}.create_issue_in_jira")
|
|
1325
|
+
def test_sync_regscale_issues_to_jira(self, mock_create_issue_in_jira, fetch_attachments):
|
|
1326
|
+
"""Test inserting Regscale issues into jira if they do not exist"""
|
|
1327
|
+
|
|
1328
|
+
# Create RegScale issues
|
|
1329
|
+
regscale_objects = [
|
|
1330
|
+
Issue(
|
|
1331
|
+
id=1,
|
|
1332
|
+
title="Test Issue",
|
|
1333
|
+
status="Open",
|
|
1334
|
+
description="This is a test issue",
|
|
1335
|
+
dueDate=get_current_datetime(),
|
|
1336
|
+
parentId=self.PARENT_ID,
|
|
1337
|
+
parentModule=self.PARENT_MODULE,
|
|
1338
|
+
),
|
|
1339
|
+
Issue(
|
|
1340
|
+
id=3,
|
|
1341
|
+
title="Test Issue with Jira ID",
|
|
1342
|
+
status="Open",
|
|
1343
|
+
description="This is a test issue with Jira ID",
|
|
1344
|
+
dueDate=get_current_datetime(),
|
|
1345
|
+
parentId=self.PARENT_ID,
|
|
1346
|
+
parentModule=self.PARENT_MODULE,
|
|
1347
|
+
jiraId="JIRA-3",
|
|
1348
|
+
),
|
|
1349
|
+
]
|
|
1350
|
+
|
|
1351
|
+
# Create mock Jira issues
|
|
1352
|
+
open_status = MagicMock()
|
|
1353
|
+
open_status.name = "open"
|
|
1354
|
+
|
|
1355
|
+
returned_jira_issues = [
|
|
1356
|
+
MagicMock(
|
|
1357
|
+
key="JIRA-1",
|
|
1358
|
+
fields=MagicMock(
|
|
1359
|
+
summary="Test Issue",
|
|
1360
|
+
description="This is a test issue",
|
|
1361
|
+
status=open_status,
|
|
1362
|
+
duedate=None,
|
|
1363
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
1364
|
+
attachment=None,
|
|
1365
|
+
),
|
|
1366
|
+
)
|
|
1367
|
+
]
|
|
1368
|
+
|
|
1369
|
+
mock_create_issue_in_jira.side_effect = returned_jira_issues
|
|
1370
|
+
|
|
1371
|
+
new_regscale_objects = sync_regscale_to_jira(
|
|
1372
|
+
regscale_objects=regscale_objects,
|
|
1373
|
+
jira_client=MagicMock(),
|
|
1374
|
+
jira_project=self.JIRA_PROJECT,
|
|
1375
|
+
jira_issue_type="Issue", # Using Issue type for issues
|
|
1376
|
+
sync_attachments=fetch_attachments,
|
|
1377
|
+
attachments={},
|
|
1378
|
+
api=MagicMock(),
|
|
1379
|
+
)
|
|
1380
|
+
|
|
1381
|
+
assert len(new_regscale_objects) == 1 # Only one new issue should be created
|
|
1382
|
+
assert mock_create_issue_in_jira.call_count == 1 # Should only be called once for the issue without Jira ID
|
|
1383
|
+
assert new_regscale_objects[0].jiraId == "JIRA-1"
|
|
1384
|
+
|
|
1385
|
+
@patch(f"{PATH}.create_issue_in_jira")
|
|
1386
|
+
def test_sync_regscale_tasks_to_jira(self, mock_create_issue_in_jira, fetch_attachments):
|
|
1387
|
+
"""Test inserting Regscale tasks into jira if they do not exist"""
|
|
1388
|
+
|
|
1389
|
+
# Create RegScale tasks
|
|
1390
|
+
regscale_objects = [
|
|
1391
|
+
Task(
|
|
1392
|
+
id=2,
|
|
1393
|
+
title="Test Task",
|
|
1394
|
+
status="Backlog",
|
|
1395
|
+
description="This is a test task",
|
|
1396
|
+
dueDate=get_current_datetime(),
|
|
1397
|
+
parentId=self.PARENT_ID,
|
|
1398
|
+
parentModule=self.PARENT_MODULE,
|
|
1399
|
+
),
|
|
1400
|
+
Task(
|
|
1401
|
+
id=4,
|
|
1402
|
+
title="Test Task with Other ID",
|
|
1403
|
+
status="Backlog",
|
|
1404
|
+
description="This is a test task with other ID",
|
|
1405
|
+
dueDate=get_current_datetime(),
|
|
1406
|
+
parentId=self.PARENT_ID,
|
|
1407
|
+
parentModule=self.PARENT_MODULE,
|
|
1408
|
+
otherIdentifier="JIRA-4",
|
|
1409
|
+
),
|
|
1410
|
+
]
|
|
1411
|
+
|
|
1412
|
+
# Create mock Jira issues
|
|
1413
|
+
todo_status = MagicMock()
|
|
1414
|
+
todo_status.name = "to do"
|
|
1415
|
+
|
|
1416
|
+
returned_jira_issues = [
|
|
1417
|
+
MagicMock(
|
|
1418
|
+
key="JIRA-2",
|
|
1419
|
+
fields=MagicMock(
|
|
1420
|
+
summary="Test Task",
|
|
1421
|
+
description="This is a test task",
|
|
1422
|
+
status=todo_status,
|
|
1423
|
+
duedate=None,
|
|
1424
|
+
statuscategorychangedate="2025-06-12T12:46:34.755961+0000",
|
|
1425
|
+
attachment=None,
|
|
1426
|
+
),
|
|
1427
|
+
)
|
|
1428
|
+
]
|
|
1429
|
+
|
|
1430
|
+
mock_create_issue_in_jira.side_effect = returned_jira_issues
|
|
1431
|
+
|
|
1432
|
+
new_regscale_objects = sync_regscale_to_jira(
|
|
1433
|
+
regscale_objects=regscale_objects,
|
|
1434
|
+
jira_client=MagicMock(),
|
|
1435
|
+
jira_project=self.JIRA_PROJECT,
|
|
1436
|
+
jira_issue_type="Task", # Using Task type for tasks
|
|
1437
|
+
sync_attachments=fetch_attachments,
|
|
1438
|
+
attachments={},
|
|
1439
|
+
api=MagicMock(),
|
|
1440
|
+
)
|
|
1441
|
+
|
|
1442
|
+
assert len(new_regscale_objects) == 1 # Only one new task should be created
|
|
1443
|
+
assert mock_create_issue_in_jira.call_count == 1 # Should only be called once for the task without Jira ID
|
|
1444
|
+
assert new_regscale_objects[0].otherIdentifier == "JIRA-2"
|
|
1445
|
+
|
|
1446
|
+
def test_generate_jira_comment(self):
|
|
1447
|
+
"""Test generating a Jira comment from a RegScale issue"""
|
|
1448
|
+
# Create issue with mix of included and excluded fields
|
|
1449
|
+
issue = Issue(
|
|
1450
|
+
id=1,
|
|
1451
|
+
title="Test Issue",
|
|
1452
|
+
status="Open",
|
|
1453
|
+
description="Test description",
|
|
1454
|
+
createdById="excluded-1",
|
|
1455
|
+
lastUpdatedById="excluded-2",
|
|
1456
|
+
issueOwnerId="excluded-3",
|
|
1457
|
+
assignedToId="excluded-4",
|
|
1458
|
+
uuid="excluded-5",
|
|
1459
|
+
jiraId="JIRA-123",
|
|
1460
|
+
severityLevel="High",
|
|
1461
|
+
dueDate="2023-12-31",
|
|
1462
|
+
)
|
|
1463
|
+
|
|
1464
|
+
comment = _generate_jira_comment(issue)
|
|
1465
|
+
|
|
1466
|
+
# Verify excluded fields are not in comment
|
|
1467
|
+
assert "createdById" not in comment
|
|
1468
|
+
assert "lastUpdatedById" not in comment
|
|
1469
|
+
assert "issueOwnerId" not in comment
|
|
1470
|
+
assert "assignedToId" not in comment
|
|
1471
|
+
assert "uuid" not in comment
|
|
1472
|
+
|
|
1473
|
+
# Verify included fields are in comment
|
|
1474
|
+
assert "**jiraId:** JIRA-123" in comment
|
|
1475
|
+
assert "**severityLevel:** High" in comment
|
|
1476
|
+
assert "**dueDate:** 2023-12-31" in comment
|
|
1477
|
+
assert "**title:** Test Issue" in comment
|
|
1478
|
+
assert "**status:** Open" in comment
|
|
1479
|
+
assert "**description:** Test description" in comment
|
|
1480
|
+
|
|
1481
|
+
def test_generate_jira_comment_task(self):
|
|
1482
|
+
"""Test generating a Jira comment from a RegScale task"""
|
|
1483
|
+
# Create task with mix of included and excluded fields
|
|
1484
|
+
task = Task(
|
|
1485
|
+
id=1,
|
|
1486
|
+
title="Test Task",
|
|
1487
|
+
status="Backlog",
|
|
1488
|
+
description="Test task description",
|
|
1489
|
+
createdById="excluded-1",
|
|
1490
|
+
lastUpdatedById="excluded-2",
|
|
1491
|
+
assignedToId="excluded-3",
|
|
1492
|
+
uuid="excluded-4",
|
|
1493
|
+
otherIdentifier="JIRA-123",
|
|
1494
|
+
percentComplete=50,
|
|
1495
|
+
dueDate="2023-12-31",
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
comment = _generate_jira_comment(task)
|
|
1499
|
+
|
|
1500
|
+
# Verify excluded fields are not in comment
|
|
1501
|
+
assert "createdById" not in comment
|
|
1502
|
+
assert "lastUpdatedById" not in comment
|
|
1503
|
+
assert "assignedToId" not in comment
|
|
1504
|
+
assert "uuid" not in comment
|
|
1505
|
+
|
|
1506
|
+
# Verify included fields are in comment
|
|
1507
|
+
assert "**otherIdentifier:** JIRA-123" in comment
|
|
1508
|
+
assert "**percentComplete:** 50" in comment
|
|
1509
|
+
assert "**dueDate:** 2023-12-31" in comment
|
|
1510
|
+
assert "**title:** Test Task" in comment
|
|
1511
|
+
assert "**status:** Backlog" in comment
|
|
1512
|
+
assert "**description:** Test task description" in comment
|
|
1513
|
+
|
|
1514
|
+
def test_create_issue_in_jira(self, regscale_issues_and_attachments, jira_client, fetch_attachments):
|
|
1515
|
+
"""Test creating an issue in Jira"""
|
|
1516
|
+
issues, attachments = regscale_issues_and_attachments
|
|
1517
|
+
for issue in issues:
|
|
1518
|
+
jira_issue = create_issue_in_jira(
|
|
1519
|
+
regscale_object=issue,
|
|
1520
|
+
jira_client=jira_client,
|
|
1521
|
+
jira_project=self.JIRA_PROJECT,
|
|
1522
|
+
issue_type="Bug",
|
|
1523
|
+
add_attachments=fetch_attachments,
|
|
1524
|
+
attachments=attachments,
|
|
1525
|
+
api=self.api,
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
assert jira_issue is not None
|
|
1529
|
+
assert jira_issue.key is not None
|
|
1530
|
+
assert jira_issue.fields.summary == issue.title
|
|
1531
|
+
assert issue.description in jira_issue.fields.description
|
|
1532
|
+
|
|
1533
|
+
jira_issue.delete() # cleanup issue in jira
|
|
1534
|
+
|
|
1535
|
+
def test_create_task_in_jira(self, regscale_tasks_and_attachments, jira_client, fetch_attachments):
|
|
1536
|
+
"""Test creating a task in Jira"""
|
|
1537
|
+
tasks, attachments = regscale_tasks_and_attachments
|
|
1538
|
+
for task in tasks:
|
|
1539
|
+
jira_task = create_issue_in_jira(
|
|
1540
|
+
regscale_object=task,
|
|
1541
|
+
jira_client=jira_client,
|
|
1542
|
+
jira_project=self.JIRA_PROJECT,
|
|
1543
|
+
issue_type="Task",
|
|
1544
|
+
add_attachments=fetch_attachments,
|
|
1545
|
+
attachments=attachments,
|
|
1546
|
+
api=self.api,
|
|
1547
|
+
)
|
|
1548
|
+
|
|
1549
|
+
assert jira_task is not None
|
|
1550
|
+
assert jira_task.key is not None
|
|
1551
|
+
assert jira_task.fields.summary == task.title
|
|
1552
|
+
assert task.description in jira_task.fields.description
|
|
1553
|
+
|
|
1554
|
+
jira_task.delete() # cleanup task in jira
|
|
1555
|
+
|
|
1556
|
+
def test_create_issue_in_jira_error(self):
|
|
1557
|
+
"""Test that we exit when Jira API call fails"""
|
|
1558
|
+
# Create a mock Jira client that will raise an error
|
|
1559
|
+
mock_jira_client = MagicMock()
|
|
1560
|
+
mock_jira_client.create_issue.side_effect = JIRAError("Test error")
|
|
1561
|
+
|
|
1562
|
+
mock_regscale = MagicMock()
|
|
1563
|
+
mock_regscale.get_module_string.return_value = "issues"
|
|
1564
|
+
mock_regscale.id = 1
|
|
1565
|
+
|
|
1566
|
+
mock_api = MagicMock()
|
|
1567
|
+
mock_api.config = {"domain": "https://test.regscale.com"}
|
|
1568
|
+
|
|
1569
|
+
with pytest.raises(SystemExit) as e:
|
|
1570
|
+
create_issue_in_jira(
|
|
1571
|
+
regscale_object=mock_regscale,
|
|
1572
|
+
jira_client=mock_jira_client,
|
|
1573
|
+
jira_project=self.JIRA_PROJECT,
|
|
1574
|
+
issue_type="Bug",
|
|
1575
|
+
add_attachments=True,
|
|
1576
|
+
attachments={},
|
|
1577
|
+
api=mock_api,
|
|
1578
|
+
)
|
|
1579
|
+
assert e.value.code == 1
|
|
1580
|
+
assert e.type == SystemExit
|
|
1581
|
+
|
|
1582
|
+
def test_upload_files_to_jira(self, jira_client):
|
|
1583
|
+
"""Test uploading files to Jira"""
|
|
1584
|
+
# create a jira issue to upload attachment to
|
|
1585
|
+
jira_issue = jira_client.create_issue(
|
|
1586
|
+
project=self.JIRA_PROJECT,
|
|
1587
|
+
summary="Test Issue",
|
|
1588
|
+
description="Test Description",
|
|
1589
|
+
issuetype={"name": "Bug"},
|
|
1590
|
+
)
|
|
1591
|
+
|
|
1592
|
+
# create regscale issue to link to
|
|
1593
|
+
reg_issue = Issue(
|
|
1594
|
+
id=1,
|
|
1595
|
+
title="Test Issue",
|
|
1596
|
+
description="Test Description",
|
|
1597
|
+
parentId=self.PARENT_ID,
|
|
1598
|
+
parentModule=self.PARENT_MODULE,
|
|
1599
|
+
)
|
|
1600
|
+
|
|
1601
|
+
# setup file hashes for upload
|
|
1602
|
+
file_path = os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt")
|
|
1603
|
+
with open(file_path, "rb") as file:
|
|
1604
|
+
reg_hashes = {compute_hash(file): file_path}
|
|
1605
|
+
jira_hashes = {}
|
|
1606
|
+
uploaded_attachments = []
|
|
1607
|
+
|
|
1608
|
+
upload_files_to_jira(
|
|
1609
|
+
jira_hashes,
|
|
1610
|
+
reg_hashes,
|
|
1611
|
+
jira_issue,
|
|
1612
|
+
reg_issue,
|
|
1613
|
+
jira_client,
|
|
1614
|
+
uploaded_attachments,
|
|
1615
|
+
)
|
|
1616
|
+
assert uploaded_attachments == [file_path]
|
|
1617
|
+
check_issue = jira_client.issue(jira_issue.key)
|
|
1618
|
+
assert len(check_issue.fields.attachment) == 1
|
|
1619
|
+
assert check_issue.fields.attachment[0].size > 0
|
|
1620
|
+
assert check_issue.fields.attachment[0].created is not None
|
|
1621
|
+
|
|
1622
|
+
# Clean up
|
|
1623
|
+
jira_issue.delete()
|
|
1624
|
+
|
|
1625
|
+
def test_upload_files_to_jira_duplicates(self):
|
|
1626
|
+
"""Test that we don't upload duplicates"""
|
|
1627
|
+
jira_hashes = {"dummyhash": "dummy/path/file.txt"}
|
|
1628
|
+
reg_hashes = {"dummyhash": "dummy/path/file.txt"}
|
|
1629
|
+
jira_issue = MagicMock()
|
|
1630
|
+
reg_issue = MagicMock()
|
|
1631
|
+
jira_client = MagicMock()
|
|
1632
|
+
uploaded_attachments = []
|
|
1633
|
+
|
|
1634
|
+
upload_files_to_jira(jira_hashes, reg_hashes, jira_issue, reg_issue, jira_client, uploaded_attachments)
|
|
1635
|
+
|
|
1636
|
+
assert uploaded_attachments == []
|
|
1637
|
+
jira_client.add_attachment.assert_not_called()
|
|
1638
|
+
|
|
1639
|
+
@pytest.mark.parametrize("error_type", [JIRAError, TypeError])
|
|
1640
|
+
@patch(f"{PATH}.open")
|
|
1641
|
+
def test_upload_files_to_jira_error(self, mock_open, error_type):
|
|
1642
|
+
"""Test that uploads aren't made when errors are encountered"""
|
|
1643
|
+
# Setup mock file
|
|
1644
|
+
mock_file = MagicMock()
|
|
1645
|
+
mock_file.read.return_value = b"test content"
|
|
1646
|
+
mock_open.return_value.__enter__.return_value = mock_file
|
|
1647
|
+
|
|
1648
|
+
# Setup test data
|
|
1649
|
+
file_path = "dummy/path/file.txt"
|
|
1650
|
+
reg_hashes = {"dummyhash": file_path}
|
|
1651
|
+
jira_hashes = {}
|
|
1652
|
+
jira_issue = MagicMock()
|
|
1653
|
+
reg_issue = MagicMock()
|
|
1654
|
+
jira_client = MagicMock()
|
|
1655
|
+
jira_client.add_attachment.side_effect = error_type("Test error")
|
|
1656
|
+
uploaded_attachments = []
|
|
1657
|
+
|
|
1658
|
+
upload_files_to_jira(jira_hashes, reg_hashes, jira_issue, reg_issue, jira_client, uploaded_attachments)
|
|
1659
|
+
|
|
1660
|
+
assert uploaded_attachments == []
|
|
1661
|
+
jira_client.add_attachment.assert_called_once()
|
|
1662
|
+
mock_file.read.assert_called_once()
|
|
1663
|
+
|
|
1664
|
+
def test_upload_files_to_regscale(self):
|
|
1665
|
+
"""Test uploading files to RegScale"""
|
|
1666
|
+
tmp = Issue(
|
|
1667
|
+
id=1,
|
|
1668
|
+
title="Test Issue",
|
|
1669
|
+
description="Test Description",
|
|
1670
|
+
parentId=self.PARENT_ID,
|
|
1671
|
+
parentModule=self.PARENT_MODULE,
|
|
1672
|
+
dueDate=get_current_datetime(),
|
|
1673
|
+
status="Open",
|
|
1674
|
+
)
|
|
1675
|
+
reg_issue = tmp.create()
|
|
1676
|
+
|
|
1677
|
+
file_path = os.path.join(self.get_tests_dir("tests"), "test_data", "jira_attachments", "attachment.txt")
|
|
1678
|
+
with open(file_path, "rb") as file:
|
|
1679
|
+
jira_hashes = {compute_hash(file): file_path}
|
|
1680
|
+
reg_hashes = {}
|
|
1681
|
+
uploaded_attachments = []
|
|
1682
|
+
|
|
1683
|
+
upload_files_to_regscale(jira_hashes, reg_hashes, reg_issue, self.api, uploaded_attachments)
|
|
1684
|
+
|
|
1685
|
+
assert uploaded_attachments == [file_path]
|
|
1686
|
+
check_issues, attachments = Issue.get_objects_and_attachments_by_parent(
|
|
1687
|
+
parent_id=self.PARENT_ID, parent_module=self.PARENT_MODULE
|
|
1688
|
+
)
|
|
1689
|
+
assert reg_issue in check_issues
|
|
1690
|
+
assert len(attachments[reg_issue.id]) == 1
|
|
1691
|
+
|
|
1692
|
+
@patch(f"{PATH}.File.upload_file_to_regscale", return_value=None)
|
|
1693
|
+
def test_upload_files_to_regscale_duplicates(self, mock_upload_file_to_regscale):
|
|
1694
|
+
"""Test that we don't upload duplicate attachments to regscale"""
|
|
1695
|
+
jira_hashes = {"dummyhash": "dummy/path/file.txt"}
|
|
1696
|
+
reg_hashes = {"dummyhash": "dummy/path/file.txt"}
|
|
1697
|
+
reg_issue = MagicMock()
|
|
1698
|
+
uploaded_attachments = []
|
|
1699
|
+
|
|
1700
|
+
upload_files_to_regscale(jira_hashes, reg_hashes, reg_issue, MagicMock(), uploaded_attachments)
|
|
1701
|
+
|
|
1702
|
+
assert uploaded_attachments == []
|
|
1703
|
+
mock_upload_file_to_regscale.assert_not_called()
|
|
1704
|
+
|
|
1705
|
+
@patch(f"{PATH}.File.upload_file_to_regscale", return_value=None)
|
|
1706
|
+
@patch(f"{PATH}.open")
|
|
1707
|
+
def test_upload_files_to_regscale_error(self, mock_open, mock_upload_file_to_regscale):
|
|
1708
|
+
"""Test when the uploads are unsuccessful"""
|
|
1709
|
+
mock_file = MagicMock()
|
|
1710
|
+
mock_file.read.return_value = b"test content"
|
|
1711
|
+
mock_open.return_value.__enter__.return_value = mock_file
|
|
1712
|
+
|
|
1713
|
+
# Setup test data
|
|
1714
|
+
file_path = "dummy/path/file.txt"
|
|
1715
|
+
jira_hashes = {"dummyhash": file_path}
|
|
1716
|
+
reg_hashes = {}
|
|
1717
|
+
reg_issue = MagicMock()
|
|
1718
|
+
uploaded_attachments = []
|
|
1719
|
+
api = MagicMock()
|
|
1720
|
+
|
|
1721
|
+
upload_files_to_regscale(jira_hashes, reg_hashes, reg_issue, api, uploaded_attachments)
|
|
1722
|
+
|
|
1723
|
+
assert uploaded_attachments == []
|
|
1724
|
+
mock_upload_file_to_regscale.assert_called_once()
|
|
1725
|
+
|
|
1726
|
+
def test_validate_issue_type(self):
|
|
1727
|
+
"""Test validating the issue type"""
|
|
1728
|
+
jira_client = MagicMock()
|
|
1729
|
+
issue_type_bug = MagicMock()
|
|
1730
|
+
issue_type_bug.name = "Bug"
|
|
1731
|
+
issue_type_task = MagicMock()
|
|
1732
|
+
issue_type_task.name = "Task"
|
|
1733
|
+
jira_client.issue_types.return_value = [issue_type_bug, issue_type_task]
|
|
1734
|
+
assert validate_issue_type(jira_client, "Bug") is True
|
|
1735
|
+
assert validate_issue_type(jira_client, "Task") is True
|
|
1736
|
+
with pytest.raises(SystemExit) as e:
|
|
1737
|
+
validate_issue_type(jira_client, "Invalid")
|
|
1738
|
+
assert e.value.code == 1
|
|
1739
|
+
assert e.type == SystemExit
|
|
1740
|
+
|
|
1741
|
+
def test_download_issue_attachments(self, regscale_issues_and_attachments, get_jira_issue_with_attachment):
|
|
1742
|
+
"""Test downloading attachments from Jira and RegScale issues"""
|
|
1743
|
+
issues, attachments = regscale_issues_and_attachments
|
|
1744
|
+
jira_issue = get_jira_issue_with_attachment
|
|
1745
|
+
if attachments:
|
|
1746
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1747
|
+
download_regscale_attachments_to_directory(
|
|
1748
|
+
directory=tmpdir,
|
|
1749
|
+
jira_issue=jira_issue,
|
|
1750
|
+
regscale_object=issues[0],
|
|
1751
|
+
api=self.api,
|
|
1752
|
+
)
|
|
1753
|
+
assert os.path.exists(tmpdir) is True
|
|
1754
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1755
|
+
download_regscale_attachments_to_directory(
|
|
1756
|
+
directory=tmpdir,
|
|
1757
|
+
jira_issue=jira_issue,
|
|
1758
|
+
regscale_object=issues[0],
|
|
1759
|
+
api=self.api,
|
|
1760
|
+
)
|
|
1761
|
+
assert os.path.exists(tmpdir) is True
|
|
1762
|
+
|
|
1763
|
+
def test_download_task_attachments(self, regscale_tasks_and_attachments, get_jira_task_with_attachment):
|
|
1764
|
+
"""Test downloading attachments from Jira and RegScale tasks"""
|
|
1765
|
+
tasks, attachments = regscale_tasks_and_attachments
|
|
1766
|
+
jira_task = get_jira_task_with_attachment
|
|
1767
|
+
if attachments:
|
|
1768
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1769
|
+
download_regscale_attachments_to_directory(
|
|
1770
|
+
directory=tmpdir,
|
|
1771
|
+
jira_issue=jira_task,
|
|
1772
|
+
regscale_object=tasks[0],
|
|
1773
|
+
api=self.api,
|
|
1774
|
+
)
|
|
1775
|
+
assert os.path.exists(tmpdir) is True
|
|
1776
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1777
|
+
download_regscale_attachments_to_directory(
|
|
1778
|
+
directory=tmpdir,
|
|
1779
|
+
jira_issue=jira_task,
|
|
1780
|
+
regscale_object=tasks[0],
|
|
1781
|
+
api=self.api,
|
|
1782
|
+
)
|
|
1783
|
+
assert os.path.exists(tmpdir) is True
|
|
1784
|
+
|
|
1785
|
+
@patch(f"{PATH}.job_progress", return_value=MagicMock(spec=Progress))
|
|
1786
|
+
def test_update_regscale_issue(self, mock_job_progress_object, regscale_issue_and_attachment):
|
|
1787
|
+
"""Test updating an issue in RegScale"""
|
|
1788
|
+
test_issue = regscale_issue_and_attachment
|
|
1789
|
+
# update fields in a RegScale issue
|
|
1790
|
+
test_issue.assetIdentifier = f"Updated via Test on {get_current_datetime()}"
|
|
1791
|
+
test_issue.lastUpdatedById = self.config["userId"]
|
|
1792
|
+
with mock_job_progress_object as job_progress:
|
|
1793
|
+
test_task = job_progress.add_task(
|
|
1794
|
+
description="Updating 1 issue(s) in RegScale...",
|
|
1795
|
+
total=len([test_issue]),
|
|
1796
|
+
visible=False,
|
|
1797
|
+
)
|
|
1798
|
+
update_regscale_issues(
|
|
1799
|
+
args=([test_issue], test_task),
|
|
1800
|
+
thread=0,
|
|
1801
|
+
)
|
|
1802
|
+
# make sure the task was marked complete
|
|
1803
|
+
assert test_task.finished
|
|
1804
|
+
# make sure the issue was updated
|
|
1805
|
+
updated_issue = Issue.fetch_issue_by_id(app=self.app, issue_id=test_issue.id)
|
|
1806
|
+
assert updated_issue.assetIdentifier == test_issue.assetIdentifier
|
|
1807
|
+
assert updated_issue.lastUpdatedById == test_issue.lastUpdatedById
|
|
1808
|
+
|
|
1809
|
+
@staticmethod
|
|
1810
|
+
def teardown_class(cls):
|
|
1811
|
+
"""Remove test data"""
|
|
1812
|
+
with contextlib.suppress(FileNotFoundError):
|
|
1813
|
+
shutil.rmtree("./artifacts")
|
|
1814
|
+
assert not os.path.exists("./artifacts")
|