Skip to content

Commit a48e902

Browse files
Merge pull request #370 from UiPath/chore/cli-tests
chore: add tests for cli commands
2 parents 094be5d + cc87e09 commit a48e902

File tree

8 files changed

+530
-54
lines changed

8 files changed

+530
-54
lines changed

src/uipath/_cli/cli_run.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ def python_run_middleware(
4242
if not entrypoint:
4343
return MiddlewareResult(
4444
should_continue=False,
45-
info_message="""Error: No entrypoint specified. Please provide a path to a Python script.
45+
error_message="""No entrypoint specified. Please provide a path to a Python script.
4646
Usage: `uipath run <entrypoint_path> <input_arguments> [-f <input_json_file_path>]`""",
4747
)
4848

4949
if not os.path.exists(entrypoint):
5050
return MiddlewareResult(
5151
should_continue=False,
52-
error_message=f"""Error: Script not found at path {entrypoint}.
52+
error_message=f"""Script not found at path {entrypoint}.
5353
Usage: `uipath run <entrypoint_path> <input_arguments> [-f <input_json_file_path>]`""",
5454
)
5555

tests/cli/conftest.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import os
2+
3+
import pytest
4+
5+
from tests.cli.utils.project_details import ProjectDetails
6+
from tests.cli.utils.uipath_json import UiPathJson
7+
8+
9+
@pytest.fixture
10+
def mock_env_vars() -> dict[str, str]:
11+
"""Fixture to provide mock environment variables."""
12+
return {
13+
"UIPATH_URL": "https://cloud.uipath.com",
14+
"UIPATH_ACCESS_TOKEN": "mock_token",
15+
}
16+
17+
18+
@pytest.fixture
19+
def project_details() -> ProjectDetails:
20+
if os.path.isfile("mocks/pyproject.toml"):
21+
with open("mocks/pyproject.toml", "r") as file:
22+
data = file.read()
23+
else:
24+
with open("tests/cli/mocks/pyproject.toml", "r") as file:
25+
data = file.read()
26+
return ProjectDetails.from_toml(data)
27+
28+
29+
@pytest.fixture
30+
def uipath_json(request) -> UiPathJson:
31+
file_name = (
32+
"uipath-mock.json"
33+
if not hasattr(request, "param") or request.param is None
34+
else request.param
35+
)
36+
if os.path.isfile(f"mocks/{file_name}"):
37+
with open(f"mocks/{file_name}", "r") as file:
38+
data = file.read()
39+
else:
40+
with open(f"tests/cli/mocks/{file_name}", "r") as file:
41+
data = file.read()
42+
return UiPathJson.from_json(data)

tests/cli/mocks/simple_script.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from dataclasses import dataclass
2+
from typing import Optional
3+
4+
5+
@dataclass
6+
class EchoIn:
7+
message: str
8+
repeat: Optional[int] = 1
9+
prefix: Optional[str] = None
10+
11+
12+
@dataclass
13+
class EchoOut:
14+
message: str
15+
16+
17+
def main(input: EchoIn) -> EchoOut:
18+
result = []
19+
for _ in range(input.repeat): # type: ignore
20+
line = input.message
21+
if input.prefix:
22+
line = f"{input.prefix}: {line}"
23+
result.append(line)
24+
25+
return EchoOut(message="\n".join(result))
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"entryPoints": [
3+
{
4+
"filePath": "main.py",
5+
"uniqueId": "4181f581-4918-48df-88a0-456757b6bbea",
6+
"type": "agent",
7+
"input": {
8+
"type": "object",
9+
"properties": {
10+
"message": {
11+
"type": "string"
12+
},
13+
"repeat": {
14+
"type": "integer"
15+
},
16+
"prefix": {
17+
"type": "string"
18+
}
19+
},
20+
"required": [
21+
"message"
22+
]
23+
},
24+
"output": {
25+
"type": "object",
26+
"properties": {
27+
"message": {
28+
"type": "string"
29+
}
30+
},
31+
"required": [
32+
"message"
33+
]
34+
}
35+
}
36+
],
37+
"bindings": {
38+
"version": "2.0",
39+
"resources": []
40+
}
41+
}

tests/cli/test_invoke.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import json
2+
import os
3+
import urllib.parse
4+
import uuid
5+
from unittest.mock import patch
6+
7+
import pytest
8+
from click.testing import CliRunner
9+
from pytest_httpx import HTTPXMock
10+
11+
from tests.cli.utils.project_details import ProjectDetails
12+
from uipath._cli.cli_invoke import invoke # type: ignore
13+
from uipath._cli.middlewares import MiddlewareResult
14+
15+
16+
@pytest.fixture
17+
def entrypoint():
18+
return "entrypoint.py"
19+
20+
21+
def _create_env_file(mock_env_vars: dict[str, str]):
22+
"""Create the environment file."""
23+
with open(".env", "w") as f:
24+
for key, value in mock_env_vars.items():
25+
f.write(f"{key}={value}\n")
26+
27+
28+
class TestInvoke:
29+
class TestFileInput:
30+
def test_invoke_input_file_not_found(
31+
self,
32+
runner: CliRunner,
33+
temp_dir: str,
34+
entrypoint: str,
35+
):
36+
with runner.isolated_filesystem(temp_dir=temp_dir):
37+
result = runner.invoke(invoke, [entrypoint, "--file", "not-here.json"])
38+
assert result.exit_code != 0
39+
assert "Error: Invalid value for '-f' / '--file'" in result.output
40+
41+
def test_invoke_invalid_input_file(
42+
self,
43+
runner: CliRunner,
44+
temp_dir: str,
45+
entrypoint: str,
46+
):
47+
file_name = "not-json.txt"
48+
with runner.isolated_filesystem(temp_dir=temp_dir):
49+
file_path = os.path.join(temp_dir, file_name)
50+
with open(file_path, "w") as f:
51+
f.write("file content")
52+
result = runner.invoke(invoke, [entrypoint, "--file", file_path])
53+
assert result.exit_code == 1
54+
assert "Input file extension must be '.json'." in result.output
55+
56+
def test_invoke_successful(
57+
self,
58+
runner: CliRunner,
59+
temp_dir: str,
60+
entrypoint: str,
61+
mock_env_vars: dict[str, str],
62+
httpx_mock: HTTPXMock,
63+
project_details: ProjectDetails,
64+
):
65+
base_url = mock_env_vars.get("UIPATH_URL")
66+
job_key = uuid.uuid4()
67+
my_workspace_folder_id = "123"
68+
my_workspace_feed_id = "042e669a-c95f-46a3-87b0-9e5a98d7cf8a"
69+
release_id = "123"
70+
odata_top_filter = 25
71+
personal_workspace_response_data = {
72+
"PersonalWorskpaceFeedId": my_workspace_feed_id,
73+
"PersonalWorkspace": {"Id": my_workspace_folder_id},
74+
}
75+
list_release_response_data = {
76+
"value": [
77+
{
78+
"ProcessVersion": project_details.version,
79+
"Id": release_id,
80+
"Key": "9d17b737-1283-4ebe-b1f5-7d88967b94e4",
81+
}
82+
]
83+
}
84+
start_job_response_data = {
85+
"value": [{"Key": f"{job_key}", "other_key": "other_value"}]
86+
}
87+
# mock get release info
88+
httpx_mock.add_response(
89+
url=f"{base_url}/orchestrator_/odata/Releases/UiPath.Server.Configuration.OData.ListReleases?$select=Id,Key,ProcessVersion&$top={odata_top_filter}&$filter=ProcessKey%20eq%20%27{urllib.parse.quote(project_details.name)}%27",
90+
status_code=200,
91+
text=json.dumps(list_release_response_data),
92+
)
93+
# mock get personal workspace info
94+
httpx_mock.add_response(
95+
url=f"{base_url}/orchestrator_/odata/Users/UiPath.Server.Configuration.OData.GetCurrentUserExtended?$expand=PersonalWorkspace",
96+
status_code=200,
97+
text=json.dumps(personal_workspace_response_data),
98+
)
99+
# mock start job response
100+
httpx_mock.add_response(
101+
url=f"{base_url}/orchestrator_/odata/Jobs/UiPath.Server.Configuration.OData.StartJobs",
102+
status_code=201,
103+
text=json.dumps(start_job_response_data),
104+
)
105+
106+
file_name = "input.json"
107+
json_content = """
108+
{
109+
"input_key": "input_value"
110+
}"""
111+
112+
with runner.isolated_filesystem(temp_dir=temp_dir):
113+
_create_env_file(mock_env_vars)
114+
115+
file_path = os.path.join(temp_dir, file_name)
116+
with open(file_path, "w") as f:
117+
f.write(json_content)
118+
with open("pyproject.toml", "w") as f:
119+
f.write(project_details.to_toml())
120+
result = runner.invoke(invoke, [entrypoint, "{}"])
121+
assert result.exit_code == 0
122+
assert "Starting job ..." in result.output
123+
assert "Job started successfully!" in result.output
124+
assert (
125+
f"{base_url}/orchestrator_/jobs(sidepanel:sidepanel/jobs/{job_key}/details)?fid={my_workspace_folder_id}"
126+
in result.output
127+
)
128+
129+
def test_invoke_personal_workspace_info_error(
130+
self,
131+
runner: CliRunner,
132+
temp_dir: str,
133+
entrypoint: str,
134+
mock_env_vars: dict[str, str],
135+
httpx_mock: HTTPXMock,
136+
):
137+
base_url = mock_env_vars.get("UIPATH_URL")
138+
# mock start job response
139+
httpx_mock.add_response(
140+
url=f"{base_url}/orchestrator_/odata/Users/UiPath.Server.Configuration.OData.GetCurrentUserExtended?$expand=PersonalWorkspace",
141+
status_code=401,
142+
)
143+
144+
file_name = "input.json"
145+
json_content = """
146+
{
147+
"input_key": "input_value"
148+
}"""
149+
150+
with runner.isolated_filesystem(temp_dir=temp_dir):
151+
_create_env_file(mock_env_vars)
152+
153+
file_path = os.path.join(temp_dir, file_name)
154+
with open(file_path, "w") as f:
155+
f.write(json_content)
156+
with patch("uipath._cli.cli_run.Middlewares.next") as mock_middleware:
157+
mock_middleware.return_value = MiddlewareResult(
158+
should_continue=False,
159+
info_message="Execution succeeded",
160+
error_message=None,
161+
should_include_stacktrace=False,
162+
)
163+
result = runner.invoke(invoke, [entrypoint, "{}"])
164+
assert result.exit_code == 1
165+
assert (
166+
"Failed to fetch user info. Please try reauthenticating."
167+
in result.output
168+
)

tests/cli/test_publish.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,12 @@ def test_publish_no_env(
3232
self,
3333
runner: CliRunner,
3434
temp_dir: str,
35-
mock_feeds_response: list[tuple[str, str]],
3635
) -> None:
3736
"""Test publish command when no env file exists."""
3837
with runner.isolated_filesystem(temp_dir=temp_dir):
3938
result = runner.invoke(publish)
4039
assert result.exit_code == 1
41-
assert (
42-
"Missing required environment variables. Please check your .env file contains"
43-
in result.output
44-
)
40+
assert "Missing required environment variables." in result.output
4541
assert "UIPATH_URL" in result.output
4642
assert "UIPATH_ACCESS_TOKEN" in result.output
4743

@@ -124,12 +120,12 @@ def test_publish_error(
124120
with open(os.path.join(".uipath", "test.1.0.0.nupkg"), "wb") as f:
125121
f.write(b"dummy package content")
126122

127-
_create_env_file(mock_env_vars)
123+
_create_env_file(mock_env_vars)
128124

129-
result = runner.invoke(publish, ["--tenant"])
125+
result = runner.invoke(publish, ["--tenant"])
130126

131-
assert result.exit_code == 1
132-
assert "Failed to publish package. Status code: 401" in result.output
127+
assert result.exit_code == 1
128+
assert "Failed to publish package. Status code: 401" in result.output
133129

134130
def test_publish_tenant_feed_success(
135131
self,
@@ -151,12 +147,12 @@ def test_publish_tenant_feed_success(
151147
with open(os.path.join(".uipath", "test.1.0.0.nupkg"), "wb") as f:
152148
f.write(b"dummy package content")
153149

154-
_create_env_file(mock_env_vars)
150+
_create_env_file(mock_env_vars)
155151

156-
result = runner.invoke(publish, ["--tenant"])
152+
result = runner.invoke(publish, ["--tenant"])
157153

158-
assert result.exit_code == 0
159-
assert "Package published successfully!" in result.output
154+
assert result.exit_code == 0
155+
assert "Package published successfully!" in result.output
160156

161157
def test_publish_folder_feed_success(
162158
self,

0 commit comments

Comments
 (0)