regscale-cli 6.25.1.0__py3-none-any.whl → 6.27.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (146) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +19 -4
  4. regscale/core/app/internal/evidence.py +419 -2
  5. regscale/core/app/internal/login.py +0 -1
  6. regscale/core/app/utils/catalog_utils/common.py +1 -1
  7. regscale/dev/code_gen.py +24 -20
  8. regscale/integrations/commercial/jira.py +367 -126
  9. regscale/integrations/commercial/qualys/__init__.py +7 -8
  10. regscale/integrations/commercial/qualys/scanner.py +8 -3
  11. regscale/integrations/commercial/sicura/api.py +14 -13
  12. regscale/integrations/commercial/sicura/commands.py +8 -2
  13. regscale/integrations/commercial/sicura/scanner.py +49 -39
  14. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  15. regscale/integrations/commercial/synqly/assets.py +17 -0
  16. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  17. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  18. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  19. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  20. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  21. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  22. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  23. regscale/integrations/commercial/wizv2/click.py +64 -79
  24. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  25. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  26. regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
  27. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  28. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  29. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  30. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  31. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  33. regscale/integrations/commercial/wizv2/issue.py +1 -1
  34. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  35. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  36. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  37. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  38. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  39. regscale/integrations/commercial/wizv2/reports.py +1 -1
  40. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  41. regscale/integrations/commercial/wizv2/scanner.py +39 -99
  42. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  43. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  44. regscale/integrations/commercial/wizv2/variables.py +89 -3
  45. regscale/integrations/compliance_integration.py +60 -41
  46. regscale/integrations/control_matcher.py +377 -0
  47. regscale/integrations/due_date_handler.py +14 -8
  48. regscale/integrations/milestone_manager.py +291 -0
  49. regscale/integrations/public/__init__.py +1 -0
  50. regscale/integrations/public/cci_importer.py +37 -38
  51. regscale/integrations/public/fedramp/click.py +60 -2
  52. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  53. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  54. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  55. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  56. regscale/integrations/scanner_integration.py +277 -153
  57. regscale/models/integration_models/cisa_kev_data.json +282 -9
  58. regscale/models/integration_models/nexpose.py +36 -10
  59. regscale/models/integration_models/qualys.py +3 -4
  60. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  61. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  62. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  63. regscale/models/locking.py +12 -8
  64. regscale/models/platform.py +1 -2
  65. regscale/models/regscale_models/control_implementation.py +47 -22
  66. regscale/models/regscale_models/issue.py +256 -95
  67. regscale/models/regscale_models/milestone.py +1 -1
  68. regscale/models/regscale_models/regscale_model.py +6 -1
  69. regscale/templates/__init__.py +0 -0
  70. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  71. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
  72. tests/regscale/integrations/commercial/__init__.py +0 -0
  73. tests/regscale/integrations/commercial/conftest.py +28 -0
  74. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  75. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  76. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  77. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  78. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  79. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  80. tests/regscale/integrations/commercial/test_burp.py +48 -0
  81. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  82. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  83. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  84. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  85. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  86. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  87. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  88. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  89. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  90. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  91. tests/regscale/integrations/commercial/test_snow.py +423 -0
  92. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  93. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  94. tests/regscale/integrations/commercial/test_stig.py +33 -0
  95. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  96. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  97. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  98. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  99. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  100. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  101. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  102. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  103. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  104. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  105. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  106. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  107. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  108. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  109. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  110. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  111. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  112. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  113. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  114. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  115. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  116. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  117. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  118. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  119. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  120. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  121. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  122. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  123. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  124. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  125. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  126. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  127. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  128. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  129. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  130. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
  131. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  132. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  133. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  134. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  135. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  136. tests/regscale/integrations/public/test_fedramp.py +301 -0
  137. tests/regscale/integrations/test_control_matcher.py +1397 -0
  138. tests/regscale/integrations/test_control_matching.py +155 -0
  139. tests/regscale/integrations/test_milestone_manager.py +408 -0
  140. tests/regscale/models/test_issue.py +378 -1
  141. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  142. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  143. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  144. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  145. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  146. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1228 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Tests for Okta integration in RegScale CLI"""
4
+ # standard python imports
5
+ import json
6
+ import os
7
+ from datetime import datetime, timedelta
8
+ from json import JSONDecodeError
9
+ from pathlib import Path
10
+ from unittest.mock import MagicMock, patch, mock_open
11
+ from typing import Dict, Any
12
+
13
+ import pytest
14
+ import jwcrypto.jwk as jwk
15
+
16
+ from regscale.core.app.api import Api
17
+ from regscale.core.app.application import Application
18
+ from regscale.integrations.commercial.okta import (
19
+ authenticate,
20
+ authenticate_with_okta,
21
+ analyze_okta_users,
22
+ check_and_save_data,
23
+ clean_okta_output,
24
+ compare_dates_and_user_type,
25
+ get_all_okta_users,
26
+ get_okta_data,
27
+ get_okta_token,
28
+ get_user_roles,
29
+ save_active_users_from_okta,
30
+ save_admin_users_from_okta,
31
+ save_all_users_from_okta,
32
+ save_inactive_users_from_okta,
33
+ save_recently_added_users_from_okta,
34
+ )
35
+ from tests import CLITestFixture
36
+
37
+ PATH = "regscale.integrations.commercial.okta"
38
+
39
+
40
+ class TestOkta(CLITestFixture):
41
+ """
42
+ Test for Okta integration
43
+ """
44
+
45
+ def test_init(self):
46
+ """Test init file and config"""
47
+ # Test that the core required Okta config keys exist
48
+ # Note: oktaScopes is not always present in config, so only test the core keys
49
+ self.verify_config(
50
+ [
51
+ "oktaUrl",
52
+ "oktaApiToken",
53
+ "oktaClientId",
54
+ ],
55
+ compare_template=False, # Don't compare to template since these might be defaults
56
+ )
57
+
58
+ # Test Click Commands
59
+ def test_authenticate_command(self):
60
+ """Test the authenticate Click command directly calls function"""
61
+ with patch(f"{PATH}.check_license") as mock_check_license, patch(f"{PATH}.Api") as mock_api, patch(
62
+ f"{PATH}.authenticate_with_okta"
63
+ ) as mock_auth:
64
+
65
+ # Setup mocks
66
+ mock_app = MagicMock(spec=Application)
67
+ mock_check_license.return_value = mock_app
68
+ mock_api_instance = MagicMock(spec=Api)
69
+ mock_api.return_value = mock_api_instance
70
+
71
+ # Call the click command function directly
72
+ from regscale.integrations.commercial.okta import authenticate
73
+
74
+ authenticate.callback(type="SSWS")
75
+
76
+ # Verify calls
77
+ mock_check_license.assert_called_once()
78
+ mock_api.assert_called_once()
79
+ mock_auth.assert_called_once_with(mock_app, mock_api_instance, "SSWS")
80
+
81
+ def test_get_active_users_command(self):
82
+ """Test the get_active_users Click command directly calls function"""
83
+ with patch(f"{PATH}.save_active_users_from_okta") as mock_save:
84
+ from regscale.integrations.commercial.okta import get_active_users
85
+
86
+ # Call the click command function directly
87
+ get_active_users.callback(save_output_to=Path("."), file_type=".csv")
88
+
89
+ mock_save.assert_called_once_with(save_output_to=Path("."), file_type=".csv")
90
+
91
+ def test_get_inactive_users_command(self):
92
+ """Test the get_inactive_users Click command directly calls function"""
93
+ with patch(f"{PATH}.save_inactive_users_from_okta") as mock_save:
94
+ from regscale.integrations.commercial.okta import get_inactive_users
95
+
96
+ # Call the click command function directly
97
+ get_inactive_users.callback(days=45, save_output_to=Path("."), file_type=".xlsx")
98
+
99
+ mock_save.assert_called_once_with(days=45, save_output_to=Path("."), file_type=".xlsx")
100
+
101
+ def test_get_all_users_command(self):
102
+ """Test the get_all_users Click command directly calls function"""
103
+ with patch(f"{PATH}.save_all_users_from_okta") as mock_save:
104
+ from regscale.integrations.commercial.okta import get_all_users
105
+
106
+ # Call the click command function directly
107
+ get_all_users.callback(save_output_to=Path("."), file_type=".csv")
108
+
109
+ mock_save.assert_called_once_with(save_output_to=Path("."), file_type=".csv")
110
+
111
+ def test_get_recent_users_command(self):
112
+ """Test the get_recent_users Click command directly calls function"""
113
+ with patch(f"{PATH}.save_recently_added_users_from_okta") as mock_save:
114
+ from regscale.integrations.commercial.okta import get_recent_users
115
+
116
+ # Call the click command function directly
117
+ get_recent_users.callback(days=15, save_output_to=Path("."), file_type=".xlsx")
118
+
119
+ mock_save.assert_called_once_with(days=15, save_output_to=Path("."), file_type=".xlsx")
120
+
121
+ def test_get_admin_users_command(self):
122
+ """Test the get_admin_users Click command directly calls function"""
123
+ with patch(f"{PATH}.save_admin_users_from_okta") as mock_save:
124
+ from regscale.integrations.commercial.okta import get_admin_users
125
+
126
+ # Call the click command function directly
127
+ get_admin_users.callback(save_output_to=Path("."), file_type=".csv")
128
+
129
+ mock_save.assert_called_once_with(save_output_to=Path("."), file_type=".csv")
130
+
131
+ # Test Core Functions
132
+ @patch(f"{PATH}.check_and_save_data")
133
+ @patch(f"{PATH}.get_okta_data")
134
+ @patch(f"{PATH}.check_file_path")
135
+ @patch(f"{PATH}.authenticate_with_okta")
136
+ @patch(f"{PATH}.job_progress")
137
+ @patch(f"{PATH}.is_valid")
138
+ @patch(f"{PATH}.Api")
139
+ @patch(f"{PATH}.check_license")
140
+ def test_save_active_users_from_okta_success(
141
+ self,
142
+ mock_check_license,
143
+ mock_api,
144
+ mock_is_valid,
145
+ mock_job_progress,
146
+ mock_authenticate_with_okta,
147
+ mock_check_file_path,
148
+ mock_get_okta_data,
149
+ mock_check_and_save_data,
150
+ ):
151
+ """Test saving active users from Okta - success path"""
152
+ # Setup mocks
153
+ mock_app = MagicMock(spec=Application)
154
+ mock_app.config = {"oktaApiToken": "SSWS test-token"}
155
+ mock_check_license.return_value = mock_app
156
+
157
+ mock_api_instance = MagicMock(spec=Api)
158
+ mock_api_instance.config = {"oktaUrl": "https://test.okta.com", "oktaApiToken": "SSWS test-token"}
159
+ mock_api.return_value = mock_api_instance
160
+
161
+ mock_is_valid.return_value = True
162
+
163
+ # Setup progress mock
164
+ mock_progress = MagicMock()
165
+ mock_progress.add_task.return_value = 1
166
+ mock_job_progress.__enter__.return_value = mock_progress
167
+ mock_job_progress.__exit__.return_value = None
168
+
169
+ # Setup user data
170
+ test_users = [{"id": "1", "profile": {"login": "test@example.com"}}]
171
+ mock_get_okta_data.return_value = test_users
172
+
173
+ # Test the function
174
+ save_path = Path("/test/path")
175
+ save_active_users_from_okta(save_output_to=save_path, file_type=".csv")
176
+
177
+ # Verify calls
178
+ mock_check_license.assert_called_once()
179
+ mock_api.assert_called_once()
180
+ mock_is_valid.assert_called_once_with(app=mock_app)
181
+ mock_authenticate_with_okta.assert_called_once_with(mock_app, mock_api_instance, "SSWS")
182
+ mock_check_file_path.assert_called_once_with(save_path)
183
+ mock_get_okta_data.assert_called_once()
184
+ mock_check_and_save_data.assert_called_once_with(
185
+ data=test_users,
186
+ file_name="okta_active_users",
187
+ file_path=save_path,
188
+ file_type=".csv",
189
+ data_desc="active user(s)",
190
+ )
191
+
192
+ @patch(f"{PATH}.error_and_exit")
193
+ @patch(f"{PATH}.is_valid")
194
+ @patch(f"{PATH}.Api")
195
+ @patch(f"{PATH}.check_license")
196
+ def test_save_active_users_from_okta_invalid_token(
197
+ self, mock_check_license, mock_api, mock_is_valid, mock_error_and_exit
198
+ ):
199
+ """Test saving active users with invalid RegScale token"""
200
+ # Setup mocks
201
+ mock_app = MagicMock(spec=Application)
202
+ mock_check_license.return_value = mock_app
203
+ mock_api.return_value = MagicMock(spec=Api)
204
+ mock_is_valid.return_value = False
205
+ mock_error_and_exit.side_effect = SystemExit(1)
206
+
207
+ # Test the function
208
+ with pytest.raises(SystemExit):
209
+ save_active_users_from_okta(save_output_to=Path("/test"), file_type=".csv")
210
+
211
+ # Verify error was called
212
+ mock_error_and_exit.assert_called_once_with(
213
+ "Login Error: Invalid RegScale credentials. Please log in for a new token."
214
+ )
215
+
216
+ @patch(f"{PATH}.error_and_exit")
217
+ def test_save_active_users_invalid_file_type(self, mock_error_and_exit):
218
+ """Test saving active users with invalid file type"""
219
+ mock_error_and_exit.side_effect = SystemExit(1)
220
+
221
+ with pytest.raises(SystemExit):
222
+ save_active_users_from_okta(save_output_to=Path("/test"), file_type=".pdf")
223
+
224
+ mock_error_and_exit.assert_called_once_with("Invalid file type. Please choose .csv or .xlsx.")
225
+
226
+ @patch(f"{PATH}.check_and_save_data")
227
+ @patch(f"{PATH}.analyze_okta_users")
228
+ @patch(f"{PATH}.get_all_okta_users")
229
+ @patch(f"{PATH}.check_file_path")
230
+ @patch(f"{PATH}.authenticate_with_okta")
231
+ @patch(f"{PATH}.job_progress")
232
+ @patch(f"{PATH}.is_valid")
233
+ @patch(f"{PATH}.Api")
234
+ @patch(f"{PATH}.check_license")
235
+ def test_save_inactive_users_from_okta_success(
236
+ self,
237
+ mock_check_license,
238
+ mock_api,
239
+ mock_is_valid,
240
+ mock_job_progress,
241
+ mock_authenticate_with_okta,
242
+ mock_check_file_path,
243
+ mock_get_all_okta_users,
244
+ mock_analyze_okta_users,
245
+ mock_check_and_save_data,
246
+ ):
247
+ """Test saving inactive users from Okta - success path"""
248
+ # Setup mocks
249
+ mock_app = MagicMock(spec=Application)
250
+ mock_app.config = {"oktaApiToken": "Bearer test-token"}
251
+ mock_check_license.return_value = mock_app
252
+
253
+ mock_api_instance = MagicMock(spec=Api)
254
+ mock_api.return_value = mock_api_instance
255
+
256
+ mock_is_valid.return_value = True
257
+
258
+ # Setup progress mock
259
+ mock_progress = MagicMock()
260
+ mock_job_progress.__enter__.return_value = mock_progress
261
+ mock_job_progress.__exit__.return_value = None
262
+
263
+ # Setup user data
264
+ all_users = [{"id": "1", "profile": {"login": "test@example.com"}}]
265
+ inactive_users = [{"id": "1", "profile": {"login": "test@example.com"}, "lastLogin": None}]
266
+ mock_get_all_okta_users.return_value = all_users
267
+ mock_analyze_okta_users.return_value = inactive_users
268
+
269
+ # Test the function
270
+ save_path = Path("/test/path")
271
+ save_inactive_users_from_okta(save_output_to=save_path, file_type=".xlsx", days=45)
272
+
273
+ # Verify calls
274
+ mock_check_license.assert_called_once()
275
+ mock_api.assert_called_once()
276
+ mock_is_valid.assert_called_once_with(app=mock_app)
277
+ mock_authenticate_with_okta.assert_called_once_with(mock_app, mock_api_instance, "Bearer")
278
+ mock_check_file_path.assert_called_once_with(save_path)
279
+ mock_get_all_okta_users.assert_called_once_with(mock_api_instance)
280
+ mock_analyze_okta_users.assert_called_once()
281
+ mock_check_and_save_data.assert_called_once_with(
282
+ data=inactive_users,
283
+ file_name="okta_inactive_users",
284
+ file_path=save_path,
285
+ file_type=".xlsx",
286
+ data_desc="inactive user(s)",
287
+ )
288
+
289
+ @patch(f"{PATH}.check_and_save_data")
290
+ @patch(f"{PATH}.get_all_okta_users")
291
+ @patch(f"{PATH}.check_file_path")
292
+ @patch(f"{PATH}.authenticate_with_okta")
293
+ @patch(f"{PATH}.is_valid")
294
+ @patch(f"{PATH}.Api")
295
+ @patch(f"{PATH}.check_license")
296
+ def test_save_all_users_from_okta_success(
297
+ self,
298
+ mock_check_license,
299
+ mock_api,
300
+ mock_is_valid,
301
+ mock_authenticate_with_okta,
302
+ mock_check_file_path,
303
+ mock_get_all_okta_users,
304
+ mock_check_and_save_data,
305
+ ):
306
+ """Test saving all users from Okta - success path"""
307
+ # Setup mocks
308
+ mock_app = MagicMock(spec=Application)
309
+ mock_app.config = {"oktaApiToken": "SSWS test-token"}
310
+ mock_check_license.return_value = mock_app
311
+
312
+ mock_api_instance = MagicMock(spec=Api)
313
+ mock_api.return_value = mock_api_instance
314
+
315
+ mock_is_valid.return_value = True
316
+
317
+ # Setup user data
318
+ all_users = [{"id": "1", "profile": {"login": "test@example.com"}}]
319
+ mock_get_all_okta_users.return_value = all_users
320
+
321
+ # Test the function
322
+ save_path = Path("/test/path")
323
+ save_all_users_from_okta(save_output_to=save_path, file_type=".csv")
324
+
325
+ # Verify calls
326
+ mock_check_license.assert_called_once()
327
+ mock_api.assert_called_once()
328
+ mock_is_valid.assert_called_once_with(app=mock_app)
329
+ mock_authenticate_with_okta.assert_called_once_with(mock_app, mock_api_instance, "SSWS")
330
+ mock_check_file_path.assert_called_once_with(save_path)
331
+ mock_get_all_okta_users.assert_called_once_with(mock_api_instance)
332
+ mock_check_and_save_data.assert_called_once_with(
333
+ data=all_users,
334
+ file_name="okta_users",
335
+ file_path=save_path,
336
+ file_type=".csv",
337
+ data_desc="Okta users",
338
+ )
339
+
340
+ @patch(f"{PATH}.check_and_save_data")
341
+ @patch(f"{PATH}.analyze_okta_users")
342
+ @patch(f"{PATH}.get_all_okta_users")
343
+ @patch(f"{PATH}.check_file_path")
344
+ @patch(f"{PATH}.authenticate_with_okta")
345
+ @patch(f"{PATH}.job_progress")
346
+ @patch(f"{PATH}.is_valid")
347
+ @patch(f"{PATH}.Api")
348
+ @patch(f"{PATH}.check_license")
349
+ def test_save_recently_added_users_from_okta_success(
350
+ self,
351
+ mock_check_license,
352
+ mock_api,
353
+ mock_is_valid,
354
+ mock_job_progress,
355
+ mock_authenticate_with_okta,
356
+ mock_check_file_path,
357
+ mock_get_all_okta_users,
358
+ mock_analyze_okta_users,
359
+ mock_check_and_save_data,
360
+ ):
361
+ """Test saving recently added users from Okta - success path"""
362
+ # Setup mocks
363
+ mock_app = MagicMock(spec=Application)
364
+ mock_app.config = {"oktaApiToken": "SSWS test-token"}
365
+ mock_check_license.return_value = mock_app
366
+
367
+ mock_api_instance = MagicMock(spec=Api)
368
+ mock_api.return_value = mock_api_instance
369
+
370
+ mock_is_valid.return_value = True
371
+
372
+ # Setup progress mock
373
+ mock_progress = MagicMock()
374
+ mock_job_progress.__enter__.return_value = mock_progress
375
+ mock_job_progress.__exit__.return_value = None
376
+
377
+ # Setup user data
378
+ all_users = [{"id": "1", "profile": {"login": "test@example.com"}}]
379
+ new_users = [{"id": "1", "profile": {"login": "test@example.com"}, "created": "2024-01-15T10:00:00.000Z"}]
380
+ mock_get_all_okta_users.return_value = all_users
381
+ mock_analyze_okta_users.return_value = new_users
382
+
383
+ # Test the function
384
+ save_path = Path("/test/path")
385
+ save_recently_added_users_from_okta(save_output_to=save_path, file_type=".csv", days=15)
386
+
387
+ # Verify calls
388
+ mock_check_license.assert_called_once()
389
+ mock_api.assert_called_once()
390
+ mock_is_valid.assert_called_once_with(app=mock_app)
391
+ mock_authenticate_with_okta.assert_called_once_with(mock_app, mock_api_instance, "SSWS")
392
+ mock_check_file_path.assert_called_once_with(save_path)
393
+ mock_get_all_okta_users.assert_called_once_with(mock_api_instance)
394
+ mock_analyze_okta_users.assert_called_once()
395
+ mock_check_and_save_data.assert_called_once_with(
396
+ data=new_users,
397
+ file_name="okta_new_users",
398
+ file_path=save_path,
399
+ file_type=".csv",
400
+ data_desc="new user(s)",
401
+ )
402
+
403
+ @patch(f"{PATH}.admin_users", []) # Reset global admin_users list
404
+ @patch(f"{PATH}.check_and_save_data")
405
+ @patch(f"{PATH}.create_threads")
406
+ @patch(f"{PATH}.get_all_okta_users")
407
+ @patch(f"{PATH}.check_file_path")
408
+ @patch(f"{PATH}.authenticate_with_okta")
409
+ @patch(f"{PATH}.job_progress")
410
+ @patch(f"{PATH}.is_valid")
411
+ @patch(f"{PATH}.Api")
412
+ @patch(f"{PATH}.check_license")
413
+ def test_save_admin_users_from_okta_success(
414
+ self,
415
+ mock_check_license,
416
+ mock_api,
417
+ mock_is_valid,
418
+ mock_job_progress,
419
+ mock_authenticate_with_okta,
420
+ mock_check_file_path,
421
+ mock_get_all_okta_users,
422
+ mock_create_threads,
423
+ mock_check_and_save_data,
424
+ ):
425
+ """Test saving admin users from Okta - success path"""
426
+ # Setup mocks
427
+ mock_app = MagicMock(spec=Application)
428
+ mock_app.config = {"oktaApiToken": "SSWS test-token"}
429
+ mock_check_license.return_value = mock_app
430
+
431
+ mock_api_instance = MagicMock(spec=Api)
432
+ mock_api.return_value = mock_api_instance
433
+
434
+ mock_is_valid.return_value = True
435
+
436
+ # Setup progress mock
437
+ mock_progress = MagicMock()
438
+ mock_progress.add_task.return_value = 1
439
+ mock_job_progress.__enter__.return_value = mock_progress
440
+ mock_job_progress.__exit__.return_value = None
441
+
442
+ # Setup user data
443
+ all_users = [{"id": "1", "profile": {"login": "admin@example.com"}}]
444
+ mock_get_all_okta_users.return_value = all_users
445
+
446
+ # Test the function
447
+ save_path = Path("/test/path")
448
+ save_admin_users_from_okta(save_output_to=save_path, file_type=".csv")
449
+
450
+ # Verify calls
451
+ mock_check_license.assert_called_once()
452
+ mock_api.assert_called_once()
453
+ mock_is_valid.assert_called_once_with(app=mock_app)
454
+ mock_authenticate_with_okta.assert_called_once_with(mock_app, mock_api_instance, "SSWS")
455
+ mock_check_file_path.assert_called_once_with(save_path)
456
+ mock_get_all_okta_users.assert_called_once_with(mock_api_instance)
457
+ mock_create_threads.assert_called_once()
458
+ mock_check_and_save_data.assert_called_once()
459
+
460
+ @patch(f"{PATH}.get_okta_data")
461
+ @patch(f"{PATH}.job_progress")
462
+ def test_get_all_okta_users(self, mock_job_progress, mock_get_okta_data):
463
+ """Test getting all Okta users"""
464
+ # Setup mocks
465
+ mock_api = MagicMock(spec=Api)
466
+ mock_api.config = {"oktaUrl": "https://test.okta.com", "oktaApiToken": "SSWS test-token"}
467
+
468
+ # Setup progress mock
469
+ mock_progress = MagicMock()
470
+ mock_progress.add_task.return_value = 1
471
+ mock_job_progress.add_task.return_value = 1
472
+
473
+ test_users = [{"id": "1", "profile": {"login": "test@example.com"}}]
474
+ mock_get_okta_data.return_value = test_users
475
+
476
+ # Test the function
477
+ result = get_all_okta_users(api=mock_api)
478
+
479
+ # Verify calls and result
480
+ mock_get_okta_data.assert_called_once()
481
+ assert result == test_users
482
+
483
+ @patch(f"{PATH}.parse_url_for_pagination")
484
+ @patch(f"{PATH}.job_progress")
485
+ def test_get_okta_data_success(self, mock_job_progress, mock_parse_url):
486
+ """Test getting Okta data with successful response"""
487
+ # Setup mocks
488
+ mock_api = MagicMock(spec=Api)
489
+ mock_response = MagicMock()
490
+ mock_response.status_code = 200
491
+ mock_response.json.return_value = [{"id": "1", "profile": {"login": "test@example.com"}}]
492
+ mock_response.headers.get.return_value = "" # No pagination
493
+ mock_api.get.return_value = mock_response
494
+
495
+ mock_job_progress.update = MagicMock()
496
+
497
+ # Test the function
498
+ result = get_okta_data(
499
+ api=mock_api,
500
+ task=1,
501
+ url="https://test.okta.com/api/v1/users",
502
+ headers={"Authorization": "SSWS test-token"},
503
+ params=(("limit", "200"),),
504
+ )
505
+
506
+ # Verify calls and result
507
+ mock_api.get.assert_called_once_with(
508
+ url="https://test.okta.com/api/v1/users",
509
+ headers={"Authorization": "SSWS test-token"},
510
+ params=(("limit", "200"),),
511
+ )
512
+ assert result == [{"id": "1", "profile": {"login": "test@example.com"}}]
513
+ mock_job_progress.update.assert_called_once_with(1, advance=1)
514
+
515
+ @patch(f"{PATH}.error_and_exit")
516
+ def test_get_okta_data_403_error(self, mock_error_and_exit):
517
+ """Test getting Okta data with 403 permission error"""
518
+ # Setup mocks
519
+ mock_api = MagicMock(spec=Api)
520
+ mock_response = MagicMock()
521
+ mock_response.status_code = 403
522
+ mock_api.get.return_value = mock_response
523
+
524
+ mock_error_and_exit.side_effect = SystemExit(1)
525
+
526
+ # Test the function
527
+ with pytest.raises(SystemExit):
528
+ get_okta_data(
529
+ api=mock_api,
530
+ task=1,
531
+ url="https://test.okta.com/api/v1/users",
532
+ headers={"Authorization": "SSWS test-token"},
533
+ )
534
+
535
+ # Verify error message
536
+ mock_error_and_exit.assert_called_once_with(
537
+ "RegScale CLI wasn't granted the necessary permissions for this action."
538
+ + "Please verify permissions in Okta admin portal and try again."
539
+ )
540
+
541
+ @patch(f"{PATH}.error_and_exit")
542
+ def test_get_okta_data_unexpected_error(self, mock_error_and_exit):
543
+ """Test getting Okta data with unexpected status code"""
544
+ # Setup mocks
545
+ mock_api = MagicMock(spec=Api)
546
+ mock_response = MagicMock()
547
+ mock_response.status_code = 500
548
+ mock_response.text = "Internal Server Error"
549
+ mock_api.get.return_value = mock_response
550
+
551
+ mock_error_and_exit.side_effect = SystemExit(1)
552
+
553
+ # Test the function
554
+ with pytest.raises(SystemExit):
555
+ get_okta_data(
556
+ api=mock_api,
557
+ task=1,
558
+ url="https://test.okta.com/api/v1/users",
559
+ headers={"Authorization": "SSWS test-token"},
560
+ )
561
+
562
+ # Verify error message
563
+ mock_error_and_exit.assert_called_once_with(
564
+ "Received unexpected response from Okta API.\n500: Internal Server Error"
565
+ )
566
+
567
+ @patch(f"{PATH}.error_and_exit")
568
+ def test_get_okta_data_json_decode_error(self, mock_error_and_exit):
569
+ """Test getting Okta data with JSON decode error"""
570
+ # Setup mocks
571
+ mock_api = MagicMock(spec=Api)
572
+ mock_response = MagicMock()
573
+ mock_response.status_code = 200
574
+ mock_response.json.side_effect = JSONDecodeError("Invalid JSON", "doc", 0)
575
+ mock_api.get.return_value = mock_response
576
+
577
+ mock_error_and_exit.side_effect = SystemExit(1)
578
+
579
+ # Test the function
580
+ with pytest.raises(SystemExit):
581
+ get_okta_data(
582
+ api=mock_api,
583
+ task=1,
584
+ url="https://test.okta.com/api/v1/users",
585
+ headers={"Authorization": "SSWS test-token"},
586
+ )
587
+
588
+ # Verify error message
589
+ mock_error_and_exit.assert_called_once()
590
+
591
+ @patch(f"{PATH}.parse_url_for_pagination")
592
+ @patch(f"{PATH}.job_progress")
593
+ def test_get_okta_data_with_pagination(self, mock_job_progress, mock_parse_url):
594
+ """Test getting Okta data with pagination"""
595
+ # Setup mocks for a basic pagination test
596
+ mock_api = MagicMock(spec=Api)
597
+ mock_response = MagicMock()
598
+ mock_response.status_code = 200
599
+ mock_response.json.return_value = [{"id": "1", "profile": {"login": "test1@example.com"}}]
600
+ mock_response.headers.get.return_value = 'rel="next"' # Has pagination
601
+ mock_api.get.return_value = mock_response
602
+
603
+ mock_parse_url.return_value = "https://test.okta.com/api/v1/users?after=123"
604
+ mock_job_progress.update = MagicMock()
605
+
606
+ # Mock the recursive call to return next page data
607
+ with patch(f"{PATH}.get_okta_data", return_value=[{"id": "2"}]):
608
+ # Test the function - the actual function concatenates results
609
+ result = get_okta_data(
610
+ api=mock_api,
611
+ task=1,
612
+ url="https://test.okta.com/api/v1/users",
613
+ headers={"Authorization": "SSWS test-token"},
614
+ )
615
+
616
+ # Should return both the first page and recursive result combined
617
+ expected_result = [{"id": "1", "profile": {"login": "test1@example.com"}}, {"id": "2"}]
618
+ assert result == expected_result
619
+
620
+ @patch(f"{PATH}.thread_assignment")
621
+ @patch(f"{PATH}.get_okta_data")
622
+ @patch(f"{PATH}.job_progress")
623
+ def test_get_user_roles(self, mock_job_progress, mock_get_okta_data, mock_thread_assignment):
624
+ """Test getting user roles function"""
625
+ # Setup mocks
626
+ mock_api = MagicMock(spec=Api)
627
+ mock_api.config = {"oktaUrl": "https://test.okta.com", "oktaApiToken": "SSWS test-token"}
628
+
629
+ all_users = [
630
+ {"id": "user1", "profile": {"login": "user1@example.com"}},
631
+ {"id": "user2", "profile": {"login": "admin@example.com"}},
632
+ ]
633
+
634
+ # Mock user roles - user1 has no admin role, user2 has admin role
635
+ mock_get_okta_data.side_effect = [[{"label": "User"}], [{"label": "Super Admin"}]] # user1 roles # user2 roles
636
+
637
+ mock_thread_assignment.return_value = [0, 1] # Process both users
638
+ mock_job_progress.update = MagicMock()
639
+
640
+ task = 1
641
+ args = (mock_api, all_users, task)
642
+ thread = 0
643
+
644
+ # Test the function with reset global admin_users
645
+ import regscale.integrations.commercial.okta as okta_module
646
+
647
+ okta_module.admin_users.clear() # Reset global list
648
+
649
+ get_user_roles(args=args, thread=thread)
650
+
651
+ # Verify calls
652
+ assert mock_get_okta_data.call_count == 2
653
+ mock_job_progress.update.assert_called_with(task, advance=1)
654
+
655
+ @patch(f"{PATH}.job_progress")
656
+ def test_analyze_okta_users_inactive(self, mock_job_progress):
657
+ """Test analyzing users for inactive users"""
658
+ # Setup test data
659
+ today = datetime.now()
660
+ old_date = today - timedelta(days=40)
661
+ recent_date = today - timedelta(days=10)
662
+
663
+ user_list = [
664
+ {
665
+ "id": "user1",
666
+ "profile": {"login": "user1@example.com"},
667
+ "lastLogin": old_date.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), # Inactive
668
+ },
669
+ {
670
+ "id": "user2",
671
+ "profile": {"login": "user2@example.com"},
672
+ "lastLogin": recent_date.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), # Active
673
+ },
674
+ {
675
+ "id": "user3",
676
+ "profile": {"login": "user3@example.com"},
677
+ "lastLogin": None, # Never logged in - should be inactive
678
+ },
679
+ ]
680
+
681
+ # Setup progress mock
682
+ mock_progress = MagicMock()
683
+ mock_progress.add_task.return_value = 1
684
+ mock_job_progress.add_task.return_value = 1
685
+ mock_job_progress.update = MagicMock()
686
+
687
+ filter_date = today - timedelta(days=30)
688
+
689
+ # Test the function
690
+ result = analyze_okta_users(
691
+ user_list=user_list, key="lastLogin", filter_value=filter_date, user_type="inactive"
692
+ )
693
+
694
+ # Verify result - should include user1 (old login) and user3 (no login)
695
+ assert len(result) == 2
696
+ assert any(user["id"] == "user1" for user in result)
697
+ assert any(user["id"] == "user3" for user in result)
698
+ assert not any(user["id"] == "user2" for user in result)
699
+
700
+ @patch(f"{PATH}.job_progress")
701
+ def test_analyze_okta_users_new(self, mock_job_progress):
702
+ """Test analyzing users for newly created users"""
703
+ # Setup test data
704
+ today = datetime.now()
705
+ old_date = today - timedelta(days=40)
706
+ recent_date = today - timedelta(days=10)
707
+
708
+ user_list = [
709
+ {
710
+ "id": "user1",
711
+ "profile": {"login": "user1@example.com"},
712
+ "created": old_date.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), # Old user
713
+ },
714
+ {
715
+ "id": "user2",
716
+ "profile": {"login": "user2@example.com"},
717
+ "created": recent_date.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), # New user
718
+ },
719
+ ]
720
+
721
+ # Setup progress mock
722
+ mock_progress = MagicMock()
723
+ mock_progress.add_task.return_value = 1
724
+ mock_job_progress.add_task.return_value = 1
725
+ mock_job_progress.update = MagicMock()
726
+
727
+ filter_date = today - timedelta(days=30)
728
+
729
+ # Test the function
730
+ result = analyze_okta_users(user_list=user_list, key="created", filter_value=filter_date, user_type="new")
731
+
732
+ # Verify result - should include user2 (recently created)
733
+ assert len(result) == 1
734
+ assert result[0]["id"] == "user2"
735
+
736
+ @patch(f"{PATH}.job_progress")
737
+ def test_analyze_okta_users_invalid_date_format(self, mock_job_progress):
738
+ """Test analyzing users with invalid date format"""
739
+ # Setup test data with invalid date
740
+ user_list = [
741
+ {
742
+ "id": "user1",
743
+ "profile": {"login": "user1@example.com"},
744
+ "lastLogin": "invalid-date-format", # Invalid date format
745
+ }
746
+ ]
747
+
748
+ # Setup mocks
749
+ mock_progress = MagicMock()
750
+ mock_progress.add_task.return_value = 1
751
+ mock_job_progress.add_task.return_value = 1
752
+ mock_job_progress.update = MagicMock()
753
+
754
+ filter_date = datetime.now() - timedelta(days=30)
755
+
756
+ # Test the function - should raise ValueError since the try/except in the code
757
+ # doesn't catch ValueError, only (TypeError, KeyError, AttributeError)
758
+ with pytest.raises(ValueError) as exc_info:
759
+ analyze_okta_users(user_list=user_list, key="lastLogin", filter_value=filter_date, user_type="inactive")
760
+
761
+ # Verify the error message
762
+ assert "time data 'invalid-date-format' does not match format" in str(exc_info.value)
763
+
764
+ def test_compare_dates_and_user_type_inactive(self):
765
+ """Test comparing dates for inactive user type"""
766
+ # Setup test data
767
+ user = {"id": "user1", "profile": {"login": "test@example.com"}}
768
+ filtered_users = []
769
+ today = datetime.now()
770
+ old_date = today - timedelta(days=40)
771
+ filter_date = today - timedelta(days=30)
772
+
773
+ # Test inactive user logic
774
+ compare_dates_and_user_type(
775
+ user=user,
776
+ filtered_users=filtered_users,
777
+ filter_value=filter_date,
778
+ user_type="inactive",
779
+ data_filter=old_date,
780
+ today=today,
781
+ )
782
+
783
+ # Should add user to filtered list (old_date is before filter_date)
784
+ assert len(filtered_users) == 1
785
+ assert filtered_users[0] == user
786
+
787
+ def test_compare_dates_and_user_type_new(self):
788
+ """Test comparing dates for new user type"""
789
+ # Setup test data
790
+ user = {"id": "user1", "profile": {"login": "test@example.com"}}
791
+ filtered_users = []
792
+ today = datetime.now()
793
+ recent_date = today - timedelta(days=10)
794
+ filter_date = today - timedelta(days=30)
795
+
796
+ # Test new user logic
797
+ compare_dates_and_user_type(
798
+ user=user,
799
+ filtered_users=filtered_users,
800
+ filter_value=filter_date,
801
+ user_type="new",
802
+ data_filter=recent_date,
803
+ today=today,
804
+ )
805
+
806
+ # Should add user to filtered list (recent_date is after filter_date)
807
+ assert len(filtered_users) == 1
808
+ assert filtered_users[0] == user
809
+
810
+ def test_compare_dates_and_user_type_not_matching(self):
811
+ """Test comparing dates when user doesn't match criteria"""
812
+ # Setup test data
813
+ user = {"id": "user1", "profile": {"login": "test@example.com"}}
814
+ filtered_users = []
815
+ today = datetime.now()
816
+ recent_date = today - timedelta(days=10)
817
+ filter_date = today - timedelta(days=30)
818
+
819
+ # Test inactive user logic with recent date (shouldn't match)
820
+ compare_dates_and_user_type(
821
+ user=user,
822
+ filtered_users=filtered_users,
823
+ filter_value=filter_date,
824
+ user_type="inactive",
825
+ data_filter=recent_date,
826
+ today=today,
827
+ )
828
+
829
+ # Should not add user to filtered list
830
+ assert len(filtered_users) == 0
831
+
832
+ @patch(f"{PATH}.save_data_to")
833
+ @patch(f"{PATH}.clean_okta_output")
834
+ @patch(f"{PATH}.get_current_datetime")
835
+ @patch(f"{PATH}.job_progress")
836
+ def test_check_and_save_data_with_data(
837
+ self, mock_job_progress, mock_get_current_datetime, mock_clean_okta_output, mock_save_data_to
838
+ ):
839
+ """Test check and save data function with valid data"""
840
+ # Setup mocks
841
+ test_data = [{"id": "1", "profile": {"login": "test@example.com"}}]
842
+ clean_data = {0: {"id": "1", "login": "test@example.com"}} # Use integer key like actual function
843
+
844
+ mock_get_current_datetime.return_value = "01012024"
845
+ mock_clean_okta_output.return_value = clean_data
846
+
847
+ # Setup progress mock
848
+ mock_progress = MagicMock()
849
+ mock_progress.add_task.return_value = 1
850
+ mock_progress.update = MagicMock()
851
+ mock_job_progress.__enter__.return_value = mock_progress
852
+ mock_job_progress.__exit__.return_value = None
853
+
854
+ # Test the function
855
+ check_and_save_data(
856
+ data=test_data,
857
+ file_name="test_users",
858
+ file_path=Path("/test/path"),
859
+ file_type=".csv",
860
+ data_desc="test users",
861
+ )
862
+
863
+ # Verify calls
864
+ mock_clean_okta_output.assert_called_once_with(data=test_data, skip_keys=["_links"])
865
+ mock_save_data_to.assert_called_once_with(file=Path("/test/path/test_users_01012024.csv"), data=clean_data)
866
+ # Note: The actual function doesn't update progress in a way our mock can capture,
867
+ # but it's working correctly as shown by the logs
868
+
869
+ @patch(f"{PATH}.job_progress")
870
+ def test_check_and_save_data_no_data(self, mock_job_progress):
871
+ """Test check and save data function with no data"""
872
+ # Setup mocks
873
+ test_data = []
874
+
875
+ # Test the function
876
+ check_and_save_data(
877
+ data=test_data,
878
+ file_name="test_users",
879
+ file_path=Path("/test/path"),
880
+ file_type=".csv",
881
+ data_desc="test users",
882
+ )
883
+
884
+ # Should not enter the progress context since there's no data
885
+ mock_job_progress.__enter__.assert_not_called()
886
+
887
+ @patch(f"{PATH}.remove_nested_dict")
888
+ def test_clean_okta_output(self, mock_remove_nested_dict):
889
+ """Test cleaning Okta output data"""
890
+ # Setup test data
891
+ test_data = [
892
+ {
893
+ "id": "1",
894
+ "profile": {"login": "test@example.com", "email": "test@example.com"},
895
+ "_links": {"self": "https://okta.com/user/1"},
896
+ },
897
+ {
898
+ "id": "2",
899
+ "profile": {"login": "test2@example.com", "email": "test2@example.com"},
900
+ "credentials": {"password": {"value": "secret"}},
901
+ },
902
+ ]
903
+
904
+ # Setup mock return values
905
+ mock_remove_nested_dict.side_effect = [
906
+ {"id": "1", "login": "test@example.com", "email": "test@example.com"},
907
+ {"id": "2", "login": "test2@example.com", "email": "test2@example.com"},
908
+ ]
909
+
910
+ # Test the function
911
+ result = clean_okta_output(data=test_data, skip_keys=["_links"])
912
+
913
+ # Verify result structure
914
+ assert isinstance(result, dict)
915
+ assert len(result) == 2
916
+ assert 0 in result
917
+ assert 1 in result
918
+
919
+ # Verify mock calls
920
+ assert mock_remove_nested_dict.call_count == 2
921
+
922
+ # Test Authentication Functions
923
+ @patch(f"{PATH}.error_and_exit")
924
+ def test_authenticate_with_okta_ssws_success(self, mock_error_and_exit):
925
+ """Test SSWS authentication success"""
926
+ # Setup mocks
927
+ mock_app = MagicMock(spec=Application)
928
+ mock_app.config = {"oktaUrl": "https://test.okta.com", "oktaApiToken": "SSWS test-token"}
929
+
930
+ mock_api = MagicMock(spec=Api)
931
+ mock_response = MagicMock()
932
+ mock_response.ok = True
933
+ mock_api.get.return_value = mock_response
934
+
935
+ # Test the function
936
+ authenticate_with_okta(app=mock_app, api=mock_api, type="ssws")
937
+
938
+ # Verify API call was made
939
+ mock_api.get.assert_called_once_with(
940
+ url="https://test.okta.com/api/v1/users",
941
+ headers={
942
+ "Content-Type": 'application/json; okta-response="omitCredentials, omitCredentialsLinks"',
943
+ "Accept": "application/json",
944
+ "Authorization": "SSWS test-token",
945
+ },
946
+ )
947
+ mock_error_and_exit.assert_not_called()
948
+
949
+ @patch(f"{PATH}.error_and_exit")
950
+ def test_authenticate_with_okta_ssws_failure(self, mock_error_and_exit):
951
+ """Test SSWS authentication failure"""
952
+ # Setup mocks
953
+ mock_app = MagicMock(spec=Application)
954
+ mock_app.config = {"oktaUrl": "https://test.okta.com", "oktaApiToken": "SSWS invalid-token"}
955
+
956
+ mock_api = MagicMock(spec=Api)
957
+ mock_response = MagicMock()
958
+ mock_response.ok = False
959
+ mock_api.get.return_value = mock_response
960
+
961
+ mock_error_and_exit.side_effect = SystemExit(1)
962
+
963
+ # Test the function
964
+ with pytest.raises(SystemExit):
965
+ authenticate_with_okta(app=mock_app, api=mock_api, type="ssws")
966
+
967
+ # Verify error was called
968
+ mock_error_and_exit.assert_called_once_with(
969
+ "Please verify SSWS Token from Okta is entered correctly in init.yaml, "
970
+ + "and it has okta.users.read & okta.roles.read permissions granted and try again."
971
+ )
972
+
973
+ @patch(f"{PATH}.get_okta_token")
974
+ def test_authenticate_with_okta_bearer_with_key(self, mock_get_okta_token):
975
+ """Test Bearer authentication with existing key"""
976
+ # Setup mocks
977
+ mock_app = MagicMock(spec=Application)
978
+ mock_app.config = {
979
+ "oktaSecretKey": {
980
+ "d": "test-key",
981
+ "p": "test-p",
982
+ "q": "test-q",
983
+ "dp": "test-dp",
984
+ "dq": "test-dq",
985
+ "qi": "test-qi",
986
+ "kty": "RSA",
987
+ "e": "AQAB",
988
+ "kid": "test-kid",
989
+ "n": "test-n",
990
+ }
991
+ }
992
+
993
+ mock_api = MagicMock(spec=Api)
994
+ mock_get_okta_token.return_value = "Bearer test-token"
995
+
996
+ # Test the function
997
+ authenticate_with_okta(app=mock_app, api=mock_api, type="bearer")
998
+
999
+ # Verify token generation was called
1000
+ mock_get_okta_token.assert_called_once_with(config=mock_app.config, api=mock_api, app=mock_app)
1001
+
1002
+ def test_authenticate_with_okta_bearer_no_key(self):
1003
+ """Test Bearer authentication without secret key"""
1004
+ # Setup mocks
1005
+ mock_app = MagicMock(spec=Application)
1006
+ mock_app.config = {} # No oktaSecretKey
1007
+ mock_app.save_config = MagicMock()
1008
+
1009
+ mock_api = MagicMock(spec=Api)
1010
+
1011
+ # Test the function
1012
+ authenticate_with_okta(app=mock_app, api=mock_api, type="bearer")
1013
+
1014
+ # Verify config was updated and saved
1015
+ mock_app.save_config.assert_called_once()
1016
+
1017
+ # Verify the config was updated with template values
1018
+ expected_key = {
1019
+ "d": "get from Okta",
1020
+ "p": "get from Okta",
1021
+ "q": "get from Okta",
1022
+ "dp": "get from Okta",
1023
+ "dq": "get from Okta",
1024
+ "qi": "get from Okta",
1025
+ "kty": "get from Okta",
1026
+ "e": "get from Okta",
1027
+ "kid": "get from Okta",
1028
+ "n": "get from Okta",
1029
+ }
1030
+ assert mock_app.config["oktaSecretKey"] == expected_key
1031
+ assert mock_app.config["oktaScopes"] == "okta.users.read okta.roles.read"
1032
+
1033
+ @patch(f"{PATH}.error_and_exit")
1034
+ def test_authenticate_with_okta_invalid_type(self, mock_error_and_exit):
1035
+ """Test authentication with invalid type"""
1036
+ # Setup mocks
1037
+ mock_app = MagicMock(spec=Application)
1038
+ mock_api = MagicMock(spec=Api)
1039
+ mock_error_and_exit.side_effect = SystemExit(1)
1040
+
1041
+ # Test the function
1042
+ with pytest.raises(SystemExit):
1043
+ authenticate_with_okta(app=mock_app, api=mock_api, type="invalid")
1044
+
1045
+ # Verify error was called
1046
+ mock_error_and_exit.assert_called_once_with(
1047
+ "Please enter a valid authentication type for Okta API and try again. Please choose from SSWS or Bearer."
1048
+ )
1049
+
1050
+ @patch(f"{PATH}.python_jwt.generate_jwt")
1051
+ @patch(f"{PATH}.jwk.JWK.from_json")
1052
+ @patch(f"{PATH}.time.time")
1053
+ @patch(f"{PATH}.error_and_exit")
1054
+ def test_get_okta_token_success(self, mock_error_and_exit, mock_time, mock_jwk_from_json, mock_generate_jwt):
1055
+ """Test getting Okta token successfully"""
1056
+ # Setup mocks
1057
+ mock_config = {
1058
+ "oktaSecretKey": {"kty": "RSA", "kid": "test-kid"},
1059
+ "oktaUrl": "https://test.okta.com/",
1060
+ "oktaClientId": "test-client-id",
1061
+ "oktaScopes": "okta.users.read okta.roles.read",
1062
+ }
1063
+
1064
+ mock_api = MagicMock(spec=Api)
1065
+ mock_app = MagicMock(spec=Application)
1066
+ mock_app.save_config = MagicMock()
1067
+
1068
+ # Mock time
1069
+ mock_time.return_value = 1640995200 # Fixed timestamp
1070
+
1071
+ # Mock JWK creation
1072
+ mock_jwk_token = MagicMock()
1073
+ mock_jwk_from_json.return_value = mock_jwk_token
1074
+
1075
+ # Mock JWT generation
1076
+ mock_generate_jwt.return_value = "signed-jwt-token"
1077
+
1078
+ # Mock API response
1079
+ mock_response = MagicMock()
1080
+ mock_response.status_code = 200
1081
+ mock_response.json.return_value = {"token_type": "Bearer", "access_token": "access-token-123"}
1082
+ mock_api.post.return_value = mock_response
1083
+
1084
+ # Test the function
1085
+ result = get_okta_token(config=mock_config, api=mock_api, app=mock_app)
1086
+
1087
+ # Verify result
1088
+ assert result == "Bearer access-token-123"
1089
+
1090
+ # Verify JWK creation
1091
+ mock_jwk_from_json.assert_called_once_with(json.dumps(mock_config["oktaSecretKey"]))
1092
+ mock_generate_jwt.assert_called_once()
1093
+
1094
+ # Verify API call
1095
+ expected_payload_str = (
1096
+ "grant_type=client_credentials&scope=okta.users.read okta.roles.read&client_assertion_type="
1097
+ + "urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=signed-jwt-token"
1098
+ )
1099
+ mock_api.post.assert_called_once_with(
1100
+ url="https://test.okta.com/oauth2/v1/token",
1101
+ headers={
1102
+ "Accept": "application/json",
1103
+ "Content-Type": "application/x-www-form-urlencoded",
1104
+ },
1105
+ data=expected_payload_str,
1106
+ )
1107
+
1108
+ # Verify config was updated and saved
1109
+ assert mock_config["oktaApiToken"] == "Bearer access-token-123"
1110
+ mock_app.save_config.assert_called_once_with(mock_config)
1111
+
1112
+ @patch(f"{PATH}.error_and_exit")
1113
+ def test_get_okta_token_api_error(self, mock_error_and_exit):
1114
+ """Test getting Okta token with API error"""
1115
+ # Setup mocks
1116
+ mock_config = {
1117
+ "oktaSecretKey": {"kty": "RSA"},
1118
+ "oktaUrl": "https://test.okta.com",
1119
+ "oktaClientId": "test-client-id",
1120
+ "oktaScopes": "okta.users.read",
1121
+ }
1122
+
1123
+ mock_api = MagicMock(spec=Api)
1124
+ mock_app = MagicMock(spec=Application)
1125
+
1126
+ # Mock API response with error
1127
+ mock_response = MagicMock()
1128
+ mock_response.status_code = 400
1129
+ mock_response.text = "Bad Request"
1130
+ mock_api.post.return_value = mock_response
1131
+
1132
+ mock_error_and_exit.side_effect = SystemExit(1)
1133
+
1134
+ # Mock other functions to avoid complex setup
1135
+ with patch(f"{PATH}.jwk.JWK.from_json"), patch(
1136
+ f"{PATH}.python_jwt.generate_jwt", return_value="test-jwt"
1137
+ ), patch(f"{PATH}.time.time", return_value=1640995200):
1138
+
1139
+ # Test the function
1140
+ with pytest.raises(SystemExit):
1141
+ get_okta_token(config=mock_config, api=mock_api, app=mock_app)
1142
+
1143
+ # Verify error was called
1144
+ mock_error_and_exit.assert_called_once()
1145
+ call_args = mock_error_and_exit.call_args[0][0]
1146
+ assert "Received unexpected response from Okta API" in call_args
1147
+ assert "400: Bad Request" in call_args
1148
+
1149
+ @patch(f"{PATH}.error_and_exit")
1150
+ def test_get_okta_token_json_decode_error(self, mock_error_and_exit):
1151
+ """Test getting Okta token with JSON decode error"""
1152
+ # Setup mocks
1153
+ mock_config = {
1154
+ "oktaSecretKey": {"kty": "RSA"},
1155
+ "oktaUrl": "https://test.okta.com",
1156
+ "oktaClientId": "test-client-id",
1157
+ "oktaScopes": "okta.users.read",
1158
+ }
1159
+
1160
+ mock_api = MagicMock(spec=Api)
1161
+ mock_app = MagicMock(spec=Application)
1162
+
1163
+ # Mock API response with JSON decode error
1164
+ mock_response = MagicMock()
1165
+ mock_response.status_code = 200
1166
+ mock_response.json.side_effect = JSONDecodeError("Invalid JSON", "doc", 0)
1167
+ mock_api.post.return_value = mock_response
1168
+
1169
+ mock_error_and_exit.side_effect = SystemExit(1)
1170
+
1171
+ # Mock other functions
1172
+ with patch(f"{PATH}.jwk.JWK.from_json"), patch(
1173
+ f"{PATH}.python_jwt.generate_jwt", return_value="test-jwt"
1174
+ ), patch(f"{PATH}.time.time", return_value=1640995200):
1175
+
1176
+ # Test the function
1177
+ with pytest.raises(SystemExit):
1178
+ get_okta_token(config=mock_config, api=mock_api, app=mock_app)
1179
+
1180
+ # Verify error was called
1181
+ mock_error_and_exit.assert_called_once_with("Unable to retrieve data from Okta API.")
1182
+
1183
+ @patch(f"{PATH}.remove_nested_dict")
1184
+ def test_clean_okta_output_integration(self, mock_remove_nested_dict):
1185
+ """Integration-style test for clean_okta_output function with mocked utility"""
1186
+ # Test data that mimics real Okta API response
1187
+ test_data = [
1188
+ {
1189
+ "id": "00u1a2b3c4d5e6f7g8h9",
1190
+ "status": "ACTIVE",
1191
+ "profile": {
1192
+ "firstName": "John",
1193
+ "lastName": "Doe",
1194
+ "email": "john.doe@example.com",
1195
+ "login": "john.doe@example.com",
1196
+ },
1197
+ "_links": {"self": {"href": "https://dev-123456.okta.com/api/v1/users/00u1a2b3c4d5e6f7g8h9"}},
1198
+ }
1199
+ ]
1200
+
1201
+ # Mock the utility function to return flattened data
1202
+ mock_remove_nested_dict.return_value = {
1203
+ "id": "00u1a2b3c4d5e6f7g8h9",
1204
+ "status": "ACTIVE",
1205
+ "profile_firstName": "John",
1206
+ "profile_lastName": "Doe",
1207
+ "profile_email": "john.doe@example.com",
1208
+ "profile_login": "john.doe@example.com",
1209
+ }
1210
+
1211
+ # Test the function
1212
+ result = clean_okta_output(data=test_data, skip_keys=["_links"])
1213
+
1214
+ # Verify the structure - keys should be integers
1215
+ assert isinstance(result, dict)
1216
+ assert len(result) == 1
1217
+ assert 0 in result
1218
+
1219
+ # Verify the utility function was called correctly
1220
+ mock_remove_nested_dict.assert_called_once_with(data=test_data[0], skip_keys=["_links"])
1221
+
1222
+ # Verify nested dicts were flattened and _links was removed
1223
+ user_data = result[0]
1224
+ assert "id" in user_data
1225
+ assert "status" in user_data
1226
+ assert "profile_firstName" in user_data
1227
+ assert "profile_login" in user_data
1228
+ assert "_links" not in user_data # Should be skipped