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.
- intuned_cli/__init__.py +40 -0
- intuned_cli/commands/__init__.py +18 -0
- intuned_cli/commands/attempt_api_command.py +51 -0
- intuned_cli/commands/attempt_authsession_check_command.py +38 -0
- intuned_cli/commands/attempt_authsession_command.py +12 -0
- intuned_cli/commands/attempt_authsession_create_command.py +44 -0
- intuned_cli/commands/attempt_command.py +12 -0
- intuned_cli/commands/command.py +26 -0
- intuned_cli/commands/deploy_command.py +47 -0
- intuned_cli/commands/init_command.py +21 -0
- intuned_cli/commands/run_api_command.py +69 -0
- intuned_cli/commands/run_authsession_command.py +12 -0
- intuned_cli/commands/run_authsession_create_command.py +50 -0
- intuned_cli/commands/run_authsession_update_command.py +52 -0
- intuned_cli/commands/run_authsession_validate_command.py +49 -0
- intuned_cli/commands/run_command.py +12 -0
- intuned_cli/constants/__init__.py +1 -0
- intuned_cli/constants/readme.py +134 -0
- intuned_cli/controller/__test__/__init__.py +0 -0
- intuned_cli/controller/__test__/test_api.py +529 -0
- intuned_cli/controller/__test__/test_authsession.py +907 -0
- intuned_cli/controller/api.py +212 -0
- intuned_cli/controller/authsession.py +458 -0
- intuned_cli/controller/deploy.py +352 -0
- intuned_cli/controller/init.py +97 -0
- intuned_cli/types.py +33 -0
- intuned_cli/utils/api_helpers.py +32 -0
- intuned_cli/utils/auth_session_helpers.py +57 -0
- intuned_cli/utils/backend.py +5 -0
- intuned_cli/utils/confirmation.py +0 -0
- intuned_cli/utils/console.py +6 -0
- intuned_cli/utils/error.py +27 -0
- intuned_cli/utils/exclusions.py +40 -0
- intuned_cli/utils/get_auth_parameters.py +18 -0
- intuned_cli/utils/import_function.py +15 -0
- intuned_cli/utils/timeout.py +25 -0
- {cli → intuned_internal_cli}/__init__.py +1 -1
- {cli → intuned_internal_cli}/commands/__init__.py +2 -0
- {cli → intuned_internal_cli}/commands/ai_source/deploy.py +1 -1
- {cli → intuned_internal_cli}/commands/browser/save_state.py +2 -2
- {cli → intuned_internal_cli}/commands/project/auth_session/load.py +2 -2
- {cli → intuned_internal_cli}/commands/project/type_check.py +39 -32
- intuned_internal_cli/commands/root.py +15 -0
- {intuned_runtime-1.0.0.dist-info → intuned_runtime-1.1.1.dist-info}/METADATA +5 -1
- intuned_runtime-1.1.1.dist-info/RECORD +99 -0
- intuned_runtime-1.1.1.dist-info/entry_points.txt +4 -0
- runtime/__init__.py +2 -1
- runtime/backend_functions/_call_backend_function.py +0 -5
- runtime/browser/__init__.py +5 -2
- runtime/browser/helpers.py +21 -0
- runtime/browser/launch_browser.py +31 -0
- runtime/browser/launch_camoufox.py +61 -0
- runtime/browser/launch_chromium.py +64 -61
- runtime/browser/storage_state.py +11 -12
- runtime/env.py +4 -0
- runtime/errors/run_api_errors.py +14 -10
- runtime/run/playwright_constructs.py +6 -5
- runtime/run/pydantic_encoder.py +15 -0
- runtime/run/run_api.py +5 -4
- runtime/types/run_types.py +16 -0
- intuned_runtime-1.0.0.dist-info/RECORD +0 -58
- intuned_runtime-1.0.0.dist-info/entry_points.txt +0 -3
- {cli → intuned_internal_cli}/commands/ai_source/__init__.py +0 -0
- {cli → intuned_internal_cli}/commands/ai_source/ai_source.py +0 -0
- {cli → intuned_internal_cli}/commands/browser/__init__.py +0 -0
- {cli → intuned_internal_cli}/commands/init.py +0 -0
- {cli → intuned_internal_cli}/commands/project/__init__.py +0 -0
- {cli → intuned_internal_cli}/commands/project/auth_session/__init__.py +0 -0
- {cli → intuned_internal_cli}/commands/project/auth_session/check.py +0 -0
- {cli → intuned_internal_cli}/commands/project/auth_session/create.py +0 -0
- {cli → intuned_internal_cli}/commands/project/project.py +0 -0
- {cli → intuned_internal_cli}/commands/project/run.py +0 -0
- {cli → intuned_internal_cli}/commands/project/run_interface.py +0 -0
- {cli → intuned_internal_cli}/commands/project/upgrade.py +0 -0
- {cli → intuned_internal_cli}/commands/publish_packages.py +0 -0
- {cli → intuned_internal_cli}/logger.py +0 -0
- {cli → intuned_internal_cli}/utils/ai_source_project.py +0 -0
- {cli → intuned_internal_cli}/utils/code_tree.py +0 -0
- {cli → intuned_internal_cli}/utils/run_apis.py +0 -0
- {cli → intuned_internal_cli}/utils/unix_socket.py +0 -0
- {intuned_runtime-1.0.0.dist-info → intuned_runtime-1.1.1.dist-info}/LICENSE +0 -0
- {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")
|