regscale-cli 6.26.0.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 (95) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -1
  3. regscale/core/app/internal/evidence.py +419 -2
  4. regscale/dev/code_gen.py +24 -20
  5. regscale/integrations/commercial/jira.py +367 -126
  6. regscale/integrations/commercial/qualys/__init__.py +7 -8
  7. regscale/integrations/commercial/qualys/scanner.py +8 -3
  8. regscale/integrations/commercial/synqly/assets.py +17 -0
  9. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  10. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  11. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  12. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  13. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  14. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  15. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  16. regscale/integrations/commercial/wizv2/click.py +44 -59
  17. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  18. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  19. regscale/integrations/commercial/wizv2/compliance_report.py +10 -9
  20. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  21. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  22. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  23. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  24. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  25. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  26. regscale/integrations/commercial/wizv2/issue.py +1 -1
  27. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  28. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  29. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  30. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  31. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  32. regscale/integrations/commercial/wizv2/reports.py +1 -1
  33. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  34. regscale/integrations/commercial/wizv2/scanner.py +40 -100
  35. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  36. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  37. regscale/integrations/commercial/wizv2/variables.py +89 -3
  38. regscale/integrations/compliance_integration.py +0 -46
  39. regscale/integrations/control_matcher.py +22 -3
  40. regscale/integrations/due_date_handler.py +14 -8
  41. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  42. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  43. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  44. regscale/integrations/scanner_integration.py +127 -57
  45. regscale/models/integration_models/cisa_kev_data.json +132 -9
  46. regscale/models/integration_models/qualys.py +3 -4
  47. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  48. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  49. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  50. regscale/models/regscale_models/control_implementation.py +1 -1
  51. regscale/models/regscale_models/issue.py +0 -1
  52. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  53. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +93 -60
  54. tests/regscale/integrations/commercial/test_jira.py +481 -91
  55. tests/regscale/integrations/commercial/test_wiz.py +96 -200
  56. tests/regscale/integrations/commercial/wizv2/__init__.py +1 -1
  57. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  58. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  59. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  60. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  61. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  62. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  63. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  64. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  65. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  66. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  67. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  68. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  69. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  70. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  71. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  72. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  73. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  74. tests/regscale/integrations/commercial/wizv2/test_issue.py +1 -1
  75. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  76. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  77. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  78. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  79. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +1 -1
  80. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +72 -29
  81. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  82. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  83. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +946 -78
  84. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +97 -202
  85. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  86. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  87. tests/regscale/integrations/public/test_fedramp.py +301 -0
  88. tests/regscale/integrations/test_control_matcher.py +83 -0
  89. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  90. tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +0 -750
  91. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  92. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  93. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  94. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  95. {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,537 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Comprehensive unit tests for WizDataMixin module.
5
+
6
+ Tests cover:
7
+ - fetch_data_if_needed with various file states (exists/not exists, fresh/stale)
8
+ - write_to_file functionality
9
+ - load_file functionality
10
+ - fetch_data with GraphQL client mocking
11
+ - Error handling and edge cases
12
+ """
13
+
14
+ import json
15
+ import os
16
+ import tempfile
17
+ from datetime import datetime, timedelta
18
+ from unittest.mock import MagicMock, patch, mock_open
19
+
20
+ import pytest
21
+
22
+ from regscale.integrations.commercial.wizv2.WizDataMixin import WizMixin
23
+
24
+
25
+ class TestWizMixinFetchDataIfNeeded:
26
+ """Test fetch_data_if_needed method with various scenarios."""
27
+
28
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.load_file")
29
+ @patch("os.path.exists")
30
+ @patch("os.path.getmtime")
31
+ def test_fetch_data_if_needed_uses_fresh_cache(self, mock_getmtime, mock_exists, mock_load_file):
32
+ """Test that fetch_data_if_needed uses cache when file is fresh."""
33
+ # Setup
34
+ wiz = WizMixin()
35
+ file_path = "artifacts/test_data.json"
36
+ cached_data = [{"id": "1", "name": "cached"}]
37
+
38
+ # Mock file exists and is fresh (modified 1 hour ago, interval is 2 hours)
39
+ mock_exists.return_value = True
40
+ current_time = datetime.now()
41
+ file_mod_time = current_time - timedelta(hours=1)
42
+ mock_getmtime.return_value = file_mod_time.timestamp()
43
+ mock_load_file.return_value = cached_data
44
+
45
+ # Execute
46
+ result = wiz.fetch_data_if_needed(
47
+ file_path=file_path, query="query {}", topic_key="test", interval_hours=2, variables=None
48
+ )
49
+
50
+ # Verify
51
+ assert result == cached_data
52
+ mock_load_file.assert_called_once_with(file_path)
53
+
54
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.write_to_file")
55
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.fetch_data")
56
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.load_file")
57
+ @patch("os.path.exists")
58
+ @patch("os.path.getmtime")
59
+ def test_fetch_data_if_needed_fetches_when_cache_stale(
60
+ self, mock_getmtime, mock_exists, mock_load_file, mock_fetch_data, mock_write_to_file
61
+ ):
62
+ """Test that fetch_data_if_needed fetches new data when cache is stale."""
63
+ # Setup
64
+ wiz = WizMixin()
65
+ file_path = "artifacts/test_data.json"
66
+ fresh_data = [{"id": "2", "name": "fresh"}]
67
+
68
+ # Mock file exists but is stale (modified 3 hours ago, interval is 2 hours)
69
+ mock_exists.return_value = True
70
+ current_time = datetime.now()
71
+ file_mod_time = current_time - timedelta(hours=3)
72
+ mock_getmtime.return_value = file_mod_time.timestamp()
73
+ mock_fetch_data.return_value = fresh_data
74
+
75
+ # Execute
76
+ result = wiz.fetch_data_if_needed(
77
+ file_path=file_path, query="query {}", topic_key="test", interval_hours=2, variables=None
78
+ )
79
+
80
+ # Verify
81
+ assert result == fresh_data
82
+ mock_fetch_data.assert_called_once_with("query {}", "test", None)
83
+ mock_write_to_file.assert_called_once_with(file_path, fresh_data)
84
+ # Should not load the stale file
85
+ mock_load_file.assert_not_called()
86
+
87
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.write_to_file")
88
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.fetch_data")
89
+ @patch("os.path.exists")
90
+ def test_fetch_data_if_needed_fetches_when_no_cache(self, mock_exists, mock_fetch_data, mock_write_to_file):
91
+ """Test that fetch_data_if_needed fetches data when cache doesn't exist."""
92
+ # Setup
93
+ wiz = WizMixin()
94
+ file_path = "artifacts/test_data.json"
95
+ fresh_data = [{"id": "3", "name": "new"}]
96
+
97
+ # Mock file doesn't exist
98
+ mock_exists.return_value = False
99
+ mock_fetch_data.return_value = fresh_data
100
+
101
+ # Execute
102
+ result = wiz.fetch_data_if_needed(
103
+ file_path=file_path, query="query {}", topic_key="test", interval_hours=2, variables={"key": "value"}
104
+ )
105
+
106
+ # Verify
107
+ assert result == fresh_data
108
+ mock_fetch_data.assert_called_once_with("query {}", "test", {"key": "value"})
109
+ mock_write_to_file.assert_called_once_with(file_path, fresh_data)
110
+
111
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.load_file")
112
+ @patch("os.path.exists")
113
+ @patch("os.path.getmtime")
114
+ def test_fetch_data_if_needed_with_custom_interval(self, mock_getmtime, mock_exists, mock_load_file):
115
+ """Test fetch_data_if_needed respects custom interval hours."""
116
+ # Setup
117
+ wiz = WizMixin()
118
+ file_path = "artifacts/test_data.json"
119
+ cached_data = [{"id": "4", "name": "cached"}]
120
+
121
+ # Mock file exists and is fresh with custom 24 hour interval
122
+ mock_exists.return_value = True
123
+ current_time = datetime.now()
124
+ file_mod_time = current_time - timedelta(hours=12)
125
+ mock_getmtime.return_value = file_mod_time.timestamp()
126
+ mock_load_file.return_value = cached_data
127
+
128
+ # Execute with 24 hour interval - should use cache
129
+ result = wiz.fetch_data_if_needed(
130
+ file_path=file_path, query="query {}", topic_key="test", interval_hours=24, variables=None
131
+ )
132
+
133
+ # Verify
134
+ assert result == cached_data
135
+ mock_load_file.assert_called_once_with(file_path)
136
+
137
+
138
+ class TestWizMixinWriteToFile:
139
+ """Test write_to_file static method."""
140
+
141
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.check_file_path")
142
+ def test_write_to_file_creates_json(self, mock_check_file_path):
143
+ """Test write_to_file creates valid JSON file."""
144
+ with tempfile.TemporaryDirectory() as tmpdir:
145
+ file_path = os.path.join(tmpdir, "test.json")
146
+ test_data = [{"id": "1", "name": "test"}, {"id": "2", "name": "test2"}]
147
+
148
+ # Execute
149
+ WizMixin.write_to_file(file_path, test_data)
150
+
151
+ # Verify
152
+ mock_check_file_path.assert_called_once_with("artifacts")
153
+ assert os.path.exists(file_path)
154
+ with open(file_path, "r") as f:
155
+ loaded_data = json.load(f)
156
+ assert loaded_data == test_data
157
+
158
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.check_file_path")
159
+ def test_write_to_file_empty_list(self, mock_check_file_path):
160
+ """Test write_to_file handles empty list."""
161
+ with tempfile.TemporaryDirectory() as tmpdir:
162
+ file_path = os.path.join(tmpdir, "empty.json")
163
+ test_data = []
164
+
165
+ # Execute
166
+ WizMixin.write_to_file(file_path, test_data)
167
+
168
+ # Verify
169
+ assert os.path.exists(file_path)
170
+ with open(file_path, "r") as f:
171
+ loaded_data = json.load(f)
172
+ assert loaded_data == []
173
+
174
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.check_file_path")
175
+ def test_write_to_file_complex_data(self, mock_check_file_path):
176
+ """Test write_to_file handles complex nested data structures."""
177
+ with tempfile.TemporaryDirectory() as tmpdir:
178
+ file_path = os.path.join(tmpdir, "complex.json")
179
+ test_data = [
180
+ {"id": "1", "nested": {"key": "value"}, "list": [1, 2, 3]},
181
+ {"id": "2", "nested": {"key2": "value2"}, "list": [4, 5, 6]},
182
+ ]
183
+
184
+ # Execute
185
+ WizMixin.write_to_file(file_path, test_data)
186
+
187
+ # Verify
188
+ assert os.path.exists(file_path)
189
+ with open(file_path, "r") as f:
190
+ loaded_data = json.load(f)
191
+ assert loaded_data == test_data
192
+
193
+
194
+ class TestWizMixinLoadFile:
195
+ """Test load_file static method."""
196
+
197
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.check_file_path")
198
+ def test_load_file_loads_json(self, mock_check_file_path):
199
+ """Test load_file successfully loads JSON data."""
200
+ with tempfile.TemporaryDirectory() as tmpdir:
201
+ file_path = os.path.join(tmpdir, "test.json")
202
+ test_data = [{"id": "1", "name": "test"}]
203
+
204
+ # Create test file
205
+ with open(file_path, "w") as f:
206
+ json.dump(test_data, f)
207
+
208
+ # Execute
209
+ result = WizMixin.load_file(file_path)
210
+
211
+ # Verify
212
+ mock_check_file_path.assert_called_once_with("artifacts")
213
+ assert result == test_data
214
+
215
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.check_file_path")
216
+ def test_load_file_empty_json(self, mock_check_file_path):
217
+ """Test load_file handles empty JSON array."""
218
+ with tempfile.TemporaryDirectory() as tmpdir:
219
+ file_path = os.path.join(tmpdir, "empty.json")
220
+
221
+ # Create empty JSON file
222
+ with open(file_path, "w") as f:
223
+ json.dump([], f)
224
+
225
+ # Execute
226
+ result = WizMixin.load_file(file_path)
227
+
228
+ # Verify
229
+ assert result == []
230
+
231
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.check_file_path")
232
+ def test_load_file_complex_data(self, mock_check_file_path):
233
+ """Test load_file handles complex nested structures."""
234
+ with tempfile.TemporaryDirectory() as tmpdir:
235
+ file_path = os.path.join(tmpdir, "complex.json")
236
+ test_data = [
237
+ {"id": "1", "nested": {"deep": {"deeper": "value"}}, "list": [1, 2, {"key": "value"}]},
238
+ ]
239
+
240
+ # Create test file
241
+ with open(file_path, "w") as f:
242
+ json.dump(test_data, f)
243
+
244
+ # Execute
245
+ result = WizMixin.load_file(file_path)
246
+
247
+ # Verify
248
+ assert result == test_data
249
+
250
+
251
+ class TestWizMixinFetchData:
252
+ """Test fetch_data method with GraphQL client mocking."""
253
+
254
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
255
+ def test_fetch_data_success(self, mock_client_class):
256
+ """Test fetch_data successfully fetches data from Wiz."""
257
+ # Setup
258
+ wiz = WizMixin()
259
+ wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": "test-token-123"}
260
+
261
+ mock_client = MagicMock()
262
+ mock_client.fetch_all.return_value = [
263
+ {"id": "1", "name": "result1"},
264
+ {"id": "2", "name": "result2"},
265
+ ]
266
+ mock_client_class.return_value = mock_client
267
+
268
+ # Execute
269
+ result = wiz.fetch_data(query="query {}", topic_key="test", variables={"key": "value"})
270
+
271
+ # Verify
272
+ assert len(result) == 2
273
+ assert result[0]["name"] == "result1"
274
+ assert result[1]["name"] == "result2"
275
+
276
+ # Verify client was created with correct parameters
277
+ mock_client_class.assert_called_once_with(
278
+ endpoint="https://api.wiz.io/graphql",
279
+ query="query {}",
280
+ headers={"Content-Type": "application/json", "Authorization": "Bearer test-token-123"},
281
+ )
282
+ mock_client.fetch_all.assert_called_once_with(variables={"key": "value"}, topic_key="test")
283
+
284
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.error_and_exit")
285
+ def test_fetch_data_missing_url_config(self, mock_error_and_exit):
286
+ """Test fetch_data handles missing Wiz URL configuration."""
287
+ # Setup
288
+ wiz = WizMixin()
289
+ wiz.config = {"wizAccessToken": "test-token"} # Missing wizUrl
290
+
291
+ mock_error_and_exit.side_effect = SystemExit(1)
292
+
293
+ # Execute & Verify
294
+ with pytest.raises(SystemExit):
295
+ wiz.fetch_data(query="query {}", topic_key="test", variables=None)
296
+
297
+ mock_error_and_exit.assert_called_once_with("Wiz API endpoint not configured")
298
+
299
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.error_and_exit")
300
+ def test_fetch_data_empty_url_config(self, mock_error_and_exit):
301
+ """Test fetch_data handles empty Wiz URL configuration."""
302
+ # Setup
303
+ wiz = WizMixin()
304
+ wiz.config = {"wizUrl": "", "wizAccessToken": "test-token"}
305
+
306
+ mock_error_and_exit.side_effect = SystemExit(1)
307
+
308
+ # Execute & Verify
309
+ with pytest.raises(SystemExit):
310
+ wiz.fetch_data(query="query {}", topic_key="test", variables=None)
311
+
312
+ mock_error_and_exit.assert_called_once_with("Wiz API endpoint not configured")
313
+
314
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
315
+ def test_fetch_data_no_token_returns_empty(self, mock_client_class):
316
+ """Test fetch_data returns empty list when no access token."""
317
+ # Setup
318
+ wiz = WizMixin()
319
+ wiz.config = {"wizUrl": "https://api.wiz.io/graphql"} # No token
320
+
321
+ # Execute
322
+ result = wiz.fetch_data(query="query {}", topic_key="test", variables=None)
323
+
324
+ # Verify
325
+ assert result == []
326
+ mock_client_class.assert_not_called()
327
+
328
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
329
+ def test_fetch_data_empty_token_returns_empty(self, mock_client_class):
330
+ """Test fetch_data returns empty list when access token is empty."""
331
+ # Setup
332
+ wiz = WizMixin()
333
+ wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": ""}
334
+
335
+ # Execute
336
+ result = wiz.fetch_data(query="query {}", topic_key="test", variables=None)
337
+
338
+ # Verify
339
+ assert result == []
340
+ mock_client_class.assert_not_called()
341
+
342
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
343
+ def test_fetch_data_with_no_variables(self, mock_client_class):
344
+ """Test fetch_data works without variables parameter."""
345
+ # Setup
346
+ wiz = WizMixin()
347
+ wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": "token"}
348
+
349
+ mock_client = MagicMock()
350
+ mock_client.fetch_all.return_value = [{"id": "1"}]
351
+ mock_client_class.return_value = mock_client
352
+
353
+ # Execute
354
+ result = wiz.fetch_data(query="query {}", topic_key="test", variables=None)
355
+
356
+ # Verify
357
+ assert len(result) == 1
358
+ mock_client.fetch_all.assert_called_once_with(variables=None, topic_key="test")
359
+
360
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
361
+ def test_fetch_data_returns_empty_list_from_client(self, mock_client_class):
362
+ """Test fetch_data handles empty results from GraphQL client."""
363
+ # Setup
364
+ wiz = WizMixin()
365
+ wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": "token"}
366
+
367
+ mock_client = MagicMock()
368
+ mock_client.fetch_all.return_value = []
369
+ mock_client_class.return_value = mock_client
370
+
371
+ # Execute
372
+ result = wiz.fetch_data(query="query {}", topic_key="test", variables=None)
373
+
374
+ # Verify
375
+ assert result == []
376
+
377
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
378
+ def test_fetch_data_bearer_token_format(self, mock_client_class):
379
+ """Test fetch_data properly formats Bearer token in headers."""
380
+ # Setup
381
+ wiz = WizMixin()
382
+ wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": "my-access-token"}
383
+
384
+ mock_client = MagicMock()
385
+ mock_client.fetch_all.return_value = []
386
+ mock_client_class.return_value = mock_client
387
+
388
+ # Execute
389
+ wiz.fetch_data(query="query {}", topic_key="test", variables=None)
390
+
391
+ # Verify Bearer token format
392
+ call_args = mock_client_class.call_args
393
+ assert call_args[1]["headers"]["Authorization"] == "Bearer my-access-token"
394
+
395
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
396
+ def test_fetch_data_content_type_header(self, mock_client_class):
397
+ """Test fetch_data includes correct Content-Type header."""
398
+ # Setup
399
+ wiz = WizMixin()
400
+ wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": "token"}
401
+
402
+ mock_client = MagicMock()
403
+ mock_client.fetch_all.return_value = []
404
+ mock_client_class.return_value = mock_client
405
+
406
+ # Execute
407
+ wiz.fetch_data(query="query {}", topic_key="test", variables=None)
408
+
409
+ # Verify Content-Type header
410
+ call_args = mock_client_class.call_args
411
+ assert call_args[1]["headers"]["Content-Type"] == "application/json"
412
+
413
+
414
+ class TestWizMixinEdgeCases:
415
+ """Test edge cases and error scenarios."""
416
+
417
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.write_to_file")
418
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.fetch_data")
419
+ @patch("os.path.exists")
420
+ @patch("os.path.getmtime")
421
+ def test_fetch_data_if_needed_exactly_at_interval_boundary(
422
+ self, mock_getmtime, mock_exists, mock_fetch_data, mock_write_to_file
423
+ ):
424
+ """Test behavior when file age exactly equals interval."""
425
+ # Setup
426
+ wiz = WizMixin()
427
+ file_path = "artifacts/test.json"
428
+ fresh_data = [{"id": "1"}]
429
+
430
+ # Mock file modified exactly 2 hours ago with 2 hour interval
431
+ mock_exists.return_value = True
432
+ current_time = datetime.now()
433
+ file_mod_time = current_time - timedelta(hours=2)
434
+ mock_getmtime.return_value = file_mod_time.timestamp()
435
+ mock_fetch_data.return_value = fresh_data
436
+
437
+ # Execute
438
+ result = wiz.fetch_data_if_needed(
439
+ file_path=file_path, query="query {}", topic_key="test", interval_hours=2, variables=None
440
+ )
441
+
442
+ # Verify - at boundary, should fetch new data
443
+ assert result == fresh_data
444
+ mock_fetch_data.assert_called_once()
445
+ mock_write_to_file.assert_called_once()
446
+
447
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.load_file")
448
+ @patch("os.path.exists")
449
+ @patch("os.path.getmtime")
450
+ def test_fetch_data_if_needed_very_fresh_file(self, mock_getmtime, mock_exists, mock_load_file):
451
+ """Test with very recently modified file (seconds old)."""
452
+ # Setup
453
+ wiz = WizMixin()
454
+ file_path = "artifacts/test.json"
455
+ cached_data = [{"id": "1"}]
456
+
457
+ # Mock file modified 30 seconds ago
458
+ mock_exists.return_value = True
459
+ current_time = datetime.now()
460
+ file_mod_time = current_time - timedelta(seconds=30)
461
+ mock_getmtime.return_value = file_mod_time.timestamp()
462
+ mock_load_file.return_value = cached_data
463
+
464
+ # Execute with 1 hour interval
465
+ result = wiz.fetch_data_if_needed(
466
+ file_path=file_path, query="query {}", topic_key="test", interval_hours=1, variables=None
467
+ )
468
+
469
+ # Verify - should use cache
470
+ assert result == cached_data
471
+ mock_load_file.assert_called_once()
472
+
473
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.write_to_file")
474
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.WizMixin.fetch_data")
475
+ @patch("os.path.exists")
476
+ def test_fetch_data_if_needed_with_zero_interval(self, mock_exists, mock_fetch_data, mock_write_to_file):
477
+ """Test with zero hour interval (always fetch)."""
478
+ # Setup
479
+ wiz = WizMixin()
480
+ file_path = "artifacts/test.json"
481
+ fresh_data = [{"id": "1"}]
482
+
483
+ mock_exists.return_value = False
484
+ mock_fetch_data.return_value = fresh_data
485
+
486
+ # Execute with 0 hour interval
487
+ result = wiz.fetch_data_if_needed(
488
+ file_path=file_path, query="query {}", topic_key="test", interval_hours=0, variables=None
489
+ )
490
+
491
+ # Verify
492
+ assert result == fresh_data
493
+ mock_fetch_data.assert_called_once()
494
+
495
+ @patch("regscale.integrations.commercial.wizv2.WizDataMixin.PaginatedGraphQLClient")
496
+ def test_fetch_data_with_complex_variables(self, mock_client_class):
497
+ """Test fetch_data with complex nested variables."""
498
+ # Setup
499
+ wiz = WizMixin()
500
+ wiz.config = {"wizUrl": "https://api.wiz.io/graphql", "wizAccessToken": "token"}
501
+
502
+ complex_variables = {
503
+ "filterBy": {"status": ["OPEN", "IN_PROGRESS"], "severity": ["HIGH", "CRITICAL"]},
504
+ "first": 100,
505
+ "orderBy": {"field": "CREATED_AT", "direction": "DESC"},
506
+ }
507
+
508
+ mock_client = MagicMock()
509
+ mock_client.fetch_all.return_value = [{"id": "1"}]
510
+ mock_client_class.return_value = mock_client
511
+
512
+ # Execute
513
+ result = wiz.fetch_data(query="query {}", topic_key="test", variables=complex_variables)
514
+
515
+ # Verify
516
+ assert len(result) == 1
517
+ mock_client.fetch_all.assert_called_once_with(variables=complex_variables, topic_key="test")
518
+
519
+ @patch("regscale.core.app.utils.app_utils.check_file_path")
520
+ def test_write_to_file_overwrites_existing(self, mock_check_file_path):
521
+ """Test write_to_file overwrites existing file."""
522
+ with tempfile.TemporaryDirectory() as tmpdir:
523
+ file_path = os.path.join(tmpdir, "test.json")
524
+
525
+ # Write initial data
526
+ initial_data = [{"id": "1", "name": "old"}]
527
+ WizMixin.write_to_file(file_path, initial_data)
528
+
529
+ # Overwrite with new data
530
+ new_data = [{"id": "2", "name": "new"}]
531
+ WizMixin.write_to_file(file_path, new_data)
532
+
533
+ # Verify only new data exists
534
+ with open(file_path, "r") as f:
535
+ loaded_data = json.load(f)
536
+ assert loaded_data == new_data
537
+ assert len(loaded_data) == 1