intuned-runtime 1.0.0__py3-none-any.whl → 1.1.1__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.
Files changed (82) hide show
  1. intuned_cli/__init__.py +40 -0
  2. intuned_cli/commands/__init__.py +18 -0
  3. intuned_cli/commands/attempt_api_command.py +51 -0
  4. intuned_cli/commands/attempt_authsession_check_command.py +38 -0
  5. intuned_cli/commands/attempt_authsession_command.py +12 -0
  6. intuned_cli/commands/attempt_authsession_create_command.py +44 -0
  7. intuned_cli/commands/attempt_command.py +12 -0
  8. intuned_cli/commands/command.py +26 -0
  9. intuned_cli/commands/deploy_command.py +47 -0
  10. intuned_cli/commands/init_command.py +21 -0
  11. intuned_cli/commands/run_api_command.py +69 -0
  12. intuned_cli/commands/run_authsession_command.py +12 -0
  13. intuned_cli/commands/run_authsession_create_command.py +50 -0
  14. intuned_cli/commands/run_authsession_update_command.py +52 -0
  15. intuned_cli/commands/run_authsession_validate_command.py +49 -0
  16. intuned_cli/commands/run_command.py +12 -0
  17. intuned_cli/constants/__init__.py +1 -0
  18. intuned_cli/constants/readme.py +134 -0
  19. intuned_cli/controller/__test__/__init__.py +0 -0
  20. intuned_cli/controller/__test__/test_api.py +529 -0
  21. intuned_cli/controller/__test__/test_authsession.py +907 -0
  22. intuned_cli/controller/api.py +212 -0
  23. intuned_cli/controller/authsession.py +458 -0
  24. intuned_cli/controller/deploy.py +352 -0
  25. intuned_cli/controller/init.py +97 -0
  26. intuned_cli/types.py +33 -0
  27. intuned_cli/utils/api_helpers.py +32 -0
  28. intuned_cli/utils/auth_session_helpers.py +57 -0
  29. intuned_cli/utils/backend.py +5 -0
  30. intuned_cli/utils/confirmation.py +0 -0
  31. intuned_cli/utils/console.py +6 -0
  32. intuned_cli/utils/error.py +27 -0
  33. intuned_cli/utils/exclusions.py +40 -0
  34. intuned_cli/utils/get_auth_parameters.py +18 -0
  35. intuned_cli/utils/import_function.py +15 -0
  36. intuned_cli/utils/timeout.py +25 -0
  37. {cli → intuned_internal_cli}/__init__.py +1 -1
  38. {cli → intuned_internal_cli}/commands/__init__.py +2 -0
  39. {cli → intuned_internal_cli}/commands/ai_source/deploy.py +1 -1
  40. {cli → intuned_internal_cli}/commands/browser/save_state.py +2 -2
  41. {cli → intuned_internal_cli}/commands/project/auth_session/load.py +2 -2
  42. {cli → intuned_internal_cli}/commands/project/type_check.py +39 -32
  43. intuned_internal_cli/commands/root.py +15 -0
  44. {intuned_runtime-1.0.0.dist-info → intuned_runtime-1.1.1.dist-info}/METADATA +5 -1
  45. intuned_runtime-1.1.1.dist-info/RECORD +99 -0
  46. intuned_runtime-1.1.1.dist-info/entry_points.txt +4 -0
  47. runtime/__init__.py +2 -1
  48. runtime/backend_functions/_call_backend_function.py +0 -5
  49. runtime/browser/__init__.py +5 -2
  50. runtime/browser/helpers.py +21 -0
  51. runtime/browser/launch_browser.py +31 -0
  52. runtime/browser/launch_camoufox.py +61 -0
  53. runtime/browser/launch_chromium.py +64 -61
  54. runtime/browser/storage_state.py +11 -12
  55. runtime/env.py +4 -0
  56. runtime/errors/run_api_errors.py +14 -10
  57. runtime/run/playwright_constructs.py +6 -5
  58. runtime/run/pydantic_encoder.py +15 -0
  59. runtime/run/run_api.py +5 -4
  60. runtime/types/run_types.py +16 -0
  61. intuned_runtime-1.0.0.dist-info/RECORD +0 -58
  62. intuned_runtime-1.0.0.dist-info/entry_points.txt +0 -3
  63. {cli → intuned_internal_cli}/commands/ai_source/__init__.py +0 -0
  64. {cli → intuned_internal_cli}/commands/ai_source/ai_source.py +0 -0
  65. {cli → intuned_internal_cli}/commands/browser/__init__.py +0 -0
  66. {cli → intuned_internal_cli}/commands/init.py +0 -0
  67. {cli → intuned_internal_cli}/commands/project/__init__.py +0 -0
  68. {cli → intuned_internal_cli}/commands/project/auth_session/__init__.py +0 -0
  69. {cli → intuned_internal_cli}/commands/project/auth_session/check.py +0 -0
  70. {cli → intuned_internal_cli}/commands/project/auth_session/create.py +0 -0
  71. {cli → intuned_internal_cli}/commands/project/project.py +0 -0
  72. {cli → intuned_internal_cli}/commands/project/run.py +0 -0
  73. {cli → intuned_internal_cli}/commands/project/run_interface.py +0 -0
  74. {cli → intuned_internal_cli}/commands/project/upgrade.py +0 -0
  75. {cli → intuned_internal_cli}/commands/publish_packages.py +0 -0
  76. {cli → intuned_internal_cli}/logger.py +0 -0
  77. {cli → intuned_internal_cli}/utils/ai_source_project.py +0 -0
  78. {cli → intuned_internal_cli}/utils/code_tree.py +0 -0
  79. {cli → intuned_internal_cli}/utils/run_apis.py +0 -0
  80. {cli → intuned_internal_cli}/utils/unix_socket.py +0 -0
  81. {intuned_runtime-1.0.0.dist-info → intuned_runtime-1.1.1.dist-info}/LICENSE +0 -0
  82. {intuned_runtime-1.0.0.dist-info → intuned_runtime-1.1.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,134 @@
1
+ readme = r"""# Intuned CLI
2
+
3
+ ## Development Commands
4
+
5
+ For each command, add `--help` to see more details and options.
6
+
7
+ ### Initialize a Project
8
+ `pipx run --spec intuned-runtime intuned init`
9
+
10
+ Once you install the dependencies, you will have `intuned` command available in your environment.
11
+
12
+ ### Run an API
13
+ `intuned run api <api-name> <parameters>`
14
+
15
+
16
+ ### Deploy a Project
17
+ `intuned deploy [project-name]`
18
+
19
+
20
+
21
+ ### Create an auth session
22
+ `intuned run authsession create <parameters>`
23
+
24
+
25
+
26
+ ### Validate an auth session
27
+ `intuned run authsession validate <auth-session-name>`
28
+
29
+ ## Configuration
30
+
31
+ ### Environment Variables and Settings
32
+ - `workspaceId`: Your Intuned workspace ID ([How to get your workspaceId](https://docs.intunedhq.com/docs/guides/platform/how-to-get-a-workspace-id))
33
+ - Set in `intuned.json` file under the `workspaceId` property
34
+ - Or provide via CLI with `--workspace-id` flag during deployment
35
+
36
+ - `projectName`: The name of your Intuned project
37
+ - Set in `intuned.json` file under the `projectName` property
38
+ - Or override via command line when deploying with `yarn intuned deploy my-project-name` or `npm run intuned deploy my-project-name`
39
+
40
+ - `INTUNED_API_KEY`: Your Intuned API key
41
+ - Set as an environment variable: `export INTUNED_API_KEY=your_api_key_here`
42
+ - Or include in your .env file for development
43
+ - Or provide via CLI with `--api-key` flag during deployment
44
+
45
+ ## Project Structure
46
+
47
+ ### Generated Artifacts
48
+ - `./intuned.json`: Project configuration file
49
+ - `./api`: Folder containing API implementation files
50
+ - `./auth-sessions`: Folder containing auth-session APIs if you use them
51
+ - `./auth-sessions-instances`: Folder containing auth session instances
52
+
53
+ ## Types of auth sessions
54
+ - `MANUAL`: Manual auth session, records the session using a recorder and stores it in the `auth-sessions-instances` folder
55
+ - `API`: Auth session created via create API, stores the session in the `auth-sessions-instances` folder
56
+
57
+ ### Notes
58
+ - All commands should be run from the project root directory
59
+ - Verify you're in the correct location by confirming the presence of package.json and intuned.json
60
+ - Running commands from subdirectories may result in errors
61
+ - You can manage your deployed projects through the Intuned platform
62
+
63
+ ## `Intuned.json` Reference
64
+ ```jsonc
65
+ {
66
+ // Your Intuned workspace ID.
67
+ // Optional - If not provided here, it must be supplied via the \`--workspace-id\` flag during deployment.
68
+ "workspaceId": "your_workspace_id",
69
+
70
+ // The name of your Intuned project.
71
+ // Optional - If not provided here, it must be supplied via the command line when deploying.
72
+ "projectName": "your_project_name",
73
+
74
+ // Replication settings
75
+ "replication": {
76
+ // The maximum number of concurrent executions allowed via Intuned API. This does not affect jobs.
77
+ // A number of machines equal to this will be allocated to handle API requests.
78
+ // Not applicable if api access is disabled.
79
+ "maxConcurrentRequests": 1,
80
+
81
+ // The machine size to use for this project. This is applicable for both API requests and jobs.
82
+ // "standard": Standard machine size (6 shared vCPUs, 2GB RAM)
83
+ // "large": Large machine size (8 shared vCPUs, 4GB RAM)
84
+ // "xlarge": Extra large machine size (1 performance vCPU, 8GB RAM)
85
+ "size": "standard"
86
+ }
87
+
88
+ // Auth session settings
89
+ "authSessions": {
90
+ // Whether auth sessions are enabled for this project.
91
+ // If enabled, "auth-sessions/check.py" API must be implemented to validate the auth session.
92
+ "enabled": true,
93
+
94
+ // Whether to save Playwright traces for auth session runs.
95
+ "saveTraces": false,
96
+
97
+ // The type of auth session to use.
98
+ // "API" type requires implementing "auth-sessions/create.py" API to create/recreate the auth session programmatically.
99
+ // "MANUAL" type uses a recorder to manually create the auth session.
100
+ "type": "API",
101
+
102
+ // Recorder start URL for the recorder to navigate to when creating the auth session.
103
+ // Required if "type" is "MANUAL". Not used if "type" is "API".
104
+ "startUrl": "https://example.com/login",
105
+
106
+ // Recorder finish URL for the recorder. Once this URL is reached, the recorder stops and saves the auth session.
107
+ // Required if "type" is "MANUAL". Not used if "type" is "API".
108
+ "finishUrl": "https://example.com/dashboard",
109
+
110
+ // Recorder browser mode
111
+ // "fullscreen": Launches the browser in fullscreen mode.
112
+ // "kiosk": Launches the browser in kiosk mode (no address bar, no navigation controls).
113
+ // Only applicable for "MANUAL" type.
114
+ "browserMode": "fullscreen"
115
+ }
116
+
117
+ // API access settings
118
+ "apiAccess": {
119
+ // Whether to enable consumption through Intuned API. If this is false, the project can only be consumed through jobs.
120
+ // This is required for projects that use auth sessions.
121
+ "enabled": true
122
+ },
123
+
124
+ // Whether to run the deployed API in a headful browser. Running in headful can help with some anti-bot detections. However, it requires more resources and may work slower or crash if the machine size is "standard".
125
+ "headful": false,
126
+
127
+ // The region where your Intuned project is hosted.
128
+ // For a list of available regions, contact support or refer to the documentation.
129
+ // Optional - Default: "us"
130
+ "region": "us"
131
+ }
132
+ ```
133
+
134
+ """
File without changes
@@ -0,0 +1,529 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+ from typing import Generator
4
+ from unittest.mock import AsyncMock
5
+ from unittest.mock import Mock
6
+ from unittest.mock import patch
7
+
8
+ import pytest
9
+
10
+ from runtime.types.run_types import PayloadToAppend
11
+ from runtime.types.run_types import StorageState
12
+
13
+
14
+ def get_mock_console():
15
+ """Create a mock console that tracks calls."""
16
+ mock_console = Mock()
17
+ mock_console.print = Mock()
18
+ return mock_console
19
+
20
+
21
+ class AsyncContextManagerMock:
22
+ def __init__(self):
23
+ self.aenter_called = False
24
+ self.aexit_called = False
25
+ self.exception_info = None
26
+
27
+ async def __aenter__(self):
28
+ self.aenter_called = True
29
+ return self
30
+
31
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any):
32
+ self.aexit_called = True
33
+ self.exception_info = (exc_type, exc_val, exc_tb)
34
+ # Return False/None to propagate exceptions
35
+ return False
36
+
37
+
38
+ @dataclass
39
+ class SharedMocks:
40
+ console: Mock
41
+ assert_api_file_exists: AsyncMock
42
+ register_get_auth_session_parameters: AsyncMock
43
+
44
+
45
+ @pytest.fixture(autouse=True)
46
+ def shared_mocks() -> Generator[SharedMocks, Any, None]:
47
+ """Mock dependencies for API controller tests."""
48
+ _mock_console_patch = patch("intuned_cli.controller.api.console", get_mock_console())
49
+ _mock_assert_api_patch = patch("intuned_cli.controller.api.assert_api_file_exists", new_callable=AsyncMock)
50
+ _mock_register_auth_patch = patch("intuned_cli.controller.api.register_get_auth_session_parameters")
51
+
52
+ with (
53
+ _mock_console_patch,
54
+ _mock_assert_api_patch as mock_assert_api,
55
+ _mock_register_auth_patch as mock_register_auth,
56
+ ):
57
+ # Setup default return values
58
+ mock_assert_api.return_value = None
59
+ mock_register_auth.return_value = None
60
+
61
+ yield SharedMocks(
62
+ console=get_mock_console(),
63
+ assert_api_file_exists=mock_assert_api,
64
+ register_get_auth_session_parameters=mock_register_auth,
65
+ )
66
+
67
+
68
+ @dataclass
69
+ class AttemptApiMocks:
70
+ extendable_timeout: AsyncMock
71
+ run_api: AsyncMock
72
+
73
+
74
+ @pytest.fixture
75
+ def attempt_api_mocks() -> Generator[AttemptApiMocks, Any, None]:
76
+ """Mock dependencies for attempt_api tests."""
77
+ _mock_timeout_patch = patch("intuned_cli.controller.api.extendable_timeout")
78
+ _mock_run_api_patch = patch("intuned_cli.controller.api.run_api", new_callable=AsyncMock)
79
+
80
+ with (
81
+ _mock_timeout_patch as mock_timeout,
82
+ _mock_run_api_patch as mock_run_api,
83
+ ):
84
+ # Setup default return values
85
+ mock_timeout.return_value = AsyncContextManagerMock()
86
+
87
+ # Mock run_api to return success by default
88
+ mock_result = Mock()
89
+ mock_result.result = "test_result"
90
+ mock_result.payload_to_append = []
91
+ mock_run_api.return_value = mock_result
92
+
93
+ yield AttemptApiMocks(
94
+ extendable_timeout=mock_timeout,
95
+ run_api=mock_run_api,
96
+ )
97
+
98
+
99
+ @dataclass
100
+ class ExecuteCLIMocks:
101
+ attempt_api: AsyncMock
102
+ execute_run_validate_auth_session_cli: AsyncMock
103
+ write_results_to_file: AsyncMock
104
+
105
+
106
+ @pytest.fixture
107
+ def execute_cli_mocks() -> Generator[ExecuteCLIMocks, Any, None]:
108
+ """Mock dependencies for execute_*_cli tests."""
109
+ _mock_validate_auth_patch = patch(
110
+ "intuned_cli.controller.api.execute_run_validate_auth_session_cli", new_callable=AsyncMock
111
+ )
112
+ _mock_run_api_patch = patch("intuned_cli.controller.api.attempt_api", new_callable=AsyncMock)
113
+ _mock_write_results_to_file_patch = patch(
114
+ "intuned_cli.controller.api.write_results_to_file", new_callable=AsyncMock
115
+ )
116
+
117
+ with (
118
+ _mock_validate_auth_patch as mock_validate_auth,
119
+ _mock_run_api_patch as mock_attempt_api,
120
+ _mock_write_results_to_file_patch as mock_write_results_to_file,
121
+ ):
122
+ # Setup default return values
123
+ mock_validate_auth.return_value = {"cookies": [], "storageState": {}}
124
+
125
+ # Mock run_api to return success by default
126
+ mock_attempt_api.return_value = ("test_result", [])
127
+
128
+ yield ExecuteCLIMocks(
129
+ execute_run_validate_auth_session_cli=mock_validate_auth,
130
+ attempt_api=mock_attempt_api,
131
+ write_results_to_file=mock_write_results_to_file,
132
+ )
133
+
134
+
135
+ class TestAttemptApi:
136
+ """Test suite for attempt_api function."""
137
+
138
+ @pytest.mark.asyncio
139
+ async def test_attempt_api_calls_timeout_middleware_with_timeout(self, attempt_api_mocks: AttemptApiMocks):
140
+ """Test that attempt_api calls timeout middleware with the correct timeout."""
141
+ # Import inside test to avoid circular import issues
142
+ from intuned_cli.controller.api import attempt_api
143
+
144
+ await attempt_api(
145
+ api_name="testApi",
146
+ parameters={},
147
+ headless=False,
148
+ timeout=6000,
149
+ )
150
+
151
+ attempt_api_mocks.extendable_timeout.assert_called_once_with(6000)
152
+
153
+ @pytest.mark.asyncio
154
+ async def test_attempt_api_calls_run_api_with_correct_parameters_and_parses_proxy(
155
+ self, attempt_api_mocks: AttemptApiMocks
156
+ ):
157
+ """Test that attempt_api calls run_api with correct parameters and parses proxy."""
158
+ from intuned_cli.controller.api import attempt_api
159
+ from runtime.types.run_types import ProxyConfig
160
+
161
+ with patch("intuned_cli.controller.api.ProxyConfig.parse_from_str") as mock_parse_proxy:
162
+ proxy_config = ProxyConfig(
163
+ username="user",
164
+ password="pass",
165
+ server="proxy-server",
166
+ )
167
+ mock_parse_proxy.return_value = proxy_config
168
+
169
+ parameters: dict[Any, Any] = {}
170
+ auth = StorageState(cookies=[], origins=[], session_storage=[])
171
+ await attempt_api(
172
+ api_name="testApi",
173
+ parameters=parameters,
174
+ headless=False,
175
+ auth=auth,
176
+ proxy="proxy",
177
+ timeout=999999999,
178
+ )
179
+
180
+ # mock_parse_proxy.assert_called_once_with("proxy")
181
+ attempt_api_mocks.run_api.assert_called_once()
182
+
183
+ # Verify the call arguments
184
+ call_args = attempt_api_mocks.run_api.call_args[0][0]
185
+ assert call_args.automation_function.name == "api/testApi"
186
+ assert call_args.automation_function.params is parameters
187
+ assert call_args.run_options.headless is False
188
+ assert call_args.run_options.proxy == proxy_config
189
+ assert call_args.auth.session.state is auth
190
+ assert call_args.auth.run_check is False
191
+
192
+ @pytest.mark.asyncio
193
+ async def test_attempt_api_returns_result_and_extended_payloads_if_run_api_succeeds(
194
+ self, attempt_api_mocks: AttemptApiMocks
195
+ ):
196
+ """Test that attempt_api returns the result and extended payloads when run_api succeeds."""
197
+ from intuned_cli.controller.api import attempt_api
198
+
199
+ expected_result = {}
200
+ expected_payload_to_append = []
201
+
202
+ mock_result = Mock()
203
+ mock_result.result = expected_result
204
+ mock_result.payload_to_append = expected_payload_to_append
205
+ attempt_api_mocks.run_api.return_value = mock_result
206
+
207
+ result, payloads = await attempt_api(
208
+ api_name="testApi",
209
+ parameters="inputData",
210
+ headless=False,
211
+ timeout=999999999,
212
+ )
213
+
214
+ assert result is expected_result
215
+ assert payloads is expected_payload_to_append
216
+
217
+ @pytest.mark.asyncio
218
+ async def test_attempt_api_throws_error_when_run_api_fails_with_error(self, attempt_api_mocks: AttemptApiMocks):
219
+ """Test that attempt_api throws the error when run_api fails."""
220
+ from intuned_cli.controller.api import attempt_api
221
+
222
+ error = Exception("runApi failed")
223
+ attempt_api_mocks.run_api.side_effect = error
224
+
225
+ with pytest.raises(Exception, match="runApi failed"):
226
+ await attempt_api(
227
+ api_name="testApi",
228
+ parameters={},
229
+ headless=False,
230
+ timeout=999999999,
231
+ )
232
+
233
+
234
+ class TestExecuteRunApiCli:
235
+ """Test suite for API controller functions."""
236
+
237
+ @pytest.mark.asyncio
238
+ async def test_execute_run_api_cli_calls_attempt_api_once_if_success(self, execute_cli_mocks: ExecuteCLIMocks):
239
+ """Test that execute_run_api_cli calls attemptApi once if successful."""
240
+ from intuned_cli.controller.api import execute_run_api_cli
241
+
242
+ await execute_run_api_cli(
243
+ api_name="testApi",
244
+ input_data={},
245
+ retries=3,
246
+ headless=False,
247
+ timeout=30,
248
+ )
249
+
250
+ execute_cli_mocks.attempt_api.assert_called_once()
251
+
252
+ @pytest.mark.asyncio
253
+ async def test_execute_run_api_cli_stops_retrying_after_max_retries(self, execute_cli_mocks: ExecuteCLIMocks):
254
+ """Test that execute_run_api_cli stops retrying after max retries."""
255
+ from intuned_cli.controller.api import execute_run_api_cli
256
+ from intuned_cli.utils.error import CLIError
257
+ from runtime.errors.run_api_errors import AutomationError
258
+
259
+ execute_cli_mocks.attempt_api.side_effect = AutomationError(Exception("runApi failed"))
260
+
261
+ with pytest.raises(CLIError):
262
+ await execute_run_api_cli(
263
+ api_name="testApi",
264
+ input_data={},
265
+ retries=10,
266
+ headless=False,
267
+ timeout=30,
268
+ )
269
+
270
+ assert execute_cli_mocks.attempt_api.call_count == 10
271
+
272
+ @pytest.mark.asyncio
273
+ async def test_execute_run_api_cli_stops_retrying_on_non_automation_errors(
274
+ self, execute_cli_mocks: ExecuteCLIMocks
275
+ ):
276
+ """Test that execute_run_api_cli stops retrying on non-automation errors."""
277
+ from intuned_cli.controller.api import execute_run_api_cli
278
+
279
+ execute_cli_mocks.attempt_api.side_effect = Exception("runApi failed")
280
+
281
+ with pytest.raises(Exception, match="runApi failed"):
282
+ await execute_run_api_cli(
283
+ api_name="testApi",
284
+ input_data={},
285
+ retries=3,
286
+ headless=False,
287
+ timeout=30,
288
+ )
289
+
290
+ execute_cli_mocks.attempt_api.assert_called_once()
291
+
292
+ @pytest.mark.asyncio
293
+ async def test_execute_run_api_cli_stops_retrying_on_success(self, execute_cli_mocks: ExecuteCLIMocks):
294
+ """Test that execute_run_api_cli stops retrying on success."""
295
+ from intuned_cli.controller.api import execute_run_api_cli
296
+ from runtime.errors.run_api_errors import AutomationError
297
+
298
+ execute_cli_mocks.attempt_api.side_effect = [
299
+ AutomationError(Exception("runApi failed")),
300
+ ("success", []),
301
+ ]
302
+
303
+ await execute_run_api_cli(
304
+ api_name="testApi",
305
+ input_data={},
306
+ retries=10,
307
+ headless=False,
308
+ timeout=30,
309
+ )
310
+
311
+ assert execute_cli_mocks.attempt_api.call_count == 2
312
+
313
+ @pytest.mark.asyncio
314
+ async def test_execute_run_api_cli_validates_auth_session_before_each_attempt_if_provided(
315
+ self, execute_cli_mocks: ExecuteCLIMocks
316
+ ):
317
+ """Test that execute_run_api_cli validates auth session before each attempt if provided."""
318
+ with patch(
319
+ "intuned_cli.controller.api.execute_run_validate_auth_session_cli", new_callable=AsyncMock
320
+ ) as mock_validate_auth:
321
+ from intuned_cli.controller.api import AuthSessionInput
322
+ from intuned_cli.controller.api import execute_run_api_cli
323
+ from runtime.errors.run_api_errors import AutomationError
324
+
325
+ execute_cli_mocks.attempt_api.side_effect = [
326
+ AutomationError(Exception("runApi failed")),
327
+ ("success", []),
328
+ ]
329
+
330
+ auth_session = AuthSessionInput(
331
+ id="authSessionId",
332
+ auto_recreate=False,
333
+ check_retries=1,
334
+ create_retries=2,
335
+ )
336
+
337
+ await execute_run_api_cli(
338
+ api_name="testApi",
339
+ input_data={},
340
+ auth_session=auth_session,
341
+ retries=10,
342
+ headless=False,
343
+ timeout=30,
344
+ )
345
+
346
+ assert mock_validate_auth.call_count == 2
347
+ # Verify the call arguments
348
+ call_args = mock_validate_auth.call_args
349
+ assert call_args[1]["id"] == "authSessionId"
350
+ assert call_args[1]["auto_recreate"] is False
351
+ assert call_args[1]["check_retries"] == 1
352
+ assert call_args[1]["create_retries"] == 2
353
+
354
+ @pytest.mark.asyncio
355
+ async def test_execute_run_api_cli_doesnt_validate_auth_session_if_not_provided(
356
+ self, execute_cli_mocks: ExecuteCLIMocks
357
+ ):
358
+ """Test that execute_run_api_cli doesn't validate auth session if not provided."""
359
+ from intuned_cli.controller.api import execute_run_api_cli
360
+
361
+ await execute_run_api_cli(
362
+ api_name="testApi",
363
+ input_data={},
364
+ retries=1,
365
+ headless=False,
366
+ timeout=30,
367
+ )
368
+
369
+ execute_cli_mocks.execute_run_validate_auth_session_cli.assert_not_called()
370
+
371
+ @pytest.mark.asyncio
372
+ async def test_execute_run_api_cli_fails_if_auth_session_is_provided_but_not_valid(
373
+ self, execute_cli_mocks: ExecuteCLIMocks
374
+ ):
375
+ """Test that execute_run_api_cli fails if auth session is provided but not valid."""
376
+ from intuned_cli.controller.api import AuthSessionInput
377
+ from intuned_cli.controller.api import execute_run_api_cli
378
+ from intuned_cli.utils.error import CLIError
379
+
380
+ execute_cli_mocks.execute_run_validate_auth_session_cli.side_effect = CLIError("Auth session validation failed")
381
+
382
+ auth_session = AuthSessionInput(
383
+ id="authSessionId",
384
+ auto_recreate=False,
385
+ check_retries=1,
386
+ create_retries=2,
387
+ )
388
+
389
+ with pytest.raises(CLIError, match="Auth session validation failed"):
390
+ await execute_run_api_cli(
391
+ api_name="testApi",
392
+ input_data={},
393
+ auth_session=auth_session,
394
+ retries=10,
395
+ headless=False,
396
+ timeout=30,
397
+ )
398
+
399
+ # Verify auth validation was called
400
+ call_args = execute_cli_mocks.execute_run_validate_auth_session_cli.call_args
401
+ assert call_args[1]["id"] == "authSessionId"
402
+ assert call_args[1]["auto_recreate"] is False
403
+ assert call_args[1]["check_retries"] == 1
404
+ assert call_args[1]["create_retries"] == 2
405
+
406
+ execute_cli_mocks.attempt_api.assert_not_called()
407
+
408
+ @pytest.mark.asyncio
409
+ async def test_execute_run_api_cli_writes_result_to_file_if_output_file_is_provided(
410
+ self, shared_mocks: SharedMocks, execute_cli_mocks: ExecuteCLIMocks
411
+ ):
412
+ """Test that execute_run_api_cli writes result to file if outputFile is provided."""
413
+
414
+ from intuned_cli.controller.api import execute_run_api_cli
415
+ from runtime.types.run_types import PayloadToAppend
416
+
417
+ execute_cli_mocks.attempt_api.return_value = (
418
+ "test_result",
419
+ [PayloadToAppend(api_name="test_api", parameters={"key": "value"})],
420
+ )
421
+
422
+ await execute_run_api_cli(
423
+ api_name="testApi",
424
+ input_data={},
425
+ output_file="output.json",
426
+ retries=1,
427
+ headless=False,
428
+ timeout=30,
429
+ )
430
+
431
+ execute_cli_mocks.write_results_to_file.assert_called_once()
432
+ call_args = execute_cli_mocks.write_results_to_file.call_args.kwargs
433
+ assert str(call_args["file_path"]) == "output.json"
434
+ assert call_args["result"] == "test_result"
435
+ assert call_args["extended_payloads"] == [PayloadToAppend(api_name="test_api", parameters={"key": "value"})]
436
+
437
+ @pytest.mark.asyncio
438
+ async def test_execute_run_api_cli_asserts_api_file_exists(
439
+ self, shared_mocks: SharedMocks, execute_cli_mocks: ExecuteCLIMocks
440
+ ):
441
+ """Test that execute_run_api_cli asserts API file exists."""
442
+ from intuned_cli.controller.api import execute_run_api_cli
443
+
444
+ await execute_run_api_cli(
445
+ api_name="testApi",
446
+ input_data={},
447
+ retries=1,
448
+ headless=False,
449
+ timeout=30,
450
+ )
451
+
452
+ shared_mocks.assert_api_file_exists.assert_called_once_with("api", "testApi")
453
+
454
+
455
+ class TestExecuteAttemptApiCli:
456
+ """Test suite for execute_attempt_api_cli function."""
457
+
458
+ @pytest.mark.asyncio
459
+ async def test_execute_attempt_api_cli_calls_attempt_api_once(self, execute_cli_mocks: ExecuteCLIMocks):
460
+ """Test that executeAttemptApiCLI calls attemptApi once."""
461
+ from intuned_cli.controller.api import execute_attempt_api_cli
462
+ from runtime.errors.run_api_errors import AutomationError
463
+
464
+ await execute_attempt_api_cli(
465
+ api_name="testApi",
466
+ input_data={},
467
+ headless=False,
468
+ timeout=30,
469
+ )
470
+
471
+ execute_cli_mocks.attempt_api.assert_called_once()
472
+
473
+ # Test that it throws AutomationError when run_api fails
474
+ execute_cli_mocks.attempt_api.reset_mock()
475
+ execute_cli_mocks.attempt_api.side_effect = AutomationError(Exception("runApi failed"))
476
+
477
+ with pytest.raises(AutomationError):
478
+ await execute_attempt_api_cli(
479
+ api_name="testApi",
480
+ input_data={},
481
+ headless=False,
482
+ timeout=30,
483
+ )
484
+
485
+ execute_cli_mocks.attempt_api.assert_called_once()
486
+
487
+ @pytest.mark.asyncio
488
+ async def test_execute_attempt_api_cli_writes_result_to_file_if_output_file_is_provided(
489
+ self, shared_mocks: SharedMocks, execute_cli_mocks: ExecuteCLIMocks
490
+ ):
491
+ """Test that executeAttemptApiCLI writes result to file if outputFile is provided."""
492
+
493
+ from intuned_cli.controller.api import execute_attempt_api_cli
494
+
495
+ execute_cli_mocks.attempt_api.return_value = (
496
+ "test_result",
497
+ [PayloadToAppend(api_name="test_api", parameters={"key": "value"})],
498
+ )
499
+
500
+ await execute_attempt_api_cli(
501
+ api_name="testApi",
502
+ input_data={},
503
+ output_file="output.json",
504
+ headless=False,
505
+ timeout=30,
506
+ )
507
+
508
+ # Verify file was written
509
+ execute_cli_mocks.write_results_to_file.assert_called_once()
510
+ call_args = execute_cli_mocks.write_results_to_file.call_args.kwargs
511
+ assert str(call_args["file_path"]) == "output.json"
512
+ assert call_args["result"] == "test_result"
513
+ assert call_args["extended_payloads"] == [PayloadToAppend(api_name="test_api", parameters={"key": "value"})]
514
+
515
+ @pytest.mark.asyncio
516
+ async def test_execute_attempt_api_cli_asserts_api_file_exists(
517
+ self, shared_mocks: SharedMocks, execute_cli_mocks: ExecuteCLIMocks
518
+ ):
519
+ """Test that executeAttemptApiCLI asserts API file exists."""
520
+ from intuned_cli.controller.api import execute_attempt_api_cli
521
+
522
+ await execute_attempt_api_cli(
523
+ api_name="testApi",
524
+ input_data={},
525
+ headless=False,
526
+ timeout=30,
527
+ )
528
+
529
+ shared_mocks.assert_api_file_exists.assert_called_once_with("api", "testApi")