Skip to content

Commit 1936e31

Browse files
committed
add folder name as part of workato init workflow
1 parent 4731fef commit 1936e31

File tree

5 files changed

+622
-66
lines changed

5 files changed

+622
-66
lines changed

src/workato_platform_cli/cli/commands/init.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
default="table",
4444
help="Output format: table (default) or json (only with --non-interactive)",
4545
)
46+
@click.option(
47+
"--folder-name",
48+
help="Custom folder name for the project (defaults to project name)",
49+
)
4650
@handle_cli_exceptions
4751
@handle_api_exceptions
4852
async def init(
@@ -54,6 +58,7 @@ async def init(
5458
project_id: int | None = None,
5559
non_interactive: bool = False,
5660
output_mode: str = "table",
61+
folder_name: str | None = None,
5762
) -> None:
5863
"""Initialize Workato CLI for a new project
5964
@@ -156,6 +161,7 @@ async def init(
156161
project_id=project_id,
157162
output_mode=output_mode,
158163
non_interactive=non_interactive,
164+
folder_name=folder_name,
159165
)
160166

161167
# Check if project directory exists and is non-empty

src/workato_platform_cli/cli/utils/config/manager.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ async def initialize(
5656
project_id: int | None = None,
5757
output_mode: str = "table",
5858
non_interactive: bool = False,
59+
folder_name: str | None = None,
5960
) -> "ConfigManager":
6061
"""Initialize workspace with interactive or non-interactive setup"""
6162
if output_mode == "table":
@@ -80,14 +81,15 @@ async def initialize(
8081
api_url=api_url,
8182
project_name=project_name,
8283
project_id=project_id,
84+
folder_name=folder_name,
8385
)
8486
else:
8587
# Run setup flow
86-
await manager._run_setup_flow()
88+
await manager._run_setup_flow(folder_name=folder_name)
8789

8890
return manager
8991

90-
async def _run_setup_flow(self) -> None:
92+
async def _run_setup_flow(self, folder_name: str | None = None) -> None:
9193
"""Run the complete setup flow"""
9294
workspace_root = self.workspace_manager.find_workspace_root()
9395
self.config_dir = workspace_root
@@ -99,7 +101,7 @@ async def _run_setup_flow(self) -> None:
99101
profile_name = await self._setup_profile()
100102

101103
# Step 2: Project setup
102-
await self._setup_project(profile_name, workspace_root)
104+
await self._setup_project(profile_name, workspace_root, folder_name)
103105

104106
# Step 3: Create workspace files
105107
self._create_workspace_files(workspace_root)
@@ -114,6 +116,7 @@ async def _setup_non_interactive(
114116
api_url: str | None = None,
115117
project_name: str | None = None,
116118
project_id: int | None = None,
119+
folder_name: str | None = None,
117120
) -> None:
118121
"""Perform all setup actions non-interactively"""
119122

@@ -238,7 +241,9 @@ async def _setup_non_interactive(
238241

239242
# Project doesn't exist locally - create new directory
240243
current_dir = Path.cwd().resolve()
241-
project_path = current_dir / selected_project.name
244+
# Use custom folder name if provided, otherwise use project name
245+
folder_name = folder_name if folder_name else selected_project.name
246+
project_path = current_dir / folder_name
242247

243248
# Create project directory
244249
project_path.mkdir(parents=True, exist_ok=True)
@@ -552,7 +557,9 @@ async def _create_new_profile(self, profile_name: str) -> None:
552557
# Save profile and token
553558
self.profile_manager.set_profile(profile_name, profile_data, token)
554559

555-
async def _setup_project(self, profile_name: str, workspace_root: Path) -> None:
560+
async def _setup_project(
561+
self, profile_name: str, workspace_root: Path, folder_name: str | None = None
562+
) -> None:
556563
"""Setup project interactively"""
557564
click.echo("📁 Step 2: Setup project")
558565

@@ -607,6 +614,16 @@ async def _setup_project(self, profile_name: str, workspace_root: Path) -> None:
607614
if not selected_project:
608615
raise click.ClickException("No project selected")
609616

617+
# Prompt for custom folder name if not provided via CLI
618+
if folder_name is None:
619+
click.echo()
620+
custom_name = await click.prompt(
621+
"Folder name (leave blank for default)",
622+
default=selected_project.name,
623+
type=str,
624+
)
625+
folder_name = custom_name.strip() if custom_name.strip() else None
626+
610627
# Check if this specific project already exists locally in the workspace
611628
local_projects = self._find_all_projects(workspace_root)
612629
existing_local_path = None
@@ -641,7 +658,9 @@ async def _setup_project(self, profile_name: str, workspace_root: Path) -> None:
641658
else:
642659
# Project doesn't exist locally - create new directory
643660
current_dir = Path.cwd().resolve()
644-
project_path = current_dir / selected_project.name
661+
# Use custom folder name if provided, otherwise use project name
662+
folder_name = folder_name if folder_name else selected_project.name
663+
project_path = current_dir / folder_name
645664

646665
# Validate project path
647666
try:

tests/unit/commands/conftest.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Shared test fixtures for command tests."""
2+
3+
from collections.abc import Callable
4+
from unittest.mock import AsyncMock, Mock
5+
6+
import pytest
7+
8+
9+
@pytest.fixture
10+
def mock_init_dependencies(monkeypatch) -> Callable[..., dict[str, Mock | AsyncMock]]:
11+
"""Setup common init command dependencies and mocks.
12+
13+
Returns a factory that creates and patches all common init test dependencies.
14+
15+
Usage:
16+
mocks = mock_init_dependencies(profile="test-profile", token="test-token")
17+
# mocks contains: initialize_mock, pull_mock
18+
"""
19+
20+
def _factory(
21+
profile: str = "default",
22+
token: str = "test-token",
23+
) -> dict[str, Mock | AsyncMock]:
24+
from workato_platform_cli.cli.commands import init as init_module
25+
26+
# Create mocks
27+
mock_config_manager = Mock()
28+
mock_workato_client = Mock()
29+
workato_context = AsyncMock()
30+
31+
# Setup config manager defaults
32+
mock_config_manager.load_config.return_value = Mock(profile=profile)
33+
mock_config_manager.get_project_directory.return_value = None
34+
mock_config_manager.profile_manager.resolve_environment_variables.return_value = (
35+
token,
36+
"https://api.workato.com",
37+
)
38+
39+
# Setup Workato context
40+
workato_context.__aenter__.return_value = mock_workato_client
41+
workato_context.__aexit__.return_value = False
42+
43+
# Create and patch initialize mock
44+
mock_initialize = AsyncMock(return_value=mock_config_manager)
45+
monkeypatch.setattr(
46+
init_module.ConfigManager,
47+
"initialize",
48+
mock_initialize,
49+
)
50+
51+
# Create and patch pull mock
52+
mock_pull = AsyncMock()
53+
monkeypatch.setattr(init_module, "_pull_project", mock_pull)
54+
55+
# Patch Workato (Configuration doesn't need mocking)
56+
monkeypatch.setattr(init_module, "Workato", lambda **_: workato_context)
57+
58+
# Silence click.echo
59+
monkeypatch.setattr(init_module.click, "echo", lambda _="": None)
60+
61+
return {
62+
"initialize_mock": mock_initialize,
63+
"pull_mock": mock_pull,
64+
}
65+
66+
return _factory

tests/unit/commands/test_init.py

Lines changed: 106 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,59 +13,26 @@
1313

1414

1515
@pytest.mark.asyncio
16-
async def test_init_interactive_mode(monkeypatch: pytest.MonkeyPatch) -> None:
16+
async def test_init_interactive_mode(mock_init_dependencies) -> None:
1717
"""Test interactive mode (default behavior)."""
18-
mock_config_manager = Mock()
19-
mock_workato_client = Mock()
20-
workato_context = AsyncMock()
18+
mocks = mock_init_dependencies(profile="default", token="token")
2119

22-
with (
23-
patch.object(
24-
mock_config_manager, "load_config", return_value=Mock(profile="default")
25-
),
26-
patch.object(
27-
mock_config_manager,
28-
"get_project_directory",
29-
return_value=None,
30-
),
31-
patch.object(
32-
mock_config_manager.profile_manager,
33-
"resolve_environment_variables",
34-
return_value=("token", "https://api.workato.com"),
35-
),
36-
patch.object(workato_context, "__aenter__", return_value=mock_workato_client),
37-
patch.object(workato_context, "__aexit__", return_value=False),
38-
):
39-
mock_initialize = AsyncMock(return_value=mock_config_manager)
40-
monkeypatch.setattr(
41-
init_module.ConfigManager,
42-
"initialize",
43-
mock_initialize,
44-
)
45-
46-
mock_pull = AsyncMock()
47-
monkeypatch.setattr(init_module, "_pull_project", mock_pull)
48-
49-
monkeypatch.setattr(init_module, "Workato", lambda **_: workato_context)
50-
monkeypatch.setattr(init_module, "Configuration", lambda **_: Mock())
51-
52-
monkeypatch.setattr(init_module.click, "echo", lambda _="": None)
53-
54-
assert init_module.init.callback
55-
await init_module.init.callback()
56-
57-
# Should call initialize with no parameters (interactive mode)
58-
mock_initialize.assert_awaited_once_with(
59-
profile_name=None,
60-
region=None,
61-
api_token=None,
62-
api_url=None,
63-
project_name=None,
64-
project_id=None,
65-
output_mode="table",
66-
non_interactive=False,
67-
)
68-
mock_pull.assert_awaited_once()
20+
assert init_module.init.callback
21+
await init_module.init.callback()
22+
23+
# Should call initialize with no parameters (interactive mode)
24+
mocks["initialize_mock"].assert_awaited_once_with(
25+
profile_name=None,
26+
region=None,
27+
api_token=None,
28+
api_url=None,
29+
project_name=None,
30+
project_id=None,
31+
output_mode="table",
32+
non_interactive=False,
33+
folder_name=None,
34+
)
35+
mocks["pull_mock"].assert_awaited_once()
6936

7037

7138
@pytest.mark.asyncio
@@ -137,6 +104,7 @@ async def test_init_non_interactive_success(monkeypatch: pytest.MonkeyPatch) ->
137104
project_id=None,
138105
output_mode="table",
139106
non_interactive=True,
107+
folder_name=None,
140108
)
141109
mock_pull.assert_awaited_once()
142110

@@ -205,6 +173,7 @@ async def test_init_non_interactive_custom_region(
205173
project_id=123,
206174
output_mode="table",
207175
non_interactive=True,
176+
folder_name=None,
208177
)
209178

210179

@@ -369,6 +338,7 @@ async def test_init_non_interactive_with_region_and_token(
369338
project_id=None,
370339
output_mode="table",
371340
non_interactive=True,
341+
folder_name=None,
372342
)
373343

374344

@@ -1553,3 +1523,88 @@ async def test_init_non_interactive_new_profile_with_region_and_token(
15531523

15541524
# Should proceed without error and not check credentials
15551525
mock_pull.assert_awaited_once()
1526+
1527+
1528+
@pytest.mark.asyncio
1529+
async def test_init_with_custom_folder_name_non_interactive(
1530+
mock_init_dependencies,
1531+
) -> None:
1532+
"""Test non-interactive mode with custom folder name."""
1533+
mocks = mock_init_dependencies(profile="test-profile", token="test-token")
1534+
1535+
assert init_module.init.callback
1536+
await init_module.init.callback(
1537+
profile="test-profile",
1538+
project_name="test-project",
1539+
non_interactive=True,
1540+
folder_name="custom-folder",
1541+
)
1542+
1543+
# Should call initialize with custom folder name
1544+
mocks["initialize_mock"].assert_awaited_once_with(
1545+
profile_name="test-profile",
1546+
region=None,
1547+
api_token=None,
1548+
api_url=None,
1549+
project_name="test-project",
1550+
project_id=None,
1551+
output_mode="table",
1552+
non_interactive=True,
1553+
folder_name="custom-folder",
1554+
)
1555+
mocks["pull_mock"].assert_awaited_once()
1556+
1557+
1558+
@pytest.mark.asyncio
1559+
async def test_init_with_empty_folder_name_non_interactive(
1560+
mock_init_dependencies,
1561+
) -> None:
1562+
"""Test non-interactive mode with empty string folder_name falls back to None."""
1563+
mocks = mock_init_dependencies(profile="test-profile", token="test-token")
1564+
1565+
assert init_module.init.callback
1566+
await init_module.init.callback(
1567+
profile="test-profile",
1568+
project_name="test-project",
1569+
non_interactive=True,
1570+
folder_name="", # Empty string should be treated as None
1571+
)
1572+
1573+
# Should call initialize with folder_name="" (empty string is passed through)
1574+
mocks["initialize_mock"].assert_awaited_once_with(
1575+
profile_name="test-profile",
1576+
region=None,
1577+
api_token=None,
1578+
api_url=None,
1579+
project_name="test-project",
1580+
project_id=None,
1581+
output_mode="table",
1582+
non_interactive=True,
1583+
folder_name="", # Empty string is passed as-is to ConfigManager.initialize
1584+
)
1585+
mocks["pull_mock"].assert_awaited_once()
1586+
1587+
1588+
@pytest.mark.asyncio
1589+
async def test_init_with_custom_folder_name_interactive(
1590+
mock_init_dependencies,
1591+
) -> None:
1592+
"""Test interactive mode with custom folder name."""
1593+
mocks = mock_init_dependencies(profile="default", token="token")
1594+
1595+
assert init_module.init.callback
1596+
await init_module.init.callback(folder_name="my-custom-folder")
1597+
1598+
# Should call initialize with custom folder name
1599+
mocks["initialize_mock"].assert_awaited_once_with(
1600+
profile_name=None,
1601+
region=None,
1602+
api_token=None,
1603+
api_url=None,
1604+
project_name=None,
1605+
project_id=None,
1606+
output_mode="table",
1607+
non_interactive=False,
1608+
folder_name="my-custom-folder",
1609+
)
1610+
mocks["pull_mock"].assert_awaited_once()

0 commit comments

Comments
 (0)