regscale-cli 6.25.0.1__py3-none-any.whl → 6.26.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/application.py +18 -3
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/integrations/commercial/sicura/api.py +14 -13
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +49 -39
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/synqly/assets.py +17 -0
- regscale/integrations/commercial/wizv2/click.py +26 -26
- regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
- regscale/integrations/commercial/wizv2/constants.py +20 -71
- regscale/integrations/commercial/wizv2/scanner.py +3 -3
- regscale/integrations/compliance_integration.py +67 -2
- regscale/integrations/control_matcher.py +358 -0
- regscale/integrations/due_date_handler.py +118 -6
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +1 -0
- regscale/integrations/public/cci_importer.py +37 -38
- regscale/integrations/public/fedramp/click.py +60 -2
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +199 -130
- regscale/models/integration_models/cisa_kev_data.json +199 -4
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +46 -21
- regscale/models/regscale_models/issue.py +256 -94
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/regscale_model.py +6 -1
- regscale/templates/__init__.py +0 -0
- regscale/utils/threading/threadhandler.py +20 -15
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +84 -37
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3731 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +1814 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +350 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1469 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +750 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/test_control_matcher.py +1314 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_issue.py +378 -1
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.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
|