diff --git a/src/workato_platform_cli/cli/commands/profiles.py b/src/workato_platform_cli/cli/commands/profiles.py index 8b9af5f..5da767f 100644 --- a/src/workato_platform_cli/cli/commands/profiles.py +++ b/src/workato_platform_cli/cli/commands/profiles.py @@ -6,12 +6,20 @@ from typing import Any import asyncclick as click +import certifi from dependency_injector.wiring import Provide, inject +from workato_platform_cli import Workato from workato_platform_cli.cli.containers import Container from workato_platform_cli.cli.utils.config import ConfigData, ConfigManager +from workato_platform_cli.cli.utils.config.models import ( + AVAILABLE_REGIONS, + ProfileData, + RegionInfo, +) from workato_platform_cli.cli.utils.exception_handler import handle_cli_exceptions +from workato_platform_cli.client.workato_api.configuration import Configuration @click.group() @@ -360,12 +368,83 @@ async def delete( click.echo(f"❌ Failed to delete profile '{profile_name}'") +async def _create_profile_non_interactive( + region: str | None, + api_token: str | None, + api_url: str | None, +) -> tuple[ProfileData, str] | None: + """Create profile data non-interactively. + + Returns (ProfileData, token) on success, or None on error (error already echoed). + """ + # Validate required parameters + if not region: + click.echo("❌ --region is required in non-interactive mode") + return None + if not api_token: + click.echo("❌ --api-token is required in non-interactive mode") + return None + if region == "custom" and not api_url: + click.echo("❌ --api-url is required when region=custom") + return None + + # Get region info + if region == "custom": + region_info = RegionInfo(region="custom", name="Custom", url=api_url) + else: + region_info_lookup = AVAILABLE_REGIONS.get(region) + if not region_info_lookup: + click.echo(f"❌ Invalid region: {region}") + return None + region_info = region_info_lookup + + # Validate credentials and get workspace info + api_config = Configuration( + access_token=api_token, host=region_info.url, ssl_ca_cert=certifi.where() + ) + try: + async with Workato(configuration=api_config) as workato_api_client: + user_info = await workato_api_client.users_api.get_workspace_details() + except Exception as e: + click.echo(f"❌ Authentication failed: {e}") + return None + + profile_data = ProfileData( + region=region_info.region, + region_url=region_info.url, + workspace_id=user_info.id, + ) + return profile_data, api_token + + @profiles.command() @click.argument("profile_name") +@click.option( + "--region", + type=click.Choice(["us", "eu", "jp", "au", "sg", "custom"]), + help="Workato region", +) +@click.option( + "--api-token", + help="Workato API token", +) +@click.option( + "--api-url", + help="Custom API URL (required when region=custom)", +) +@click.option( + "--non-interactive", + is_flag=True, + help="Run in non-interactive mode (requires --region and --api-token)", +) @handle_cli_exceptions @inject async def create( profile_name: str, + region: str | None = None, + api_token: str | None = None, + api_url: str | None = None, + non_interactive: bool = False, config_manager: ConfigManager = Provide[Container.config_manager], ) -> None: """Create a new profile with API credentials""" @@ -377,31 +456,38 @@ async def create( click.echo("💡 Or use 'workato profiles delete' to remove it first") return - click.echo(f"🔧 Creating profile: {profile_name}") - click.echo() - - # Create profile interactively - try: - ( - profile_data, - token, - ) = await config_manager.profile_manager.create_profile_interactive( - profile_name - ) - except click.ClickException: - click.echo("❌ Profile creation cancelled") - return + # Get profile data and token (either interactively or non-interactively) + if non_interactive: + result = await _create_profile_non_interactive(region, api_token, api_url) + if result is None: + return + profile_data, token = result + else: + click.echo(f"🔧 Creating profile: {profile_name}") + click.echo() - # Save profile + try: + ( + profile_data, + token, + ) = await config_manager.profile_manager.create_profile_interactive( + profile_name + ) + except click.ClickException: + click.echo("❌ Profile creation cancelled") + return + + # Save profile (common for both modes) try: config_manager.profile_manager.set_profile(profile_name, profile_data, token) except ValueError as e: click.echo(f"❌ Failed to save profile: {e}") return - # Set as current profile + # Set as current profile (common for both modes) config_manager.profile_manager.set_current_profile(profile_name) + # Success message (common for both modes) click.echo(f"✅ Profile '{profile_name}' created successfully") click.echo(f"✅ Set '{profile_name}' as the active profile") click.echo() diff --git a/tests/unit/commands/test_profiles.py b/tests/unit/commands/test_profiles.py index 4d4b0b3..a12809f 100644 --- a/tests/unit/commands/test_profiles.py +++ b/tests/unit/commands/test_profiles.py @@ -1112,3 +1112,48 @@ async def test_create_profile_keyring_failure( output = capsys.readouterr().out assert "❌ Failed to save profile:" in output assert "Failed to store token in keyring" in output + + +@pytest.mark.asyncio +async def test_create_profile_non_interactive( + capsys: pytest.CaptureFixture[str], + make_config_manager: Callable[..., Mock], +) -> None: + """Test successful non-interactive profile creation.""" + config_manager = make_config_manager( + get_profile=Mock(return_value=None), # Profile doesn't exist yet + set_profile=Mock(), + set_current_profile=Mock(), + ) + + # Mock Workato API client + mock_client = AsyncMock() + mock_user = Mock() + mock_user.id = 123 + mock_client.users_api.get_workspace_details = AsyncMock(return_value=mock_user) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + + with patch( + "workato_platform_cli.cli.commands.profiles.Workato", + return_value=mock_client, + ): + assert create.callback + await create.callback( + profile_name="test_profile", + region="us", + api_token="test_token", + api_url=None, + non_interactive=True, + config_manager=config_manager, + ) + + output = capsys.readouterr().out + assert "✅ Profile 'test_profile' created successfully" in output + assert "✅ Set 'test_profile' as the active profile" in output + + # Verify profile was set and made current + config_manager.profile_manager.set_profile.assert_called_once() + config_manager.profile_manager.set_current_profile.assert_called_once_with( + "test_profile" + )