Skip to content

Commit fd85ee6

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

File tree

6 files changed

+659
-113
lines changed

6 files changed

+659
-113
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+
directory_name = folder_name if folder_name else selected_project.name
246+
project_path = current_dir / directory_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+
directory_name = folder_name if folder_name else selected_project.name
663+
project_path = current_dir / directory_name
645664

646665
# Validate project path
647666
try:

tests/unit/commands/conftest.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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+
from workato_platform_cli.cli.utils.config import ConfigData, ProfileData
9+
10+
11+
@pytest.fixture
12+
def profile_data_factory() -> Callable[..., ProfileData]:
13+
"""Create ProfileData instances for test scenarios."""
14+
15+
def _factory(
16+
*,
17+
region: str = "us",
18+
region_url: str = "https://app.workato.com",
19+
workspace_id: int = 123,
20+
) -> ProfileData:
21+
return ProfileData(
22+
region=region,
23+
region_url=region_url,
24+
workspace_id=workspace_id,
25+
)
26+
27+
return _factory
28+
29+
30+
@pytest.fixture
31+
def make_config_manager() -> Callable[..., Mock]:
32+
"""Factory for building config manager stubs with attached profile manager."""
33+
34+
def _factory(**profile_methods: Mock) -> Mock:
35+
profile_manager = Mock()
36+
config_manager = Mock()
37+
config_manager.profile_manager = profile_manager
38+
# Provide deterministic config data unless overridden in tests
39+
config_manager.load_config.return_value = ConfigData()
40+
41+
config_methods = {
42+
"load_config",
43+
"save_config",
44+
"get_workspace_root",
45+
"get_project_directory",
46+
}
47+
48+
for name, value in profile_methods.items():
49+
if name in config_methods:
50+
setattr(config_manager, name, value)
51+
else:
52+
setattr(profile_manager, name, value)
53+
54+
return config_manager
55+
56+
return _factory
57+
58+
59+
@pytest.fixture
60+
def mock_workato_context() -> Callable[..., tuple[Mock, AsyncMock]]:
61+
"""Create mock Workato client and context for async operations.
62+
63+
Returns:
64+
Callable that returns (mock_client, async_context) tuple
65+
"""
66+
67+
def _factory() -> tuple[Mock, AsyncMock]:
68+
mock_client = Mock()
69+
context = AsyncMock()
70+
context.__aenter__.return_value = mock_client
71+
context.__aexit__.return_value = False
72+
return mock_client, context
73+
74+
return _factory
75+
76+
77+
@pytest.fixture
78+
def mock_init_dependencies(monkeypatch) -> Callable[..., dict[str, Mock | AsyncMock]]:
79+
"""Setup common init command dependencies and mocks.
80+
81+
Returns a factory that creates and patches all common init test dependencies.
82+
83+
Usage:
84+
mocks = mock_init_dependencies(
85+
profile="test-profile",
86+
token="test-token",
87+
api_url="https://api.workato.com"
88+
)
89+
# mocks contains: config_manager, workato_client, initialize_mock, pull_mock
90+
"""
91+
92+
def _factory(
93+
profile: str = "default",
94+
token: str = "test-token",
95+
api_url: str = "https://api.workato.com",
96+
project_id: int | None = None,
97+
**config_overrides,
98+
) -> dict[str, Mock | AsyncMock]:
99+
from workato_platform_cli.cli.commands import init as init_module
100+
101+
# Create mocks
102+
mock_config_manager = Mock()
103+
mock_workato_client = Mock()
104+
workato_context = AsyncMock()
105+
106+
# Setup config manager defaults
107+
mock_config_manager.load_config.return_value = Mock(profile=profile)
108+
mock_config_manager.get_project_directory.return_value = project_id
109+
mock_config_manager.profile_manager.resolve_environment_variables.return_value = (
110+
token,
111+
api_url,
112+
)
113+
114+
# Apply any config overrides
115+
for attr, value in config_overrides.items():
116+
if "." in attr:
117+
# Support nested attributes like "profile_manager.get_profile"
118+
obj = mock_config_manager
119+
parts = attr.split(".")
120+
for part in parts[:-1]:
121+
obj = getattr(obj, part)
122+
setattr(obj, parts[-1], value)
123+
else:
124+
setattr(mock_config_manager, attr, value)
125+
126+
# Setup Workato context
127+
workato_context.__aenter__.return_value = mock_workato_client
128+
workato_context.__aexit__.return_value = False
129+
130+
# Create and patch initialize mock
131+
mock_initialize = AsyncMock(return_value=mock_config_manager)
132+
monkeypatch.setattr(
133+
init_module.ConfigManager,
134+
"initialize",
135+
mock_initialize,
136+
)
137+
138+
# Create and patch pull mock
139+
mock_pull = AsyncMock()
140+
monkeypatch.setattr(init_module, "_pull_project", mock_pull)
141+
142+
# Patch Workato and Configuration
143+
monkeypatch.setattr(init_module, "Workato", lambda **_: workato_context)
144+
monkeypatch.setattr(init_module, "Configuration", lambda **_: Mock())
145+
146+
# Silence click.echo
147+
monkeypatch.setattr(init_module.click, "echo", lambda _="": None)
148+
149+
return {
150+
"config_manager": mock_config_manager,
151+
"workato_client": mock_workato_client,
152+
"workato_context": workato_context,
153+
"initialize_mock": mock_initialize,
154+
"pull_mock": mock_pull,
155+
}
156+
157+
return _factory

0 commit comments

Comments
 (0)