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
@@ -1,7 +1,12 @@
1
+ import logging
2
+ from collections import defaultdict
3
+ from typing import Dict, List
4
+ from unittest.mock import MagicMock, patch
5
+
1
6
  import pytest
2
7
 
3
8
  from regscale.core.app.utils.app_utils import get_current_datetime
4
- from regscale.models.regscale_models.issue import Issue, IssueSeverity, IssueStatus
9
+ from regscale.models.regscale_models.issue import Issue, IssueSeverity, IssueStatus, OpenIssueDict
5
10
 
6
11
 
7
12
  def test_bad_issue_instance():
@@ -34,3 +39,375 @@ def test_good_issue_instance():
34
39
  dateCreated=get_current_datetime(),
35
40
  )
36
41
  assert isinstance(issue, Issue)
42
+
43
+
44
+ class TestOpenIssuesRefactoredMethods:
45
+ """Test suite for refactored get_open_issues_ids_by_implementation_id methods"""
46
+
47
+ @pytest.fixture
48
+ def mock_logger(self):
49
+ """Create a mock logger"""
50
+ return MagicMock(spec=logging.Logger)
51
+
52
+ @pytest.fixture
53
+ def sample_open_issues(self) -> Dict[int, List[OpenIssueDict]]:
54
+ """Create sample open issues data"""
55
+ return {
56
+ 1: [
57
+ OpenIssueDict(id=101, otherIdentifier="ISS-101", integrationFindingId="FIND-101"),
58
+ OpenIssueDict(id=102, otherIdentifier="ISS-102", integrationFindingId="FIND-102"),
59
+ ],
60
+ 2: [
61
+ OpenIssueDict(id=103, otherIdentifier="ISS-103", integrationFindingId="FIND-103"),
62
+ ],
63
+ }
64
+
65
+ @pytest.fixture
66
+ def mock_api_response_single_control(self):
67
+ """Mock API response for single control support"""
68
+ return {
69
+ "issues": {
70
+ "items": [
71
+ {"id": 101, "controlId": 1, "otherIdentifier": "ISS-101", "integrationFindingId": "FIND-101"},
72
+ {"id": 102, "controlId": 1, "otherIdentifier": "ISS-102", "integrationFindingId": "FIND-102"},
73
+ {"id": 103, "controlId": 2, "otherIdentifier": "ISS-103", "integrationFindingId": "FIND-103"},
74
+ ],
75
+ "pageInfo": {"hasNextPage": False},
76
+ "totalCount": 3,
77
+ }
78
+ }
79
+
80
+ @pytest.fixture
81
+ def mock_api_response_multiple_controls(self):
82
+ """Mock API response for multiple control support"""
83
+ return {
84
+ "issues": {
85
+ "items": [
86
+ {
87
+ "id": 101,
88
+ "otherIdentifier": "ISS-101",
89
+ "integrationFindingId": "FIND-101",
90
+ "controlImplementations": [{"id": 1}, {"id": 2}],
91
+ },
92
+ {
93
+ "id": 102,
94
+ "otherIdentifier": "ISS-102",
95
+ "integrationFindingId": "FIND-102",
96
+ "controlImplementations": [{"id": 1}],
97
+ },
98
+ ],
99
+ "pageInfo": {"hasNextPage": False},
100
+ "totalCount": 2,
101
+ }
102
+ }
103
+
104
+ def test_check_cache_with_cache_disabled(self, mock_logger):
105
+ """Test _check_cache when cache is disabled"""
106
+ with patch.object(Issue, "_is_cache_disabled", return_value=True):
107
+ result = Issue._check_cache(123, mock_logger)
108
+ assert result is None
109
+ mock_logger.info.assert_not_called()
110
+
111
+ def test_check_cache_with_valid_cache(self, mock_logger, sample_open_issues):
112
+ """Test _check_cache when valid cache exists"""
113
+ with patch.object(Issue, "_is_cache_disabled", return_value=False), patch.object(
114
+ Issue, "_get_from_cache", return_value=sample_open_issues
115
+ ):
116
+ result = Issue._check_cache(123, mock_logger)
117
+ assert result == sample_open_issues
118
+ mock_logger.info.assert_called_once()
119
+
120
+ def test_check_cache_with_no_cache(self, mock_logger):
121
+ """Test _check_cache when no cache exists"""
122
+ with patch.object(Issue, "_is_cache_disabled", return_value=False), patch.object(
123
+ Issue, "_get_from_cache", return_value=None
124
+ ):
125
+ result = Issue._check_cache(123, mock_logger)
126
+ assert result is None
127
+
128
+ def test_get_query_fields_with_multiple_controls(self):
129
+ """Test _get_query_fields for multiple control support"""
130
+ result = Issue._get_query_fields(supports_multiple_controls=True)
131
+ assert "controlImplementations" in result
132
+ assert "id" in result
133
+ assert "otherIdentifier" in result
134
+ assert "integrationFindingId" in result
135
+
136
+ def test_get_query_fields_with_single_control(self):
137
+ """Test _get_query_fields for single control support"""
138
+ result = Issue._get_query_fields(supports_multiple_controls=False)
139
+ assert "controlId" in result
140
+ assert "id" in result
141
+ assert "otherIdentifier" in result
142
+ assert "integrationFindingId" in result
143
+ assert "controlImplementations" not in result
144
+
145
+ def test_build_query_for_security_plan(self):
146
+ """Test _build_query for security plan"""
147
+ with patch.object(Issue, "get_module_string", return_value="issues"):
148
+ query = Issue._build_query(plan_id=123, is_component=False, skip=0, take=50, fields="id, title")
149
+ assert "securityPlanId" in query
150
+ assert "eq: 123" in query
151
+ assert 'status: {eq: "Open"}' in query
152
+ assert "componentId" not in query
153
+
154
+ def test_build_query_for_component(self):
155
+ """Test _build_query for component"""
156
+ with patch.object(Issue, "get_module_string", return_value="issues"):
157
+ query = Issue._build_query(plan_id=456, is_component=True, skip=10, take=25, fields="id, title")
158
+ assert "componentId" in query
159
+ assert "eq: 456" in query
160
+ assert 'status: {eq: "Open"}' in query
161
+ assert "securityPlanId" not in query
162
+
163
+ def test_log_progress_with_large_dataset(self, mock_logger):
164
+ """Test _log_progress logs for large datasets"""
165
+ Issue._log_progress(skip=100, take=50, items_count=50, total_count=2000, logger=mock_logger)
166
+ mock_logger.info.assert_called_once()
167
+ assert "Processing batch 3" in mock_logger.info.call_args[0][0]
168
+
169
+ def test_log_progress_with_small_dataset(self, mock_logger):
170
+ """Test _log_progress does not log for small datasets"""
171
+ Issue._log_progress(skip=0, take=50, items_count=50, total_count=100, logger=mock_logger)
172
+ mock_logger.info.assert_not_called()
173
+
174
+ def test_add_issue_to_single_control(self):
175
+ """Test _add_issue_to_single_control"""
176
+ control_issues = defaultdict(list)
177
+ issue_dict = OpenIssueDict(id=101, otherIdentifier="ISS-101", integrationFindingId="FIND-101")
178
+ item = {"id": 101, "controlId": 5}
179
+
180
+ Issue._add_issue_to_single_control(item, issue_dict, control_issues)
181
+
182
+ assert len(control_issues) == 1
183
+ assert 5 in control_issues
184
+ assert control_issues[5][0] == issue_dict
185
+
186
+ def test_add_issue_to_single_control_no_control_id(self):
187
+ """Test _add_issue_to_single_control when no controlId"""
188
+ control_issues = defaultdict(list)
189
+ issue_dict = OpenIssueDict(id=101, otherIdentifier="ISS-101", integrationFindingId="FIND-101")
190
+ item = {"id": 101}
191
+
192
+ Issue._add_issue_to_single_control(item, issue_dict, control_issues)
193
+
194
+ assert len(control_issues) == 0
195
+
196
+ def test_add_issue_to_multiple_controls(self):
197
+ """Test _add_issue_to_multiple_controls"""
198
+ control_issues = defaultdict(list)
199
+ issue_dict = OpenIssueDict(id=101, otherIdentifier="ISS-101", integrationFindingId="FIND-101")
200
+ item = {"id": 101, "controlImplementations": [{"id": 1}, {"id": 2}, {"id": 3}]}
201
+
202
+ Issue._add_issue_to_multiple_controls(item, issue_dict, control_issues)
203
+
204
+ assert len(control_issues) == 3
205
+ assert 1 in control_issues
206
+ assert 2 in control_issues
207
+ assert 3 in control_issues
208
+ assert control_issues[1][0] == issue_dict
209
+
210
+ def test_add_issue_to_multiple_controls_no_implementations(self):
211
+ """Test _add_issue_to_multiple_controls when no implementations"""
212
+ control_issues = defaultdict(list)
213
+ issue_dict = OpenIssueDict(id=101, otherIdentifier="ISS-101", integrationFindingId="FIND-101")
214
+ item = {"id": 101}
215
+
216
+ Issue._add_issue_to_multiple_controls(item, issue_dict, control_issues)
217
+
218
+ assert len(control_issues) == 0
219
+
220
+ def test_process_issue_items_single_control(self):
221
+ """Test _process_issue_items with single control support"""
222
+ items = [
223
+ {"id": 101, "controlId": 1, "otherIdentifier": "ISS-101", "integrationFindingId": "FIND-101"},
224
+ {"id": 102, "controlId": 2, "otherIdentifier": "ISS-102", "integrationFindingId": "FIND-102"},
225
+ ]
226
+ control_issues = defaultdict(list)
227
+
228
+ Issue._process_issue_items(items, supports_multiple_controls=False, control_issues=control_issues)
229
+
230
+ assert len(control_issues) == 2
231
+ assert len(control_issues[1]) == 1
232
+ assert len(control_issues[2]) == 1
233
+ assert control_issues[1][0]["id"] == 101
234
+ assert control_issues[2][0]["id"] == 102
235
+
236
+ def test_process_issue_items_multiple_controls(self):
237
+ """Test _process_issue_items with multiple control support"""
238
+ items = [
239
+ {
240
+ "id": 101,
241
+ "otherIdentifier": "ISS-101",
242
+ "integrationFindingId": "FIND-101",
243
+ "controlImplementations": [{"id": 1}, {"id": 2}],
244
+ },
245
+ {
246
+ "id": 102,
247
+ "otherIdentifier": "ISS-102",
248
+ "integrationFindingId": "FIND-102",
249
+ "controlImplementations": [{"id": 1}],
250
+ },
251
+ ]
252
+ control_issues = defaultdict(list)
253
+
254
+ Issue._process_issue_items(items, supports_multiple_controls=True, control_issues=control_issues)
255
+
256
+ assert len(control_issues) == 2
257
+ assert len(control_issues[1]) == 2
258
+ assert len(control_issues[2]) == 1
259
+ assert control_issues[1][0]["id"] == 101
260
+ assert control_issues[1][1]["id"] == 102
261
+
262
+ def test_log_completion(self, mock_logger):
263
+ """Test _log_completion logs completion message"""
264
+ import time
265
+
266
+ start_time = time.time() - 5.5 # 5.5 seconds ago
267
+
268
+ Issue._log_completion(
269
+ plan_id=123, total_fetched=150, control_count=25, start_time=start_time, logger=mock_logger
270
+ )
271
+
272
+ mock_logger.info.assert_called_once()
273
+ log_message = mock_logger.info.call_args[0][0]
274
+ assert "150 open issue(s)" in log_message
275
+ assert "25 control(s)" in log_message
276
+ assert "123" in log_message
277
+ assert "5.5" in log_message
278
+
279
+ @patch.object(Issue, "_get_api_handler")
280
+ @patch.object(Issue, "get_module_string", return_value="issues")
281
+ @patch.object(Issue, "is_multiple_controls_supported", return_value=False)
282
+ def test_paginate_and_process_issues_single_page(
283
+ self, mock_supports, mock_module_string, mock_api_handler, mock_logger
284
+ ):
285
+ """Test _paginate_and_process_issues with single page response"""
286
+ mock_api = MagicMock()
287
+ mock_api.graph.return_value = {
288
+ "issues": {
289
+ "items": [
290
+ {"id": 101, "controlId": 1, "otherIdentifier": "ISS-101", "integrationFindingId": "FIND-101"},
291
+ ],
292
+ "pageInfo": {"hasNextPage": False},
293
+ "totalCount": 1,
294
+ }
295
+ }
296
+ mock_api_handler.return_value = mock_api
297
+ control_issues = defaultdict(list)
298
+
299
+ total_fetched = Issue._paginate_and_process_issues(
300
+ plan_id=123, is_component=False, control_issues=control_issues, logger=mock_logger
301
+ )
302
+
303
+ assert total_fetched == 1
304
+ assert len(control_issues) == 1
305
+ assert len(control_issues[1]) == 1
306
+
307
+ @patch.object(Issue, "_get_api_handler")
308
+ @patch.object(Issue, "get_module_string", return_value="issues")
309
+ @patch.object(Issue, "is_multiple_controls_supported", return_value=False)
310
+ def test_paginate_and_process_issues_multiple_pages(
311
+ self, mock_supports, mock_module_string, mock_api_handler, mock_logger
312
+ ):
313
+ """Test _paginate_and_process_issues with multiple pages"""
314
+ mock_api = MagicMock()
315
+ # Simulate two pages
316
+ mock_api.graph.side_effect = [
317
+ {
318
+ "issues": {
319
+ "items": [
320
+ {"id": 101, "controlId": 1, "otherIdentifier": "ISS-101", "integrationFindingId": "FIND-101"},
321
+ ],
322
+ "pageInfo": {"hasNextPage": True},
323
+ "totalCount": 2,
324
+ }
325
+ },
326
+ {
327
+ "issues": {
328
+ "items": [
329
+ {"id": 102, "controlId": 2, "otherIdentifier": "ISS-102", "integrationFindingId": "FIND-102"},
330
+ ],
331
+ "pageInfo": {"hasNextPage": False},
332
+ "totalCount": 2,
333
+ }
334
+ },
335
+ ]
336
+ mock_api_handler.return_value = mock_api
337
+ control_issues = defaultdict(list)
338
+
339
+ total_fetched = Issue._paginate_and_process_issues(
340
+ plan_id=123, is_component=False, control_issues=control_issues, logger=mock_logger
341
+ )
342
+
343
+ assert total_fetched == 2
344
+ assert len(control_issues) == 2
345
+ assert mock_api.graph.call_count == 2
346
+
347
+ @patch.object(Issue, "_paginate_and_process_issues")
348
+ def test_fetch_open_issues_from_api_success(self, mock_paginate, mock_logger):
349
+ """Test _fetch_open_issues_from_api successful execution"""
350
+ mock_paginate.return_value = 150
351
+ control_issues = Issue._fetch_open_issues_from_api(plan_id=123, is_component=False, logger=mock_logger)
352
+
353
+ assert isinstance(control_issues, defaultdict)
354
+ mock_logger.info.assert_called()
355
+
356
+ @patch.object(Issue, "_paginate_and_process_issues")
357
+ def test_fetch_open_issues_from_api_exception(self, mock_paginate, mock_logger):
358
+ """Test _fetch_open_issues_from_api handles exceptions"""
359
+ mock_paginate.side_effect = Exception("API Error")
360
+
361
+ control_issues = Issue._fetch_open_issues_from_api(plan_id=123, is_component=False, logger=mock_logger)
362
+
363
+ assert isinstance(control_issues, defaultdict)
364
+ assert len(control_issues) == 0
365
+ mock_logger.error.assert_called_once()
366
+
367
+ @patch.object(Issue, "_check_cache")
368
+ @patch.object(Issue, "_fetch_open_issues_from_api")
369
+ @patch.object(Issue, "_is_cache_disabled", return_value=False)
370
+ @patch.object(Issue, "_cache_data")
371
+ def test_get_open_issues_ids_by_implementation_id_with_cache(
372
+ self, mock_cache_data, mock_disabled, mock_fetch, mock_check_cache, sample_open_issues
373
+ ):
374
+ """Test get_open_issues_ids_by_implementation_id returns cached data"""
375
+ mock_check_cache.return_value = sample_open_issues
376
+
377
+ result = Issue.get_open_issues_ids_by_implementation_id(plan_id=123)
378
+
379
+ assert result == sample_open_issues
380
+ mock_fetch.assert_not_called()
381
+ mock_cache_data.assert_not_called()
382
+
383
+ @patch.object(Issue, "_check_cache", return_value=None)
384
+ @patch.object(Issue, "_fetch_open_issues_from_api")
385
+ @patch.object(Issue, "_is_cache_disabled", return_value=False)
386
+ @patch.object(Issue, "_cache_data")
387
+ def test_get_open_issues_ids_by_implementation_id_without_cache(
388
+ self, mock_cache_data, mock_disabled, mock_fetch, mock_check_cache, sample_open_issues
389
+ ):
390
+ """Test get_open_issues_ids_by_implementation_id fetches and caches data"""
391
+ mock_fetch.return_value = sample_open_issues
392
+
393
+ result = Issue.get_open_issues_ids_by_implementation_id(plan_id=123)
394
+
395
+ assert result == sample_open_issues
396
+ mock_fetch.assert_called_once()
397
+ mock_cache_data.assert_called_once_with(123, sample_open_issues)
398
+
399
+ @patch.object(Issue, "_check_cache", return_value=None)
400
+ @patch.object(Issue, "_fetch_open_issues_from_api")
401
+ @patch.object(Issue, "_is_cache_disabled", return_value=True)
402
+ @patch.object(Issue, "_cache_data")
403
+ def test_get_open_issues_ids_by_implementation_id_cache_disabled(
404
+ self, mock_cache_data, mock_disabled, mock_fetch, mock_check_cache, sample_open_issues
405
+ ):
406
+ """Test get_open_issues_ids_by_implementation_id with cache disabled"""
407
+ mock_fetch.return_value = sample_open_issues
408
+
409
+ result = Issue.get_open_issues_ids_by_implementation_id(plan_id=123)
410
+
411
+ assert result == sample_open_issues
412
+ mock_fetch.assert_called_once()
413
+ mock_cache_data.assert_not_called()