chore: import upstream snapshot with attribution
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Tests for cua-cli."""
|
||||
@@ -0,0 +1 @@
|
||||
"""API module tests."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Auth module tests."""
|
||||
@@ -0,0 +1,230 @@
|
||||
"""Tests for auth.store module."""
|
||||
|
||||
import sqlite3
|
||||
import threading
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from cua_cli.auth.store import (
|
||||
CredentialStore,
|
||||
clear_credentials,
|
||||
get_api_key,
|
||||
require_api_key,
|
||||
save_api_key,
|
||||
)
|
||||
|
||||
|
||||
class TestCredentialStore:
|
||||
"""Tests for CredentialStore class."""
|
||||
|
||||
def test_init_creates_database(self, tmp_path):
|
||||
"""Test that initializing creates the database file."""
|
||||
db_path = tmp_path / "test.db"
|
||||
CredentialStore(db_path)
|
||||
|
||||
assert db_path.exists()
|
||||
|
||||
def test_init_creates_parent_directory(self, tmp_path):
|
||||
"""Test that initializing creates parent directories."""
|
||||
db_path = tmp_path / "nested" / "dir" / "test.db"
|
||||
CredentialStore(db_path)
|
||||
|
||||
assert db_path.parent.exists()
|
||||
assert db_path.exists()
|
||||
|
||||
def test_set_and_get(self, tmp_path):
|
||||
"""Test setting and getting a value."""
|
||||
store = CredentialStore(tmp_path / "test.db")
|
||||
|
||||
store.set("test_key", "test_value")
|
||||
result = store.get("test_key")
|
||||
|
||||
assert result == "test_value"
|
||||
|
||||
def test_get_nonexistent_key(self, tmp_path):
|
||||
"""Test getting a nonexistent key returns None."""
|
||||
store = CredentialStore(tmp_path / "test.db")
|
||||
|
||||
result = store.get("nonexistent")
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_set_overwrites_existing(self, tmp_path):
|
||||
"""Test that setting an existing key overwrites the value."""
|
||||
store = CredentialStore(tmp_path / "test.db")
|
||||
|
||||
store.set("key", "value1")
|
||||
store.set("key", "value2")
|
||||
result = store.get("key")
|
||||
|
||||
assert result == "value2"
|
||||
|
||||
def test_delete_existing_key(self, tmp_path):
|
||||
"""Test deleting an existing key returns True."""
|
||||
store = CredentialStore(tmp_path / "test.db")
|
||||
|
||||
store.set("key", "value")
|
||||
result = store.delete("key")
|
||||
|
||||
assert result is True
|
||||
assert store.get("key") is None
|
||||
|
||||
def test_delete_nonexistent_key(self, tmp_path):
|
||||
"""Test deleting a nonexistent key returns False."""
|
||||
store = CredentialStore(tmp_path / "test.db")
|
||||
|
||||
result = store.delete("nonexistent")
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_clear_removes_all(self, tmp_path):
|
||||
"""Test that clear removes all values."""
|
||||
store = CredentialStore(tmp_path / "test.db")
|
||||
|
||||
store.set("key1", "value1")
|
||||
store.set("key2", "value2")
|
||||
store.clear()
|
||||
|
||||
assert store.get("key1") is None
|
||||
assert store.get("key2") is None
|
||||
|
||||
def test_wal_mode_enabled(self, tmp_path):
|
||||
"""Test that WAL mode is enabled for concurrent access."""
|
||||
db_path = tmp_path / "test.db"
|
||||
CredentialStore(db_path)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
cursor = conn.execute("PRAGMA journal_mode")
|
||||
mode = cursor.fetchone()[0]
|
||||
assert mode.lower() == "wal"
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_concurrent_access(self, tmp_path):
|
||||
"""Test that concurrent access works with WAL mode."""
|
||||
db_path = tmp_path / "test.db"
|
||||
store = CredentialStore(db_path)
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
def writer():
|
||||
try:
|
||||
for i in range(10):
|
||||
store.set(f"key_{i}", f"value_{i}")
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
|
||||
def reader():
|
||||
try:
|
||||
for i in range(10):
|
||||
result = store.get(f"key_{i}")
|
||||
if result is not None:
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
|
||||
threads = [
|
||||
threading.Thread(target=writer),
|
||||
threading.Thread(target=reader),
|
||||
threading.Thread(target=reader),
|
||||
]
|
||||
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
assert len(errors) == 0
|
||||
|
||||
|
||||
class TestGetApiKey:
|
||||
"""Tests for get_api_key function."""
|
||||
|
||||
def test_returns_env_var_first(self, monkeypatch, tmp_path):
|
||||
"""Test that environment variable takes precedence."""
|
||||
# Set up a store with a different value
|
||||
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
|
||||
with patch("cua_cli.auth.store._store", None):
|
||||
monkeypatch.setenv("CUA_API_KEY", "env-key")
|
||||
save_api_key("stored-key")
|
||||
|
||||
result = get_api_key()
|
||||
|
||||
assert result == "env-key"
|
||||
|
||||
def test_falls_back_to_stored(self, monkeypatch, tmp_path):
|
||||
"""Test that stored key is used when env var not set."""
|
||||
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
|
||||
with patch("cua_cli.auth.store._store", None):
|
||||
monkeypatch.delenv("CUA_API_KEY", raising=False)
|
||||
save_api_key("stored-key")
|
||||
|
||||
result = get_api_key()
|
||||
|
||||
assert result == "stored-key"
|
||||
|
||||
def test_returns_none_when_not_found(self, monkeypatch, tmp_path):
|
||||
"""Test that None is returned when no key is found."""
|
||||
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
|
||||
with patch("cua_cli.auth.store._store", None):
|
||||
monkeypatch.delenv("CUA_API_KEY", raising=False)
|
||||
|
||||
result = get_api_key()
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestSaveApiKey:
|
||||
"""Tests for save_api_key function."""
|
||||
|
||||
def test_saves_api_key(self, tmp_path, monkeypatch):
|
||||
"""Test that API key is saved correctly."""
|
||||
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
|
||||
with patch("cua_cli.auth.store._store", None):
|
||||
monkeypatch.delenv("CUA_API_KEY", raising=False)
|
||||
|
||||
save_api_key("my-api-key")
|
||||
result = get_api_key()
|
||||
|
||||
assert result == "my-api-key"
|
||||
|
||||
|
||||
class TestClearCredentials:
|
||||
"""Tests for clear_credentials function."""
|
||||
|
||||
def test_clears_all_credentials(self, tmp_path, monkeypatch):
|
||||
"""Test that all credentials are cleared."""
|
||||
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
|
||||
with patch("cua_cli.auth.store._store", None):
|
||||
monkeypatch.delenv("CUA_API_KEY", raising=False)
|
||||
|
||||
save_api_key("my-api-key")
|
||||
clear_credentials()
|
||||
result = get_api_key()
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestRequireApiKey:
|
||||
"""Tests for require_api_key function."""
|
||||
|
||||
def test_returns_key_when_available(self, monkeypatch):
|
||||
"""Test that key is returned when available."""
|
||||
monkeypatch.setenv("CUA_API_KEY", "my-api-key")
|
||||
|
||||
result = require_api_key()
|
||||
|
||||
assert result == "my-api-key"
|
||||
|
||||
def test_raises_when_not_available(self, monkeypatch, tmp_path):
|
||||
"""Test that RuntimeError is raised when no key available."""
|
||||
with patch("cua_cli.auth.store.CREDENTIALS_DB", tmp_path / "test.db"):
|
||||
with patch("cua_cli.auth.store._store", None):
|
||||
monkeypatch.delenv("CUA_API_KEY", raising=False)
|
||||
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
require_api_key()
|
||||
|
||||
assert "No API key configured" in str(exc_info.value)
|
||||
assert "cua auth login" in str(exc_info.value)
|
||||
@@ -0,0 +1 @@
|
||||
"""Command module tests."""
|
||||
@@ -0,0 +1,200 @@
|
||||
"""Tests for auth command module."""
|
||||
|
||||
import argparse
|
||||
from unittest.mock import patch
|
||||
|
||||
from cua_cli.commands import auth
|
||||
|
||||
|
||||
class TestRegisterParser:
|
||||
"""Tests for register_parser function."""
|
||||
|
||||
def test_registers_auth_command(self):
|
||||
"""Test that auth command is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
auth.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["auth", "login"])
|
||||
assert args.auth_command == "login"
|
||||
|
||||
def test_login_has_api_key_flag(self):
|
||||
"""Test that login has --api-key flag."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
auth.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["auth", "login", "--api-key", "my-key"])
|
||||
assert args.api_key == "my-key"
|
||||
|
||||
def test_logout_command(self):
|
||||
"""Test that logout command is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
auth.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["auth", "logout"])
|
||||
assert args.auth_command == "logout"
|
||||
|
||||
def test_env_command(self):
|
||||
"""Test that env command is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
auth.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["auth", "env"])
|
||||
assert args.auth_command == "env"
|
||||
|
||||
|
||||
class TestExecute:
|
||||
"""Tests for execute function."""
|
||||
|
||||
def test_dispatch_to_login(self, args_namespace):
|
||||
"""Test dispatch to login command."""
|
||||
args = args_namespace(command="auth", auth_command="login", api_key=False)
|
||||
|
||||
with patch.object(auth, "cmd_login", return_value=0) as mock_cmd:
|
||||
result = auth.execute(args)
|
||||
|
||||
mock_cmd.assert_called_once_with(args)
|
||||
assert result == 0
|
||||
|
||||
def test_dispatch_to_logout(self, args_namespace):
|
||||
"""Test dispatch to logout command."""
|
||||
args = args_namespace(command="auth", auth_command="logout")
|
||||
|
||||
with patch.object(auth, "cmd_logout", return_value=0) as mock_cmd:
|
||||
auth.execute(args)
|
||||
|
||||
mock_cmd.assert_called_once_with(args)
|
||||
|
||||
def test_dispatch_to_env(self, args_namespace):
|
||||
"""Test dispatch to env command."""
|
||||
args = args_namespace(command="auth", auth_command="env")
|
||||
|
||||
with patch.object(auth, "cmd_env", return_value=0) as mock_cmd:
|
||||
auth.execute(args)
|
||||
|
||||
mock_cmd.assert_called_once_with(args)
|
||||
|
||||
def test_unknown_command_returns_error(self, args_namespace):
|
||||
"""Test that unknown command returns error."""
|
||||
args = args_namespace(command="auth", auth_command=None)
|
||||
|
||||
with patch.object(auth, "print_error"):
|
||||
result = auth.execute(args)
|
||||
|
||||
assert result == 1
|
||||
|
||||
|
||||
class TestCmdLogin:
|
||||
"""Tests for cmd_login function."""
|
||||
|
||||
def test_login_with_api_key_direct(self, args_namespace):
|
||||
"""Test login with --api-key flag and value."""
|
||||
args = args_namespace(api_key="test-api-key")
|
||||
|
||||
with patch.object(auth, "get_api_key", return_value=None):
|
||||
with patch.object(auth, "save_api_key") as mock_save:
|
||||
with patch.object(auth, "print_info"):
|
||||
with patch.object(auth, "print_success"):
|
||||
result = auth.cmd_login(args)
|
||||
|
||||
mock_save.assert_called_once_with("test-api-key")
|
||||
assert result == 0
|
||||
|
||||
def test_login_already_authenticated(self, args_namespace):
|
||||
"""Test login when already authenticated."""
|
||||
args = args_namespace(api_key=None)
|
||||
|
||||
with patch.object(auth, "get_api_key", return_value="existing-key"):
|
||||
with patch.object(auth, "print_info") as mock_info:
|
||||
result = auth.cmd_login(args)
|
||||
|
||||
assert result == 0
|
||||
mock_info.assert_called()
|
||||
|
||||
def test_login_browser_flow(self, args_namespace):
|
||||
"""Test login with browser OAuth flow."""
|
||||
args = args_namespace(api_key=None)
|
||||
|
||||
with patch.object(auth, "get_api_key", return_value=None):
|
||||
with patch.object(auth, "run_async") as mock_run:
|
||||
mock_run.return_value = "browser-api-key"
|
||||
with patch.object(auth, "save_api_key") as mock_save:
|
||||
with patch.object(auth, "print_success"):
|
||||
result = auth.cmd_login(args)
|
||||
|
||||
mock_save.assert_called_once_with("browser-api-key")
|
||||
assert result == 0
|
||||
|
||||
def test_login_browser_flow_timeout(self, args_namespace):
|
||||
"""Test login when browser flow times out."""
|
||||
args = args_namespace(api_key=None)
|
||||
|
||||
with patch.object(auth, "get_api_key", return_value=None):
|
||||
with patch.object(auth, "run_async") as mock_run:
|
||||
mock_run.side_effect = TimeoutError("Authentication timed out")
|
||||
with patch.object(auth, "print_error"):
|
||||
result = auth.cmd_login(args)
|
||||
|
||||
assert result == 1
|
||||
|
||||
|
||||
class TestCmdLogout:
|
||||
"""Tests for cmd_logout function."""
|
||||
|
||||
def test_logout_clears_credentials(self, args_namespace):
|
||||
"""Test that logout clears credentials."""
|
||||
args = args_namespace()
|
||||
|
||||
with patch.object(auth, "clear_credentials") as mock_clear:
|
||||
with patch.object(auth, "print_success"):
|
||||
result = auth.cmd_logout(args)
|
||||
|
||||
mock_clear.assert_called_once()
|
||||
assert result == 0
|
||||
|
||||
|
||||
class TestCmdEnv:
|
||||
"""Tests for cmd_env function."""
|
||||
|
||||
def test_env_writes_to_dotenv(self, args_namespace, tmp_path, monkeypatch):
|
||||
"""Test that env command writes to .env file."""
|
||||
args = args_namespace(file=str(tmp_path / ".env"))
|
||||
|
||||
with patch.object(auth, "get_api_key", return_value="test-api-key"):
|
||||
with patch.object(auth, "print_success"):
|
||||
result = auth.cmd_env(args)
|
||||
|
||||
assert result == 0
|
||||
env_file = tmp_path / ".env"
|
||||
assert env_file.exists()
|
||||
content = env_file.read_text()
|
||||
assert "CUA_API_KEY=test-api-key" in content
|
||||
|
||||
def test_env_no_credentials_fails(self, args_namespace, tmp_path):
|
||||
"""Test that env command fails when not logged in."""
|
||||
args = args_namespace(file=str(tmp_path / ".env"))
|
||||
|
||||
with patch.object(auth, "get_api_key", return_value=None):
|
||||
with patch.object(auth, "print_error"):
|
||||
result = auth.cmd_env(args)
|
||||
|
||||
assert result == 1
|
||||
|
||||
def test_env_appends_to_existing_dotenv(self, args_namespace, tmp_path):
|
||||
"""Test that env command appends to existing .env file."""
|
||||
env_file = tmp_path / ".env"
|
||||
env_file.write_text("EXISTING_VAR=value\n")
|
||||
args = args_namespace(file=str(env_file))
|
||||
|
||||
with patch.object(auth, "get_api_key", return_value="test-api-key"):
|
||||
with patch.object(auth, "print_success"):
|
||||
result = auth.cmd_env(args)
|
||||
|
||||
assert result == 0
|
||||
content = env_file.read_text()
|
||||
assert "EXISTING_VAR=value" in content
|
||||
assert "CUA_API_KEY=test-api-key" in content
|
||||
@@ -0,0 +1,297 @@
|
||||
"""Tests for image command module."""
|
||||
|
||||
import argparse
|
||||
from unittest.mock import patch
|
||||
|
||||
from cua_cli.commands import image
|
||||
|
||||
|
||||
class TestRegisterParser:
|
||||
"""Tests for register_parser function."""
|
||||
|
||||
def test_registers_image_command(self):
|
||||
"""Test that image command is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
image.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["image", "list"])
|
||||
assert args.image_command == "list"
|
||||
|
||||
def test_registers_img_alias(self):
|
||||
"""Test that img alias is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
image.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["img", "list"])
|
||||
assert args.image_command == "list"
|
||||
|
||||
def test_list_has_json_flag(self):
|
||||
"""Test that list command has --json flag."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
image.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["image", "list", "--json"])
|
||||
assert args.json is True
|
||||
|
||||
def test_list_has_local_flag(self):
|
||||
"""Test that list command has --local flag."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
image.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["image", "list", "--local"])
|
||||
assert args.local is True
|
||||
|
||||
def test_push_command(self):
|
||||
"""Test that push command is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
image.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["image", "push", "my-image"])
|
||||
assert args.image_command == "push"
|
||||
assert args.name == "my-image"
|
||||
|
||||
def test_pull_command(self):
|
||||
"""Test that pull command is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
image.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["image", "pull", "my-image"])
|
||||
assert args.image_command == "pull"
|
||||
assert args.name == "my-image"
|
||||
|
||||
def test_delete_command(self):
|
||||
"""Test that delete command is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
image.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["image", "delete", "my-image", "--force"])
|
||||
assert args.image_command == "delete"
|
||||
assert args.name == "my-image"
|
||||
assert args.force is True
|
||||
|
||||
|
||||
class TestExecute:
|
||||
"""Tests for execute function."""
|
||||
|
||||
def test_dispatch_to_list(self, args_namespace):
|
||||
"""Test dispatch to list command."""
|
||||
args = args_namespace(
|
||||
command="image",
|
||||
image_command="list",
|
||||
json=False,
|
||||
local=False,
|
||||
cloud=False,
|
||||
)
|
||||
|
||||
with patch.object(image, "cmd_list", return_value=0) as mock_cmd:
|
||||
result = image.execute(args)
|
||||
|
||||
mock_cmd.assert_called_once_with(args)
|
||||
assert result == 0
|
||||
|
||||
def test_dispatch_to_push(self, args_namespace):
|
||||
"""Test dispatch to push command."""
|
||||
args = args_namespace(command="image", image_command="push", name="my-image")
|
||||
|
||||
with patch.object(image, "cmd_push", return_value=0) as mock_cmd:
|
||||
image.execute(args)
|
||||
|
||||
mock_cmd.assert_called_once_with(args)
|
||||
|
||||
def test_dispatch_to_pull(self, args_namespace):
|
||||
"""Test dispatch to pull command."""
|
||||
args = args_namespace(command="image", image_command="pull", name="my-image")
|
||||
|
||||
with patch.object(image, "cmd_pull", return_value=0) as mock_cmd:
|
||||
image.execute(args)
|
||||
|
||||
mock_cmd.assert_called_once_with(args)
|
||||
|
||||
def test_dispatch_to_delete(self, args_namespace):
|
||||
"""Test dispatch to delete command."""
|
||||
args = args_namespace(command="image", image_command="delete", name="my-image")
|
||||
|
||||
with patch.object(image, "cmd_delete", return_value=0) as mock_cmd:
|
||||
image.execute(args)
|
||||
|
||||
mock_cmd.assert_called_once_with(args)
|
||||
|
||||
def test_unknown_command_returns_error(self, args_namespace):
|
||||
"""Test that unknown command returns error."""
|
||||
args = args_namespace(command="image", image_command=None)
|
||||
|
||||
with patch.object(image, "print_error"):
|
||||
result = image.execute(args)
|
||||
|
||||
assert result == 1
|
||||
|
||||
|
||||
class TestCmdList:
|
||||
"""Tests for cmd_list function."""
|
||||
|
||||
def test_list_cloud_images(self, args_namespace, sample_cloud_images, mock_api_key):
|
||||
"""Test listing cloud images."""
|
||||
args = args_namespace(json=False, local=False, cloud=True)
|
||||
|
||||
with patch.object(image, "run_async") as mock_run:
|
||||
mock_run.return_value = [
|
||||
{
|
||||
"name": "ubuntu-22.04",
|
||||
"type": "qcow2",
|
||||
"tag": "latest",
|
||||
"size": "5.0 GB",
|
||||
"status": "ready",
|
||||
"created": "2024-01-10",
|
||||
"source": "cloud",
|
||||
}
|
||||
]
|
||||
with patch.object(image, "print_table") as mock_print:
|
||||
result = image.cmd_list(args)
|
||||
|
||||
assert result == 0
|
||||
mock_print.assert_called_once()
|
||||
|
||||
def test_list_local_images(self, args_namespace, tmp_path, monkeypatch):
|
||||
"""Test listing local images delegates to local_image."""
|
||||
args = args_namespace(json=False, local=True, cloud=False)
|
||||
|
||||
with patch("cua_cli.commands.local_image.cmd_local_list", return_value=0) as mock_local:
|
||||
result = image.cmd_list(args)
|
||||
|
||||
assert result == 0
|
||||
mock_local.assert_called_once_with(args)
|
||||
|
||||
def test_list_empty(self, args_namespace, tmp_path, monkeypatch):
|
||||
"""Test listing when no local images exist."""
|
||||
args = args_namespace(json=False, local=True, cloud=False)
|
||||
|
||||
with patch("cua_cli.commands.local_image.cmd_local_list", return_value=0) as mock_local:
|
||||
result = image.cmd_list(args)
|
||||
|
||||
assert result == 0
|
||||
mock_local.assert_called_once()
|
||||
|
||||
def test_list_json_output(self, args_namespace, mock_api_key):
|
||||
"""Test JSON output format."""
|
||||
args = args_namespace(json=True, local=False, cloud=True)
|
||||
|
||||
with patch.object(image, "run_async") as mock_run:
|
||||
mock_run.return_value = [{"name": "test", "source": "cloud"}]
|
||||
with patch.object(image, "print_json") as mock_print:
|
||||
result = image.cmd_list(args)
|
||||
|
||||
assert result == 0
|
||||
mock_print.assert_called_once()
|
||||
|
||||
|
||||
class TestCmdPush:
|
||||
"""Tests for cmd_push function."""
|
||||
|
||||
def test_push_success(self, args_namespace, temp_image_file, mock_api_key):
|
||||
"""Test successful image push."""
|
||||
args = args_namespace(
|
||||
name="test-image",
|
||||
tag="latest",
|
||||
type="qcow2",
|
||||
file=str(temp_image_file),
|
||||
)
|
||||
|
||||
with patch.object(image, "run_async") as mock_run:
|
||||
mock_run.return_value = 0
|
||||
result = image.cmd_push(args)
|
||||
|
||||
assert result == 0
|
||||
|
||||
def test_push_file_not_found(self, args_namespace, mock_api_key):
|
||||
"""Test push with nonexistent file."""
|
||||
args = args_namespace(
|
||||
name="test-image",
|
||||
tag="latest",
|
||||
type="qcow2",
|
||||
file="/nonexistent/path",
|
||||
)
|
||||
|
||||
with patch.object(image, "print_error") as mock_error:
|
||||
result = image.cmd_push(args)
|
||||
|
||||
assert result == 1
|
||||
mock_error.assert_called()
|
||||
|
||||
|
||||
class TestCmdPull:
|
||||
"""Tests for cmd_pull function."""
|
||||
|
||||
def test_pull_success(self, args_namespace, tmp_path, mock_api_key):
|
||||
"""Test successful image pull."""
|
||||
args = args_namespace(
|
||||
name="test-image",
|
||||
tag="latest",
|
||||
output=str(tmp_path / "output.qcow2"),
|
||||
)
|
||||
|
||||
with patch.object(image, "run_async") as mock_run:
|
||||
mock_run.return_value = 0
|
||||
result = image.cmd_pull(args)
|
||||
|
||||
assert result == 0
|
||||
|
||||
|
||||
class TestCmdDelete:
|
||||
"""Tests for cmd_delete function."""
|
||||
|
||||
def test_delete_with_force(self, args_namespace, mock_api_key):
|
||||
"""Test delete with --force flag."""
|
||||
args = args_namespace(name="test-image", tag="latest", force=True, local=False)
|
||||
|
||||
with patch.object(image, "run_async") as mock_run:
|
||||
mock_run.return_value = 0
|
||||
result = image.cmd_delete(args)
|
||||
|
||||
assert result == 0
|
||||
|
||||
def test_delete_without_force_prompts(self, args_namespace, mock_api_key):
|
||||
"""Test delete without --force shows prompt."""
|
||||
args = args_namespace(name="test-image", tag="latest", force=False, local=False)
|
||||
|
||||
with patch.object(image, "print_info") as mock_print:
|
||||
result = image.cmd_delete(args)
|
||||
|
||||
assert result == 1
|
||||
mock_print.assert_called()
|
||||
|
||||
def test_delete_local_delegates(self, args_namespace):
|
||||
"""Test delete with --local delegates to local_image."""
|
||||
args = args_namespace(name="test-image", tag="latest", force=True, local=True)
|
||||
|
||||
with patch("cua_cli.commands.local_image.cmd_local_delete", return_value=0) as mock_local:
|
||||
result = image.cmd_delete(args)
|
||||
|
||||
assert result == 0
|
||||
mock_local.assert_called_once_with(args)
|
||||
|
||||
|
||||
class TestHelperFunctions:
|
||||
"""Tests for helper functions."""
|
||||
|
||||
def test_format_date_valid(self):
|
||||
"""Test formatting a valid ISO date."""
|
||||
result = image._format_date("2024-01-15T10:30:00Z")
|
||||
assert result == "2024-01-15"
|
||||
|
||||
def test_format_date_empty(self):
|
||||
"""Test formatting empty string."""
|
||||
result = image._format_date("")
|
||||
assert result == "-"
|
||||
|
||||
def test_format_date_invalid(self):
|
||||
"""Test formatting invalid date."""
|
||||
result = image._format_date("not-a-date")
|
||||
assert result == "not-a-date" # Falls back to first 10 chars
|
||||
@@ -0,0 +1,232 @@
|
||||
"""Tests for MCP server command module."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
from cua_cli.commands.mcp import (
|
||||
PERMISSION_GROUPS,
|
||||
Permission,
|
||||
execute,
|
||||
parse_permissions,
|
||||
register_parser,
|
||||
)
|
||||
|
||||
|
||||
class TestPermission:
|
||||
"""Tests for Permission enum."""
|
||||
|
||||
def test_all_permissions_have_values(self):
|
||||
"""Test that all permissions have string values."""
|
||||
for perm in Permission:
|
||||
assert isinstance(perm.value, str)
|
||||
assert ":" in perm.value
|
||||
|
||||
def test_sandbox_permissions(self):
|
||||
"""Test sandbox permission values."""
|
||||
assert Permission.SANDBOX_LIST.value == "sandbox:list"
|
||||
assert Permission.SANDBOX_CREATE.value == "sandbox:create"
|
||||
assert Permission.SANDBOX_DELETE.value == "sandbox:delete"
|
||||
|
||||
def test_computer_permissions(self):
|
||||
"""Test computer permission values."""
|
||||
assert Permission.COMPUTER_SCREENSHOT.value == "computer:screenshot"
|
||||
assert Permission.COMPUTER_CLICK.value == "computer:click"
|
||||
assert Permission.COMPUTER_TYPE.value == "computer:type"
|
||||
|
||||
def test_skills_permissions(self):
|
||||
"""Test skills permission values."""
|
||||
assert Permission.SKILLS_LIST.value == "skills:list"
|
||||
assert Permission.SKILLS_READ.value == "skills:read"
|
||||
assert Permission.SKILLS_RECORD.value == "skills:record"
|
||||
|
||||
|
||||
class TestPermissionGroups:
|
||||
"""Tests for permission groups."""
|
||||
|
||||
def test_sandbox_all_group(self):
|
||||
"""Test sandbox:all group contains all sandbox permissions."""
|
||||
group = PERMISSION_GROUPS["sandbox:all"]
|
||||
assert Permission.SANDBOX_LIST in group
|
||||
assert Permission.SANDBOX_CREATE in group
|
||||
assert Permission.SANDBOX_DELETE in group
|
||||
assert Permission.SANDBOX_START in group
|
||||
assert Permission.SANDBOX_STOP in group
|
||||
|
||||
def test_sandbox_readonly_group(self):
|
||||
"""Test sandbox:readonly group contains only read permissions."""
|
||||
group = PERMISSION_GROUPS["sandbox:readonly"]
|
||||
assert Permission.SANDBOX_LIST in group
|
||||
assert Permission.SANDBOX_GET in group
|
||||
assert Permission.SANDBOX_CREATE not in group
|
||||
assert Permission.SANDBOX_DELETE not in group
|
||||
|
||||
def test_computer_all_group(self):
|
||||
"""Test computer:all group contains all computer permissions."""
|
||||
group = PERMISSION_GROUPS["computer:all"]
|
||||
assert Permission.COMPUTER_SCREENSHOT in group
|
||||
assert Permission.COMPUTER_CLICK in group
|
||||
assert Permission.COMPUTER_TYPE in group
|
||||
assert Permission.COMPUTER_SHELL in group
|
||||
|
||||
def test_computer_readonly_group(self):
|
||||
"""Test computer:readonly group contains only screenshot."""
|
||||
group = PERMISSION_GROUPS["computer:readonly"]
|
||||
assert Permission.COMPUTER_SCREENSHOT in group
|
||||
assert Permission.COMPUTER_CLICK not in group
|
||||
|
||||
def test_all_group_contains_everything(self):
|
||||
"""Test that 'all' group contains all permissions."""
|
||||
group = PERMISSION_GROUPS["all"]
|
||||
assert len(group) == len(Permission)
|
||||
|
||||
|
||||
class TestParsePermissions:
|
||||
"""Tests for parse_permissions function."""
|
||||
|
||||
def test_empty_string_returns_empty_set(self):
|
||||
"""Test that empty string returns empty set."""
|
||||
result = parse_permissions("")
|
||||
assert result == set()
|
||||
|
||||
def test_single_permission(self):
|
||||
"""Test parsing a single permission."""
|
||||
result = parse_permissions("sandbox:list")
|
||||
assert result == {Permission.SANDBOX_LIST}
|
||||
|
||||
def test_multiple_permissions(self):
|
||||
"""Test parsing multiple permissions."""
|
||||
result = parse_permissions("sandbox:list,sandbox:create")
|
||||
assert result == {Permission.SANDBOX_LIST, Permission.SANDBOX_CREATE}
|
||||
|
||||
def test_permission_with_spaces(self):
|
||||
"""Test parsing permissions with spaces."""
|
||||
result = parse_permissions("sandbox:list, sandbox:create")
|
||||
assert result == {Permission.SANDBOX_LIST, Permission.SANDBOX_CREATE}
|
||||
|
||||
def test_group_expansion(self):
|
||||
"""Test that groups are expanded."""
|
||||
result = parse_permissions("sandbox:readonly")
|
||||
assert Permission.SANDBOX_LIST in result
|
||||
assert Permission.SANDBOX_GET in result
|
||||
|
||||
def test_all_group(self):
|
||||
"""Test that 'all' group includes everything."""
|
||||
result = parse_permissions("all")
|
||||
assert len(result) == len(Permission)
|
||||
|
||||
def test_mixed_groups_and_permissions(self):
|
||||
"""Test mixing groups and individual permissions."""
|
||||
result = parse_permissions("sandbox:readonly,computer:screenshot")
|
||||
assert Permission.SANDBOX_LIST in result
|
||||
assert Permission.SANDBOX_GET in result
|
||||
assert Permission.COMPUTER_SCREENSHOT in result
|
||||
|
||||
def test_unknown_permission_ignored(self):
|
||||
"""Test that unknown permissions are ignored with warning."""
|
||||
with patch("cua_cli.commands.mcp.logger") as mock_logger:
|
||||
result = parse_permissions("sandbox:list,unknown:perm")
|
||||
|
||||
assert result == {Permission.SANDBOX_LIST}
|
||||
mock_logger.warning.assert_called()
|
||||
|
||||
|
||||
class TestRegisterParser:
|
||||
"""Tests for register_parser function."""
|
||||
|
||||
def test_registers_serve_mcp_command(self):
|
||||
"""Test that serve-mcp command is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["serve-mcp"])
|
||||
assert hasattr(args, "permissions")
|
||||
|
||||
def test_has_permissions_flag(self):
|
||||
"""Test that --permissions flag is available."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["serve-mcp", "--permissions", "sandbox:all"])
|
||||
assert args.permissions == "sandbox:all"
|
||||
|
||||
def test_has_sandbox_flag(self):
|
||||
"""Test that --sandbox flag is available."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["serve-mcp", "--sandbox", "my-sandbox"])
|
||||
assert args.sandbox == "my-sandbox"
|
||||
|
||||
|
||||
class TestExecute:
|
||||
"""Tests for execute function."""
|
||||
|
||||
def test_missing_fastmcp_returns_error(self, args_namespace, capsys):
|
||||
"""Test that missing fastmcp dependency returns error."""
|
||||
|
||||
args = args_namespace(permissions="", sandbox="")
|
||||
|
||||
# Temporarily remove mcp modules to simulate ImportError
|
||||
original_modules = {}
|
||||
for mod in list(sys.modules.keys()):
|
||||
if mod.startswith("mcp"):
|
||||
original_modules[mod] = sys.modules.pop(mod)
|
||||
|
||||
try:
|
||||
with patch.dict(
|
||||
"sys.modules", {"mcp": None, "mcp.server": None, "mcp.server.fastmcp": None}
|
||||
):
|
||||
result = execute(args)
|
||||
finally:
|
||||
# Restore modules
|
||||
sys.modules.update(original_modules)
|
||||
|
||||
assert result == 1
|
||||
|
||||
def test_permissions_from_env_var(self, args_namespace, monkeypatch):
|
||||
"""Test that permissions are read from environment variable."""
|
||||
monkeypatch.setenv("CUA_MCP_PERMISSIONS", "sandbox:readonly")
|
||||
|
||||
permissions = parse_permissions("sandbox:readonly")
|
||||
assert Permission.SANDBOX_LIST in permissions
|
||||
assert Permission.SANDBOX_CREATE not in permissions
|
||||
|
||||
def test_permissions_from_args_override_env(self, args_namespace, monkeypatch):
|
||||
"""Test that command line args override environment variable."""
|
||||
args = args_namespace(permissions="computer:all", sandbox="")
|
||||
monkeypatch.setenv("CUA_MCP_PERMISSIONS", "sandbox:readonly")
|
||||
|
||||
# The args.permissions should take precedence
|
||||
assert args.permissions == "computer:all"
|
||||
|
||||
|
||||
class TestMCPToolRegistration:
|
||||
"""Tests for MCP tool registration based on permissions."""
|
||||
|
||||
def test_permission_affects_tool_registration(self):
|
||||
"""Test that permissions control which tools would be registered."""
|
||||
# This is a unit test of the permission logic, not the actual registration
|
||||
# The actual registration requires fastmcp which may not be installed
|
||||
|
||||
all_perms = parse_permissions("all")
|
||||
assert len(all_perms) == len(Permission)
|
||||
|
||||
readonly_perms = parse_permissions("sandbox:readonly")
|
||||
assert Permission.SANDBOX_LIST in readonly_perms
|
||||
assert Permission.SANDBOX_CREATE not in readonly_perms
|
||||
|
||||
# Test that the registration functions exist and are callable
|
||||
from cua_cli.commands.mcp import (
|
||||
_register_computer_tools,
|
||||
_register_sandbox_tools,
|
||||
_register_skills_tools,
|
||||
)
|
||||
|
||||
assert callable(_register_sandbox_tools)
|
||||
assert callable(_register_computer_tools)
|
||||
assert callable(_register_skills_tools)
|
||||
@@ -0,0 +1,671 @@
|
||||
"""Tests for sandbox command module."""
|
||||
|
||||
import argparse
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from cua_cli.commands import sandbox
|
||||
|
||||
|
||||
class TestRegisterParser:
|
||||
"""Tests for register_parser function."""
|
||||
|
||||
def test_registers_sandbox_command(self):
|
||||
"""Test that sandbox command is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
sandbox.register_parser(subparsers)
|
||||
|
||||
# Parse a sandbox command to verify it's registered
|
||||
args = parser.parse_args(["sandbox", "list"])
|
||||
assert args.sandbox_command == "list"
|
||||
|
||||
def test_registers_sb_alias(self):
|
||||
"""Test that sb alias is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
sandbox.register_parser(subparsers)
|
||||
|
||||
# Parse using alias
|
||||
args = parser.parse_args(["sb", "list"])
|
||||
assert args.sandbox_command == "list"
|
||||
|
||||
def test_list_command_has_json_flag(self):
|
||||
"""Test that list command has --json flag."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
sandbox.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["sandbox", "list", "--json"])
|
||||
assert args.json is True
|
||||
|
||||
def test_create_command_has_required_args(self):
|
||||
"""Test that create command has required arguments."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
sandbox.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(
|
||||
[
|
||||
"sandbox",
|
||||
"create",
|
||||
"--os",
|
||||
"linux",
|
||||
"--size",
|
||||
"medium",
|
||||
"--region",
|
||||
"north-america",
|
||||
]
|
||||
)
|
||||
assert args.os == "linux"
|
||||
assert args.size == "medium"
|
||||
assert args.region == "north-america"
|
||||
|
||||
|
||||
class TestExecute:
|
||||
"""Tests for execute function."""
|
||||
|
||||
def test_dispatch_to_list(self, args_namespace):
|
||||
"""Test dispatch to list command."""
|
||||
args = args_namespace(
|
||||
command="sandbox", sandbox_command="list", json=False, show_passwords=False
|
||||
)
|
||||
|
||||
with patch.object(sandbox, "cmd_list", return_value=0) as mock_cmd:
|
||||
result = sandbox.execute(args)
|
||||
|
||||
mock_cmd.assert_called_once_with(args)
|
||||
assert result == 0
|
||||
|
||||
def test_dispatch_to_list_alias(self, args_namespace):
|
||||
"""Test dispatch to list command via ls alias."""
|
||||
args = args_namespace(command="sb", sandbox_command="ls", json=False, show_passwords=False)
|
||||
|
||||
with patch.object(sandbox, "cmd_list", return_value=0) as mock_cmd:
|
||||
sandbox.execute(args)
|
||||
|
||||
mock_cmd.assert_called_once_with(args)
|
||||
|
||||
def test_unknown_command_returns_error(self, args_namespace):
|
||||
"""Test that unknown command returns error."""
|
||||
args = args_namespace(command="sandbox", sandbox_command=None)
|
||||
|
||||
with patch.object(sandbox, "print_error") as mock_error:
|
||||
result = sandbox.execute(args)
|
||||
|
||||
assert result == 1
|
||||
mock_error.assert_called()
|
||||
|
||||
|
||||
class TestCmdList:
|
||||
"""Tests for cmd_list function."""
|
||||
|
||||
def test_list_sandboxes_success(self, args_namespace, sample_vm_list, mock_api_key):
|
||||
"""Test listing sandboxes successfully."""
|
||||
args = args_namespace(json=False, show_passwords=False)
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.list_vms = AsyncMock(return_value=sample_vm_list)
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_table") as mock_print:
|
||||
result = sandbox.cmd_list(args)
|
||||
|
||||
assert result == 0
|
||||
mock_print.assert_called_once()
|
||||
|
||||
def test_list_sandboxes_empty(self, args_namespace, mock_api_key):
|
||||
"""Test listing when no sandboxes exist."""
|
||||
args = args_namespace(json=False, show_passwords=False)
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.list_vms = AsyncMock(return_value=[])
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_info") as mock_print:
|
||||
result = sandbox.cmd_list(args)
|
||||
|
||||
assert result == 0
|
||||
mock_print.assert_called_with("No sandboxes found.")
|
||||
|
||||
def test_list_sandboxes_json_output(self, args_namespace, sample_vm_list, mock_api_key):
|
||||
"""Test JSON output format."""
|
||||
args = args_namespace(json=True, show_passwords=False)
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.list_vms = AsyncMock(return_value=sample_vm_list)
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_json") as mock_print:
|
||||
result = sandbox.cmd_list(args)
|
||||
|
||||
assert result == 0
|
||||
mock_print.assert_called_once_with(sample_vm_list)
|
||||
|
||||
|
||||
class TestCmdCreate:
|
||||
"""Tests for cmd_create function."""
|
||||
|
||||
def test_create_sandbox_success(self, args_namespace, mock_api_key):
|
||||
"""Test creating a sandbox successfully."""
|
||||
args = args_namespace(
|
||||
os="linux",
|
||||
size="medium",
|
||||
region="north-america",
|
||||
json=False,
|
||||
)
|
||||
|
||||
# Mock the API response (status 202 = provisioning)
|
||||
async def mock_api_request(*args, **kwargs):
|
||||
return (
|
||||
202,
|
||||
{
|
||||
"status": "provisioning",
|
||||
"name": "new-sandbox",
|
||||
"host": "sandbox.example.com",
|
||||
},
|
||||
)
|
||||
|
||||
with patch.object(sandbox, "_api_request", side_effect=mock_api_request):
|
||||
with patch.object(sandbox, "print_info"):
|
||||
result = sandbox.cmd_create(args)
|
||||
|
||||
assert result == 0
|
||||
|
||||
def test_create_sandbox_error(self, args_namespace, mock_api_key):
|
||||
"""Test handling create error."""
|
||||
args = args_namespace(
|
||||
os="linux",
|
||||
size="medium",
|
||||
region="north-america",
|
||||
json=False,
|
||||
)
|
||||
|
||||
# Mock the API response (status 500 = error)
|
||||
async def mock_api_request(*args, **kwargs):
|
||||
return (500, "Quota exceeded")
|
||||
|
||||
with patch.object(sandbox, "_api_request", side_effect=mock_api_request):
|
||||
with patch.object(sandbox, "print_error") as mock_error:
|
||||
result = sandbox.cmd_create(args)
|
||||
|
||||
assert result == 1
|
||||
mock_error.assert_called()
|
||||
|
||||
|
||||
class TestCmdGet:
|
||||
"""Tests for cmd_get function."""
|
||||
|
||||
def test_get_sandbox_success(self, args_namespace, sample_vm, mock_api_key):
|
||||
"""Test getting sandbox details."""
|
||||
args = args_namespace(
|
||||
name="test-sandbox-1",
|
||||
json=False,
|
||||
show_passwords=False,
|
||||
show_vnc_url=False,
|
||||
)
|
||||
|
||||
vm_dict = {
|
||||
"name": sample_vm.name,
|
||||
"status": sample_vm.status,
|
||||
"os_type": sample_vm.os_type,
|
||||
"host": "sandbox.example.com",
|
||||
}
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.list_vms = AsyncMock(return_value=[vm_dict])
|
||||
mock_provider.get_vm = AsyncMock(return_value={"status": "running"})
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_info"):
|
||||
result = sandbox.cmd_get(args)
|
||||
|
||||
assert result == 0
|
||||
|
||||
def test_get_sandbox_not_found(self, args_namespace, mock_api_key):
|
||||
"""Test getting nonexistent sandbox."""
|
||||
args = args_namespace(
|
||||
name="nonexistent",
|
||||
json=False,
|
||||
show_passwords=False,
|
||||
show_vnc_url=False,
|
||||
)
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.list_vms = AsyncMock(return_value=[])
|
||||
mock_provider.get_vm = AsyncMock(return_value={"status": "not_found"})
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_error") as mock_error:
|
||||
result = sandbox.cmd_get(args)
|
||||
|
||||
assert result == 1
|
||||
mock_error.assert_called()
|
||||
|
||||
|
||||
class TestCmdStart:
|
||||
"""Tests for cmd_start function."""
|
||||
|
||||
def test_start_sandbox_success(self, args_namespace, mock_api_key):
|
||||
"""Test starting a sandbox."""
|
||||
args = args_namespace(name="test-sandbox")
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.run_vm = AsyncMock(return_value={"status": "starting"})
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_success"):
|
||||
result = sandbox.cmd_start(args)
|
||||
|
||||
assert result == 0
|
||||
|
||||
def test_start_sandbox_not_found(self, args_namespace, mock_api_key):
|
||||
"""Test starting nonexistent sandbox."""
|
||||
args = args_namespace(name="nonexistent")
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.run_vm = AsyncMock(return_value={"status": "not_found"})
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_error"):
|
||||
result = sandbox.cmd_start(args)
|
||||
|
||||
assert result == 1
|
||||
|
||||
|
||||
class TestCmdStop:
|
||||
"""Tests for cmd_stop function."""
|
||||
|
||||
def test_stop_sandbox_success(self, args_namespace, mock_api_key):
|
||||
"""Test stopping a sandbox."""
|
||||
args = args_namespace(name="test-sandbox")
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.stop_vm = AsyncMock(return_value={"status": "stopping"})
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_success"):
|
||||
result = sandbox.cmd_stop(args)
|
||||
|
||||
assert result == 0
|
||||
|
||||
|
||||
class TestCmdRestart:
|
||||
"""Tests for cmd_restart function."""
|
||||
|
||||
def test_restart_sandbox_success(self, args_namespace, mock_api_key):
|
||||
"""Test restarting a sandbox."""
|
||||
args = args_namespace(name="test-sandbox")
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.restart_vm = AsyncMock(return_value={"status": "restarting"})
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_success"):
|
||||
result = sandbox.cmd_restart(args)
|
||||
|
||||
assert result == 0
|
||||
|
||||
|
||||
class TestCmdSuspend:
|
||||
"""Tests for cmd_suspend function."""
|
||||
|
||||
def test_suspend_sandbox_success(self, args_namespace, mock_api_key):
|
||||
"""Test suspending a sandbox."""
|
||||
args = args_namespace(name="test-sandbox")
|
||||
|
||||
# Mock the API response (status 202 = suspending)
|
||||
async def mock_api_request(*args, **kwargs):
|
||||
return (202, {"status": "suspending"})
|
||||
|
||||
with patch.object(sandbox, "_api_request", side_effect=mock_api_request):
|
||||
with patch.object(sandbox, "print_success"):
|
||||
result = sandbox.cmd_suspend(args)
|
||||
|
||||
assert result == 0
|
||||
|
||||
def test_suspend_unsupported(self, args_namespace, mock_api_key):
|
||||
"""Test suspend on unsupported sandbox."""
|
||||
args = args_namespace(name="test-sandbox")
|
||||
|
||||
# Mock the API response (status 400 = unsupported)
|
||||
async def mock_api_request(*args, **kwargs):
|
||||
return (400, "Suspend not supported for Windows")
|
||||
|
||||
with patch.object(sandbox, "_api_request", side_effect=mock_api_request):
|
||||
with patch.object(sandbox, "print_error"):
|
||||
result = sandbox.cmd_suspend(args)
|
||||
|
||||
assert result == 1
|
||||
|
||||
|
||||
class TestCmdDelete:
|
||||
"""Tests for cmd_delete function."""
|
||||
|
||||
def test_delete_sandbox_success(self, args_namespace, mock_api_key):
|
||||
"""Test deleting a sandbox."""
|
||||
args = args_namespace(name="test-sandbox")
|
||||
|
||||
# Mock the API response (status 202 = deleting)
|
||||
async def mock_api_request(*args, **kwargs):
|
||||
return (202, {"status": "deleting"})
|
||||
|
||||
with patch.object(sandbox, "_api_request", side_effect=mock_api_request):
|
||||
with patch.object(sandbox, "print_success"):
|
||||
result = sandbox.cmd_delete(args)
|
||||
|
||||
assert result == 0
|
||||
|
||||
|
||||
class TestCmdVnc:
|
||||
"""Tests for cmd_vnc function."""
|
||||
|
||||
def test_vnc_opens_browser(self, args_namespace, mock_api_key, mock_webbrowser):
|
||||
"""Test VNC opens browser with correct URL."""
|
||||
args = args_namespace(name="test-sandbox")
|
||||
|
||||
vm_info = {
|
||||
"name": "test-sandbox",
|
||||
"vnc_url": "https://vnc.example.com/test",
|
||||
}
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.list_vms = AsyncMock(return_value=[vm_info])
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_info"):
|
||||
result = sandbox.cmd_vnc(args)
|
||||
|
||||
assert result == 0
|
||||
mock_webbrowser.assert_called_once_with("https://vnc.example.com/test")
|
||||
|
||||
def test_vnc_constructs_url_from_host(self, args_namespace, mock_api_key, mock_webbrowser):
|
||||
"""Test VNC constructs URL when vnc_url not provided."""
|
||||
args = args_namespace(name="test-sandbox")
|
||||
|
||||
vm_info = {
|
||||
"name": "test-sandbox",
|
||||
"host": "sandbox.example.com",
|
||||
"password": "secret123",
|
||||
}
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.list_vms = AsyncMock(return_value=[vm_info])
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_info"):
|
||||
result = sandbox.cmd_vnc(args)
|
||||
|
||||
assert result == 0
|
||||
mock_webbrowser.assert_called_once()
|
||||
# Check URL contains host and encoded password
|
||||
call_url = mock_webbrowser.call_args[0][0]
|
||||
assert "sandbox.example.com" in call_url
|
||||
assert "secret123" in call_url
|
||||
|
||||
def test_vnc_sandbox_not_found(self, args_namespace, mock_api_key):
|
||||
"""Test VNC with nonexistent sandbox."""
|
||||
args = args_namespace(name="nonexistent")
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.list_vms = AsyncMock(return_value=[])
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_error"):
|
||||
result = sandbox.cmd_vnc(args)
|
||||
|
||||
assert result == 1
|
||||
|
||||
|
||||
class TestCmdShell:
|
||||
"""Tests for cmd_shell function."""
|
||||
|
||||
def test_shell_parser_registration(self):
|
||||
"""Test that shell command is registered with correct arguments."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
sandbox.register_parser(subparsers)
|
||||
|
||||
# Test basic shell command
|
||||
args = parser.parse_args(["sb", "shell", "my-sandbox"])
|
||||
assert args.name == "my-sandbox"
|
||||
assert args.sandbox_command == "shell"
|
||||
|
||||
def test_shell_with_command(self):
|
||||
"""Test shell command with a command to execute."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
sandbox.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["sb", "shell", "my-sandbox", "ls", "-la"])
|
||||
assert args.name == "my-sandbox"
|
||||
assert args.shell_command == ["ls", "-la"]
|
||||
|
||||
def test_shell_with_cols_rows(self):
|
||||
"""Test shell command with terminal size options.
|
||||
|
||||
Options must come before name due to argparse REMAINDER behavior.
|
||||
Usage: cua sb shell --cols 120 --rows 40 mybox [command...]
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
sandbox.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(
|
||||
["sb", "shell", "--cols", "120", "--rows", "40", "my-sandbox", "ls"]
|
||||
)
|
||||
assert args.cols == 120
|
||||
assert args.rows == 40
|
||||
assert args.name == "my-sandbox"
|
||||
assert args.shell_command == ["ls"]
|
||||
|
||||
def test_shell_sandbox_not_found(self, args_namespace, mock_api_key):
|
||||
"""Test shell with nonexistent sandbox."""
|
||||
args = args_namespace(
|
||||
name="nonexistent",
|
||||
shell_command=[],
|
||||
cols=None,
|
||||
rows=None,
|
||||
)
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.get_vm = AsyncMock(return_value={"status": "not_found"})
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_error") as mock_error:
|
||||
with patch("sys.stdin") as mock_stdin:
|
||||
mock_stdin.isatty.return_value = False
|
||||
result = sandbox.cmd_shell(args)
|
||||
|
||||
assert result == 1
|
||||
mock_error.assert_called()
|
||||
|
||||
def test_shell_no_api_url(self, args_namespace, mock_api_key):
|
||||
"""Test shell when sandbox has no API URL."""
|
||||
args = args_namespace(
|
||||
name="test-sandbox",
|
||||
shell_command=["echo", "hello"],
|
||||
cols=None,
|
||||
rows=None,
|
||||
)
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.get_vm = AsyncMock(return_value={"status": "stopped", "api_url": None})
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_error") as mock_error:
|
||||
with patch("sys.stdin") as mock_stdin:
|
||||
mock_stdin.isatty.return_value = False
|
||||
result = sandbox.cmd_shell(args)
|
||||
|
||||
assert result == 1
|
||||
mock_error.assert_called()
|
||||
|
||||
|
||||
class TestCmdExec:
|
||||
"""Tests for cmd_exec function."""
|
||||
|
||||
def test_exec_parser_registration(self):
|
||||
"""Test that exec command is registered with correct arguments."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
sandbox.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["sb", "exec", "my-sandbox", "echo", "hello"])
|
||||
assert args.name == "my-sandbox"
|
||||
assert args.sandbox_command == "exec"
|
||||
assert args.exec_command == ["echo", "hello"]
|
||||
|
||||
def test_exec_with_json_flag(self):
|
||||
"""Test exec command with --json flag.
|
||||
|
||||
--json must come before name due to argparse REMAINDER behavior.
|
||||
Usage: cua sb exec --json mybox <command...>
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
sandbox.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["sb", "exec", "--json", "my-sandbox", "echo", "hello"])
|
||||
assert args.json is True
|
||||
assert args.name == "my-sandbox"
|
||||
assert args.exec_command == ["echo", "hello"]
|
||||
|
||||
def test_exec_no_command_error(self, args_namespace, mock_api_key):
|
||||
"""Test exec with no command returns error."""
|
||||
args = args_namespace(
|
||||
name="test-sandbox",
|
||||
exec_command=[],
|
||||
json=False,
|
||||
)
|
||||
|
||||
with patch.object(sandbox, "print_error") as mock_error:
|
||||
result = sandbox.cmd_exec(args)
|
||||
|
||||
assert result == 1
|
||||
mock_error.assert_called_with("No command provided")
|
||||
|
||||
def test_exec_sandbox_not_found(self, args_namespace, mock_api_key):
|
||||
"""Test exec with nonexistent sandbox."""
|
||||
args = args_namespace(
|
||||
name="nonexistent",
|
||||
exec_command=["echo", "hello"],
|
||||
json=False,
|
||||
)
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.get_vm = AsyncMock(return_value={"status": "not_found"})
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "print_error") as mock_error:
|
||||
result = sandbox.cmd_exec(args)
|
||||
|
||||
assert result == 1
|
||||
mock_error.assert_called()
|
||||
|
||||
def test_exec_success(self, args_namespace, mock_api_key, capsys):
|
||||
"""Test successful command execution."""
|
||||
args = args_namespace(
|
||||
name="test-sandbox",
|
||||
exec_command=["echo", "hello"],
|
||||
json=False,
|
||||
)
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.get_vm = AsyncMock(
|
||||
return_value={"status": "running", "api_url": "https://sandbox.example.com:8443"}
|
||||
)
|
||||
|
||||
async def mock_exec(*a, **kw):
|
||||
return {"success": True, "stdout": "hello\n", "stderr": "", "returncode": 0}
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "_exec_noninteractive", side_effect=mock_exec):
|
||||
result = sandbox.cmd_exec(args)
|
||||
|
||||
assert result == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "hello" in captured.out
|
||||
|
||||
def test_exec_json_output(self, args_namespace, mock_api_key):
|
||||
"""Test exec with JSON output."""
|
||||
args = args_namespace(
|
||||
name="test-sandbox",
|
||||
exec_command=["echo", "hello"],
|
||||
json=True,
|
||||
)
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.get_vm = AsyncMock(
|
||||
return_value={"status": "running", "api_url": "https://sandbox.example.com:8443"}
|
||||
)
|
||||
|
||||
async def mock_exec(*a, **kw):
|
||||
return {"success": True, "stdout": "hello\n", "stderr": "", "returncode": 0}
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "_exec_noninteractive", side_effect=mock_exec):
|
||||
with patch.object(sandbox, "print_json") as mock_json:
|
||||
result = sandbox.cmd_exec(args)
|
||||
|
||||
assert result == 0
|
||||
mock_json.assert_called_once()
|
||||
|
||||
def test_exec_command_failure(self, args_namespace, mock_api_key, capsys):
|
||||
"""Test exec when command returns non-zero exit code."""
|
||||
args = args_namespace(
|
||||
name="test-sandbox",
|
||||
exec_command=["false"],
|
||||
json=False,
|
||||
)
|
||||
|
||||
mock_provider = MagicMock()
|
||||
mock_provider.__aenter__ = AsyncMock(return_value=mock_provider)
|
||||
mock_provider.__aexit__ = AsyncMock(return_value=None)
|
||||
mock_provider.get_vm = AsyncMock(
|
||||
return_value={"status": "running", "api_url": "https://sandbox.example.com:8443"}
|
||||
)
|
||||
|
||||
async def mock_exec(*a, **kw):
|
||||
return {"success": True, "stdout": "", "stderr": "error", "returncode": 1}
|
||||
|
||||
with patch.object(sandbox, "_get_provider", return_value=mock_provider):
|
||||
with patch.object(sandbox, "_exec_noninteractive", side_effect=mock_exec):
|
||||
result = sandbox.cmd_exec(args)
|
||||
|
||||
assert result == 1
|
||||
@@ -0,0 +1,235 @@
|
||||
"""Tests for skills command module."""
|
||||
|
||||
import argparse
|
||||
from unittest.mock import patch
|
||||
|
||||
from cua_cli.commands import skills
|
||||
|
||||
|
||||
class TestRegisterParser:
|
||||
"""Tests for register_parser function."""
|
||||
|
||||
def test_registers_skills_command(self):
|
||||
"""Test that skills command is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
skills.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["skills", "list"])
|
||||
assert args.skills_command == "list"
|
||||
|
||||
def test_record_command_exists(self):
|
||||
"""Test that record command is registered."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
skills.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["skills", "record"])
|
||||
assert args.skills_command == "record"
|
||||
|
||||
def test_record_command_has_sandbox_flag(self):
|
||||
"""Test that record command has --sandbox flag."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
skills.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["skills", "record", "--sandbox", "my-sandbox"])
|
||||
assert args.sandbox == "my-sandbox"
|
||||
|
||||
def test_read_command_has_name_arg(self):
|
||||
"""Test that read command has name argument."""
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
skills.register_parser(subparsers)
|
||||
|
||||
args = parser.parse_args(["skills", "read", "my-skill"])
|
||||
assert args.name == "my-skill"
|
||||
|
||||
|
||||
class TestExecute:
|
||||
"""Tests for execute function."""
|
||||
|
||||
def test_dispatch_to_list(self, args_namespace):
|
||||
"""Test dispatch to list command."""
|
||||
args = args_namespace(command="skills", skills_command="list", json=False)
|
||||
|
||||
with patch.object(skills, "cmd_list", return_value=0) as mock_cmd:
|
||||
result = skills.execute(args)
|
||||
|
||||
mock_cmd.assert_called_once_with(args)
|
||||
assert result == 0
|
||||
|
||||
def test_dispatch_to_read(self, args_namespace):
|
||||
"""Test dispatch to read command."""
|
||||
args = args_namespace(command="skills", skills_command="read", name="my-skill")
|
||||
|
||||
with patch.object(skills, "cmd_read", return_value=0) as mock_cmd:
|
||||
skills.execute(args)
|
||||
|
||||
mock_cmd.assert_called_once_with(args)
|
||||
|
||||
def test_dispatch_to_delete(self, args_namespace):
|
||||
"""Test dispatch to delete command."""
|
||||
args = args_namespace(command="skills", skills_command="delete", name="my-skill")
|
||||
|
||||
with patch.object(skills, "cmd_delete", return_value=0) as mock_cmd:
|
||||
skills.execute(args)
|
||||
|
||||
mock_cmd.assert_called_once_with(args)
|
||||
|
||||
def test_unknown_command_returns_error(self, args_namespace):
|
||||
"""Test that unknown command returns error."""
|
||||
args = args_namespace(command="skills", skills_command=None)
|
||||
|
||||
with patch.object(skills, "print_error"):
|
||||
result = skills.execute(args)
|
||||
|
||||
assert result == 1
|
||||
|
||||
|
||||
class TestCmdList:
|
||||
"""Tests for cmd_list function."""
|
||||
|
||||
def test_list_empty_directory(self, args_namespace, temp_skills_dir):
|
||||
"""Test listing when no skills exist."""
|
||||
args = args_namespace(json=False)
|
||||
|
||||
with patch.object(skills, "print_info") as mock_print:
|
||||
result = skills.cmd_list(args)
|
||||
|
||||
assert result == 0
|
||||
mock_print.assert_called()
|
||||
|
||||
def test_list_with_skills(self, args_namespace, sample_skill, temp_skills_dir):
|
||||
"""Test listing existing skills."""
|
||||
args = args_namespace(json=False)
|
||||
|
||||
with patch.object(skills, "print_table") as mock_print:
|
||||
result = skills.cmd_list(args)
|
||||
|
||||
assert result == 0
|
||||
mock_print.assert_called_once()
|
||||
# Verify the skill is in the output
|
||||
call_args = mock_print.call_args[0]
|
||||
skill_data = call_args[0]
|
||||
assert len(skill_data) == 1
|
||||
assert skill_data[0]["name"] == "test-skill"
|
||||
|
||||
def test_list_json_output(self, args_namespace, sample_skill, temp_skills_dir):
|
||||
"""Test JSON output format."""
|
||||
args = args_namespace(json=True)
|
||||
|
||||
with patch.object(skills, "print_json") as mock_print:
|
||||
result = skills.cmd_list(args)
|
||||
|
||||
assert result == 0
|
||||
mock_print.assert_called_once()
|
||||
|
||||
|
||||
class TestCmdRead:
|
||||
"""Tests for cmd_read function."""
|
||||
|
||||
def test_read_existing_skill(self, args_namespace, sample_skill, temp_skills_dir, capsys):
|
||||
"""Test reading an existing skill."""
|
||||
args = args_namespace(name="test-skill", format="md")
|
||||
|
||||
result = skills.cmd_read(args)
|
||||
|
||||
assert result == 0
|
||||
# Verify content was printed to stdout
|
||||
captured = capsys.readouterr()
|
||||
assert "test-skill" in captured.out
|
||||
|
||||
def test_read_nonexistent_skill(self, args_namespace, temp_skills_dir):
|
||||
"""Test reading a nonexistent skill."""
|
||||
args = args_namespace(name="nonexistent", format="md")
|
||||
|
||||
with patch.object(skills, "print_error") as mock_error:
|
||||
result = skills.cmd_read(args)
|
||||
|
||||
assert result == 1
|
||||
mock_error.assert_called()
|
||||
|
||||
|
||||
class TestCmdDelete:
|
||||
"""Tests for cmd_delete function."""
|
||||
|
||||
def test_delete_existing_skill(self, args_namespace, sample_skill, temp_skills_dir):
|
||||
"""Test deleting an existing skill."""
|
||||
args = args_namespace(name="test-skill")
|
||||
|
||||
assert sample_skill.exists()
|
||||
|
||||
with patch.object(skills, "print_success"):
|
||||
result = skills.cmd_delete(args)
|
||||
|
||||
assert result == 0
|
||||
assert not sample_skill.exists()
|
||||
|
||||
def test_delete_nonexistent_skill(self, args_namespace, temp_skills_dir):
|
||||
"""Test deleting a nonexistent skill."""
|
||||
args = args_namespace(name="nonexistent")
|
||||
|
||||
with patch.object(skills, "print_error") as mock_error:
|
||||
result = skills.cmd_delete(args)
|
||||
|
||||
assert result == 1
|
||||
mock_error.assert_called()
|
||||
|
||||
|
||||
class TestCmdClean:
|
||||
"""Tests for cmd_clean function."""
|
||||
|
||||
def test_clean_removes_all_skills(self, args_namespace, sample_skill, temp_skills_dir):
|
||||
"""Test that clean removes all skills."""
|
||||
args = args_namespace()
|
||||
|
||||
assert sample_skill.exists()
|
||||
|
||||
with patch("builtins.input", return_value="y"):
|
||||
with patch.object(skills, "print_success"):
|
||||
result = skills.cmd_clean(args)
|
||||
|
||||
assert result == 0
|
||||
# Verify no skills remain
|
||||
remaining = list(temp_skills_dir.iterdir())
|
||||
assert len(remaining) == 0
|
||||
|
||||
def test_clean_empty_directory(self, args_namespace, temp_skills_dir):
|
||||
"""Test clean on empty directory."""
|
||||
args = args_namespace()
|
||||
|
||||
with patch.object(skills, "print_info"):
|
||||
result = skills.cmd_clean(args)
|
||||
|
||||
assert result == 0
|
||||
|
||||
|
||||
class TestCmdReplay:
|
||||
"""Tests for cmd_replay function."""
|
||||
|
||||
def test_replay_existing_skill(
|
||||
self, args_namespace, sample_skill, temp_skills_dir, mock_webbrowser
|
||||
):
|
||||
"""Test replaying an existing skill opens the video."""
|
||||
args = args_namespace(name="test-skill")
|
||||
|
||||
with patch.object(skills, "print_info"):
|
||||
result = skills.cmd_replay(args)
|
||||
|
||||
assert result == 0
|
||||
# Verify webbrowser was called with the video path
|
||||
mock_webbrowser.assert_called_once()
|
||||
call_url = mock_webbrowser.call_args[0][0]
|
||||
assert "test-skill.mp4" in call_url
|
||||
|
||||
def test_replay_nonexistent_skill(self, args_namespace, temp_skills_dir):
|
||||
"""Test replaying a nonexistent skill."""
|
||||
args = args_namespace(name="nonexistent")
|
||||
|
||||
with patch.object(skills, "print_error") as mock_error:
|
||||
result = skills.cmd_replay(args)
|
||||
|
||||
assert result == 1
|
||||
mock_error.assert_called()
|
||||
@@ -0,0 +1,254 @@
|
||||
"""Shared test fixtures for cua-cli tests."""
|
||||
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Mock external modules before importing cua_cli
|
||||
# This allows tests to run without cua-computer installed
|
||||
mock_providers = MagicMock()
|
||||
mock_providers.VMProviderFactory = MagicMock()
|
||||
mock_providers.VMProviderType = MagicMock()
|
||||
mock_providers.VMProviderType.CLOUD = "cloud"
|
||||
|
||||
mock_computer = MagicMock()
|
||||
mock_computer.providers = mock_providers
|
||||
mock_computer.providers.cloud = MagicMock()
|
||||
mock_computer.providers.cloud.provider = MagicMock()
|
||||
mock_computer.providers.cloud.provider.CloudProvider = MagicMock()
|
||||
|
||||
sys.modules["computer"] = mock_computer
|
||||
sys.modules["computer.providers"] = mock_providers
|
||||
sys.modules["computer.providers.cloud"] = mock_computer.providers.cloud
|
||||
sys.modules["computer.providers.cloud.provider"] = mock_computer.providers.cloud.provider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def disable_telemetry(monkeypatch):
|
||||
"""Disable telemetry for tests."""
|
||||
monkeypatch.setenv("CUA_TELEMETRY_ENABLED", "false")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_credentials_db(tmp_path, monkeypatch):
|
||||
"""Provide a temporary credentials database."""
|
||||
cua_dir = tmp_path / ".cua"
|
||||
cua_dir.mkdir(parents=True)
|
||||
db_path = cua_dir / "credentials.db"
|
||||
|
||||
# Patch the CREDENTIALS_DB path in the store module
|
||||
monkeypatch.setattr("cua_cli.auth.store.CREDENTIALS_DB", db_path)
|
||||
|
||||
yield db_path
|
||||
|
||||
if db_path.exists():
|
||||
db_path.unlink()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_api_key(monkeypatch):
|
||||
"""Set a mock API key in environment."""
|
||||
monkeypatch.setenv("CUA_API_KEY", "test-api-key-12345")
|
||||
return "test-api-key-12345"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clear_api_key_env(monkeypatch):
|
||||
"""Ensure no API key is in environment."""
|
||||
monkeypatch.delenv("CUA_API_KEY", raising=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_aiohttp_session():
|
||||
"""Mock aiohttp.ClientSession for API tests."""
|
||||
with patch("aiohttp.ClientSession") as mock_session:
|
||||
mock_instance = AsyncMock()
|
||||
mock_session.return_value.__aenter__.return_value = mock_instance
|
||||
mock_session.return_value.__aexit__.return_value = None
|
||||
yield mock_instance
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cloud_provider():
|
||||
"""Mock CloudProvider for sandbox tests."""
|
||||
with patch("cua_cli.commands.sandbox.VMProviderFactory") as mock_factory:
|
||||
mock_provider = AsyncMock()
|
||||
mock_provider.__aenter__.return_value = mock_provider
|
||||
mock_provider.__aexit__.return_value = None
|
||||
mock_factory.create_provider.return_value = mock_provider
|
||||
yield mock_provider
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_vm():
|
||||
"""Sample VM object for testing."""
|
||||
vm = MagicMock()
|
||||
vm.name = "test-sandbox-1"
|
||||
vm.status = "running"
|
||||
vm.os_type = "linux"
|
||||
vm.created_at = "2024-01-15T10:00:00Z"
|
||||
vm.size = "medium"
|
||||
vm.region = "north-america"
|
||||
vm.vnc_url = "https://vnc.example.com/test-sandbox-1"
|
||||
vm.server_url = "https://server.example.com:8000"
|
||||
return vm
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_vm_list(sample_vm):
|
||||
"""Sample VM list for testing."""
|
||||
vm2 = MagicMock()
|
||||
vm2.name = "test-sandbox-2"
|
||||
vm2.status = "stopped"
|
||||
vm2.os_type = "macos"
|
||||
vm2.created_at = "2024-01-14T08:30:00Z"
|
||||
vm2.size = "large"
|
||||
vm2.region = "europe"
|
||||
vm2.vnc_url = None
|
||||
vm2.server_url = None
|
||||
return [sample_vm, vm2]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_skills_dir(tmp_path, monkeypatch):
|
||||
"""Provide a temporary skills directory."""
|
||||
skills_dir = tmp_path / ".cua" / "skills"
|
||||
skills_dir.mkdir(parents=True)
|
||||
|
||||
# Patch the SKILLS_DIR in the skills module
|
||||
monkeypatch.setattr("cua_cli.commands.skills.SKILLS_DIR", skills_dir)
|
||||
|
||||
return skills_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_skill(temp_skills_dir):
|
||||
"""Create a sample skill for testing."""
|
||||
skill_dir = temp_skills_dir / "test-skill"
|
||||
skill_dir.mkdir()
|
||||
|
||||
# Create SKILL.md with proper frontmatter format expected by _parse_frontmatter
|
||||
skill_file = skill_dir / "SKILL.md"
|
||||
skill_file.write_text("""---
|
||||
name: test-skill
|
||||
description: A sample skill for testing
|
||||
---
|
||||
|
||||
# Test Skill
|
||||
|
||||
A sample skill for testing.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Click on the button
|
||||
2. Type some text
|
||||
3. Press Enter
|
||||
""")
|
||||
|
||||
# Create trajectory directory with trajectory.json and a video file
|
||||
trajectory_dir = skill_dir / "trajectory"
|
||||
trajectory_dir.mkdir()
|
||||
|
||||
# Create trajectory.json for proper skill info extraction
|
||||
trajectory_json = trajectory_dir / "trajectory.json"
|
||||
trajectory_json.write_text("""{
|
||||
"trajectory": [
|
||||
{"step_idx": 1, "caption": {"action": "click"}},
|
||||
{"step_idx": 2, "caption": {"action": "type"}}
|
||||
],
|
||||
"metadata": {
|
||||
"created_at": "2024-01-15T10:00:00Z"
|
||||
}
|
||||
}""")
|
||||
|
||||
# Create a fake MP4 file for replay tests
|
||||
video_file = trajectory_dir / "test-skill.mp4"
|
||||
video_file.write_bytes(b"fake mp4 content")
|
||||
|
||||
return skill_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_image_api_client():
|
||||
"""Mock CloudAPIClient for image tests."""
|
||||
with patch("cua_cli.commands.image.CloudAPIClient") as mock_client_class:
|
||||
mock_client = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
yield mock_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_cloud_images():
|
||||
"""Sample cloud images for testing."""
|
||||
return [
|
||||
{
|
||||
"name": "ubuntu-22.04",
|
||||
"image_type": "qcow2",
|
||||
"created_at": "2024-01-10T12:00:00Z",
|
||||
"versions": [
|
||||
{
|
||||
"tag": "latest",
|
||||
"size_bytes": 5368709120,
|
||||
"status": "ready",
|
||||
"created_at": "2024-01-10T12:00:00Z",
|
||||
},
|
||||
{
|
||||
"tag": "v1.0",
|
||||
"size_bytes": 5000000000,
|
||||
"status": "ready",
|
||||
"created_at": "2024-01-05T10:00:00Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "macos-sonoma",
|
||||
"image_type": "qcow2",
|
||||
"created_at": "2024-01-08T09:00:00Z",
|
||||
"versions": [
|
||||
{
|
||||
"tag": "latest",
|
||||
"size_bytes": 21474836480,
|
||||
"status": "ready",
|
||||
"created_at": "2024-01-08T09:00:00Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_image_file(tmp_path):
|
||||
"""Create a temporary image file for upload tests."""
|
||||
image_file = tmp_path / "test-image" / "data.img"
|
||||
image_file.parent.mkdir(parents=True)
|
||||
# Create a small test file
|
||||
image_file.write_bytes(b"fake image content" * 1000)
|
||||
return image_file
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_webbrowser():
|
||||
"""Mock webbrowser.open for browser tests."""
|
||||
with patch("webbrowser.open") as mock_open:
|
||||
yield mock_open
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_rich_console():
|
||||
"""Mock rich console for output tests."""
|
||||
with patch("cua_cli.utils.output.Console") as mock_console_class:
|
||||
mock_console = MagicMock()
|
||||
mock_console_class.return_value = mock_console
|
||||
yield mock_console
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def args_namespace():
|
||||
"""Create an argparse.Namespace factory for command tests."""
|
||||
import argparse
|
||||
|
||||
def create_args(**kwargs):
|
||||
return argparse.Namespace(**kwargs)
|
||||
|
||||
return create_args
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Tests for the main CLI entry point."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from cua_cli.main import create_parser, main
|
||||
|
||||
|
||||
class TestCreateParser:
|
||||
"""Tests for create_parser function."""
|
||||
|
||||
def test_creates_parser(self):
|
||||
"""Test that parser is created."""
|
||||
parser = create_parser()
|
||||
|
||||
assert isinstance(parser, argparse.ArgumentParser)
|
||||
assert parser.prog == "cua"
|
||||
|
||||
def test_has_version_flag(self):
|
||||
"""Test that --version flag is present."""
|
||||
parser = create_parser()
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
parser.parse_args(["--version"])
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
|
||||
def test_has_auth_command(self):
|
||||
"""Test that auth command is registered."""
|
||||
parser = create_parser()
|
||||
|
||||
args = parser.parse_args(["auth", "login"])
|
||||
assert args.command == "auth"
|
||||
assert args.auth_command == "login"
|
||||
|
||||
def test_has_sandbox_command(self):
|
||||
"""Test that sandbox command is registered."""
|
||||
parser = create_parser()
|
||||
|
||||
args = parser.parse_args(["sandbox", "list"])
|
||||
assert args.command == "sandbox"
|
||||
assert args.sandbox_command == "list"
|
||||
|
||||
def test_has_sb_alias(self):
|
||||
"""Test that sb alias works."""
|
||||
parser = create_parser()
|
||||
|
||||
args = parser.parse_args(["sb", "list"])
|
||||
assert args.command == "sb"
|
||||
assert args.sandbox_command == "list"
|
||||
|
||||
def test_has_image_command(self):
|
||||
"""Test that image command is registered."""
|
||||
parser = create_parser()
|
||||
|
||||
args = parser.parse_args(["image", "list"])
|
||||
assert args.command == "image"
|
||||
assert args.image_command == "list"
|
||||
|
||||
def test_has_skills_command(self):
|
||||
"""Test that skills command is registered."""
|
||||
parser = create_parser()
|
||||
|
||||
args = parser.parse_args(["skills", "list"])
|
||||
assert args.command == "skills"
|
||||
assert args.skills_command == "list"
|
||||
|
||||
def test_has_serve_mcp_command(self):
|
||||
"""Test that serve-mcp command is registered."""
|
||||
parser = create_parser()
|
||||
|
||||
args = parser.parse_args(["serve-mcp"])
|
||||
assert args.command == "serve-mcp"
|
||||
|
||||
|
||||
class TestMain:
|
||||
"""Tests for main function."""
|
||||
|
||||
def test_no_args_shows_help(self, capsys):
|
||||
"""Test that no arguments shows help."""
|
||||
with patch.object(sys, "argv", ["cua"]):
|
||||
result = main()
|
||||
|
||||
assert result == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "usage:" in captured.out.lower() or "cua" in captured.out
|
||||
|
||||
def test_dispatch_to_auth(self):
|
||||
"""Test dispatch to auth command."""
|
||||
with patch.object(sys, "argv", ["cua", "auth", "logout"]):
|
||||
with patch("cua_cli.commands.auth.execute", return_value=0) as mock_execute:
|
||||
result = main()
|
||||
|
||||
mock_execute.assert_called_once()
|
||||
assert result == 0
|
||||
|
||||
def test_dispatch_to_sandbox(self):
|
||||
"""Test dispatch to sandbox command."""
|
||||
with patch.object(sys, "argv", ["cua", "sandbox", "list"]):
|
||||
with patch("cua_cli.commands.sandbox.execute", return_value=0) as mock_execute:
|
||||
result = main()
|
||||
|
||||
mock_execute.assert_called_once()
|
||||
assert result == 0
|
||||
|
||||
def test_dispatch_to_sb_alias(self):
|
||||
"""Test dispatch to sb alias."""
|
||||
with patch.object(sys, "argv", ["cua", "sb", "list"]):
|
||||
with patch("cua_cli.commands.sandbox.execute", return_value=0) as mock_execute:
|
||||
result = main()
|
||||
|
||||
mock_execute.assert_called_once()
|
||||
assert result == 0
|
||||
|
||||
def test_dispatch_to_image(self):
|
||||
"""Test dispatch to image command."""
|
||||
with patch.object(sys, "argv", ["cua", "image", "list"]):
|
||||
with patch("cua_cli.commands.image.execute", return_value=0) as mock_execute:
|
||||
result = main()
|
||||
|
||||
mock_execute.assert_called_once()
|
||||
assert result == 0
|
||||
|
||||
def test_dispatch_to_skills(self):
|
||||
"""Test dispatch to skills command."""
|
||||
with patch.object(sys, "argv", ["cua", "skills", "list"]):
|
||||
with patch("cua_cli.commands.skills.execute", return_value=0) as mock_execute:
|
||||
result = main()
|
||||
|
||||
mock_execute.assert_called_once()
|
||||
assert result == 0
|
||||
|
||||
def test_dispatch_to_mcp(self):
|
||||
"""Test dispatch to serve-mcp command."""
|
||||
with patch.object(sys, "argv", ["cua", "serve-mcp"]):
|
||||
with patch("cua_cli.commands.mcp.execute", return_value=0) as mock_execute:
|
||||
result = main()
|
||||
|
||||
mock_execute.assert_called_once()
|
||||
assert result == 0
|
||||
|
||||
def test_keyboard_interrupt_returns_130(self):
|
||||
"""Test that KeyboardInterrupt returns exit code 130."""
|
||||
with patch.object(sys, "argv", ["cua", "sandbox", "list"]):
|
||||
with patch("cua_cli.commands.sandbox.execute", side_effect=KeyboardInterrupt):
|
||||
result = main()
|
||||
|
||||
assert result == 130
|
||||
|
||||
def test_exception_returns_1(self):
|
||||
"""Test that exceptions return exit code 1."""
|
||||
with patch.object(sys, "argv", ["cua", "sandbox", "list"]):
|
||||
with patch("cua_cli.commands.sandbox.execute", side_effect=Exception("Test error")):
|
||||
result = main()
|
||||
|
||||
assert result == 1
|
||||
|
||||
def test_unknown_command_returns_1(self):
|
||||
"""Test that unknown commands are handled."""
|
||||
# Mock parse_args to return an unknown command
|
||||
with patch.object(sys, "argv", ["cua", "unknown"]):
|
||||
# This should trigger SystemExit from argparse
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
"""Utils module tests."""
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Tests for async utilities."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from cua_cli.utils.async_utils import run_async
|
||||
|
||||
|
||||
class TestRunAsync:
|
||||
"""Tests for run_async function."""
|
||||
|
||||
def test_runs_coroutine(self):
|
||||
"""Test that coroutine is executed."""
|
||||
|
||||
async def sample_coro():
|
||||
return "result"
|
||||
|
||||
result = run_async(sample_coro())
|
||||
|
||||
assert result == "result"
|
||||
|
||||
def test_returns_correct_value(self):
|
||||
"""Test that return value is correct."""
|
||||
|
||||
async def add(a, b):
|
||||
return a + b
|
||||
|
||||
result = run_async(add(2, 3))
|
||||
|
||||
assert result == 5
|
||||
|
||||
def test_propagates_exception(self):
|
||||
"""Test that exceptions are propagated."""
|
||||
|
||||
async def failing_coro():
|
||||
raise ValueError("Test error")
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
run_async(failing_coro())
|
||||
|
||||
assert "Test error" in str(exc_info.value)
|
||||
|
||||
def test_handles_async_sleep(self):
|
||||
"""Test that async sleep works correctly."""
|
||||
|
||||
async def with_sleep():
|
||||
await asyncio.sleep(0.01)
|
||||
return "done"
|
||||
|
||||
result = run_async(with_sleep())
|
||||
|
||||
assert result == "done"
|
||||
|
||||
def test_handles_nested_coroutines(self):
|
||||
"""Test that nested coroutines work."""
|
||||
|
||||
async def inner():
|
||||
return 42
|
||||
|
||||
async def outer():
|
||||
return await inner()
|
||||
|
||||
result = run_async(outer())
|
||||
|
||||
assert result == 42
|
||||
|
||||
def test_handles_none_result(self):
|
||||
"""Test that None result is handled."""
|
||||
|
||||
async def returns_none():
|
||||
pass
|
||||
|
||||
result = run_async(returns_none())
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_handles_list_result(self):
|
||||
"""Test that list result is handled."""
|
||||
|
||||
async def returns_list():
|
||||
return [1, 2, 3]
|
||||
|
||||
result = run_async(returns_list())
|
||||
|
||||
assert result == [1, 2, 3]
|
||||
|
||||
def test_handles_dict_result(self):
|
||||
"""Test that dict result is handled."""
|
||||
|
||||
async def returns_dict():
|
||||
return {"key": "value"}
|
||||
|
||||
result = run_async(returns_dict())
|
||||
|
||||
assert result == {"key": "value"}
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Tests for output utilities."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from cua_cli.utils.output import (
|
||||
print_error,
|
||||
print_info,
|
||||
print_json,
|
||||
print_success,
|
||||
print_table,
|
||||
print_warning,
|
||||
)
|
||||
|
||||
|
||||
class TestPrintTable:
|
||||
"""Tests for print_table function."""
|
||||
|
||||
def test_prints_table_with_data(self):
|
||||
"""Test printing a table with data."""
|
||||
data = [
|
||||
{"name": "test1", "status": "running"},
|
||||
{"name": "test2", "status": "stopped"},
|
||||
]
|
||||
columns = [("name", "NAME"), ("status", "STATUS")]
|
||||
|
||||
with patch("cua_cli.utils.output.console") as mock_console:
|
||||
print_table(data, columns)
|
||||
|
||||
mock_console.print.assert_called_once()
|
||||
|
||||
def test_handles_empty_data(self):
|
||||
"""Test handling empty data list."""
|
||||
data = []
|
||||
|
||||
with patch("cua_cli.utils.output.console") as mock_console:
|
||||
print_table(data)
|
||||
|
||||
mock_console.print.assert_called_once()
|
||||
# Should print "No data" message
|
||||
call_args = mock_console.print.call_args[0][0]
|
||||
assert "No data" in call_args
|
||||
|
||||
def test_auto_generates_columns(self):
|
||||
"""Test that columns are auto-generated from data keys."""
|
||||
data = [{"foo": "bar", "baz": "qux"}]
|
||||
|
||||
with patch("cua_cli.utils.output.console") as mock_console:
|
||||
print_table(data)
|
||||
|
||||
mock_console.print.assert_called_once()
|
||||
|
||||
def test_handles_missing_keys(self):
|
||||
"""Test handling data items with missing keys."""
|
||||
data = [
|
||||
{"name": "test1", "status": "running"},
|
||||
{"name": "test2"}, # Missing status
|
||||
]
|
||||
columns = [("name", "NAME"), ("status", "STATUS")]
|
||||
|
||||
with patch("cua_cli.utils.output.console") as mock_console:
|
||||
print_table(data, columns)
|
||||
|
||||
mock_console.print.assert_called_once()
|
||||
|
||||
def test_with_title(self):
|
||||
"""Test table with title."""
|
||||
data = [{"name": "test"}]
|
||||
|
||||
with patch("cua_cli.utils.output.console") as mock_console:
|
||||
print_table(data, title="Test Table")
|
||||
|
||||
mock_console.print.assert_called_once()
|
||||
|
||||
|
||||
class TestPrintJson:
|
||||
"""Tests for print_json function."""
|
||||
|
||||
def test_prints_dict(self):
|
||||
"""Test printing a dictionary as JSON."""
|
||||
data = {"key": "value", "number": 42}
|
||||
|
||||
with patch("cua_cli.utils.output.console") as mock_console:
|
||||
print_json(data)
|
||||
|
||||
mock_console.print_json.assert_called_once()
|
||||
|
||||
def test_prints_list(self):
|
||||
"""Test printing a list as JSON."""
|
||||
data = [{"name": "test1"}, {"name": "test2"}]
|
||||
|
||||
with patch("cua_cli.utils.output.console") as mock_console:
|
||||
print_json(data)
|
||||
|
||||
mock_console.print_json.assert_called_once()
|
||||
|
||||
def test_handles_nested_data(self):
|
||||
"""Test printing nested data structures."""
|
||||
data = {
|
||||
"level1": {
|
||||
"level2": {
|
||||
"value": 123,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with patch("cua_cli.utils.output.console") as mock_console:
|
||||
print_json(data)
|
||||
|
||||
mock_console.print_json.assert_called_once()
|
||||
|
||||
|
||||
class TestPrintError:
|
||||
"""Tests for print_error function."""
|
||||
|
||||
def test_prints_to_stderr(self):
|
||||
"""Test that error is printed to stderr."""
|
||||
with patch("cua_cli.utils.output.error_console") as mock_console:
|
||||
print_error("Test error message")
|
||||
|
||||
mock_console.print.assert_called_once()
|
||||
call_args = mock_console.print.call_args[0][0]
|
||||
assert "Error:" in call_args
|
||||
assert "Test error message" in call_args
|
||||
|
||||
|
||||
class TestPrintSuccess:
|
||||
"""Tests for print_success function."""
|
||||
|
||||
def test_prints_success_message(self):
|
||||
"""Test printing success message."""
|
||||
with patch("cua_cli.utils.output.console") as mock_console:
|
||||
print_success("Operation successful")
|
||||
|
||||
mock_console.print.assert_called_once()
|
||||
call_args = mock_console.print.call_args[0][0]
|
||||
assert "Operation successful" in call_args
|
||||
|
||||
|
||||
class TestPrintWarning:
|
||||
"""Tests for print_warning function."""
|
||||
|
||||
def test_prints_warning_message(self):
|
||||
"""Test printing warning message."""
|
||||
with patch("cua_cli.utils.output.console") as mock_console:
|
||||
print_warning("This is a warning")
|
||||
|
||||
mock_console.print.assert_called_once()
|
||||
call_args = mock_console.print.call_args[0][0]
|
||||
assert "Warning:" in call_args
|
||||
assert "This is a warning" in call_args
|
||||
|
||||
|
||||
class TestPrintInfo:
|
||||
"""Tests for print_info function."""
|
||||
|
||||
def test_prints_info_message(self):
|
||||
"""Test printing info message."""
|
||||
with patch("cua_cli.utils.output.console") as mock_console:
|
||||
print_info("Information message")
|
||||
|
||||
mock_console.print.assert_called_once()
|
||||
call_args = mock_console.print.call_args[0][0]
|
||||
assert "Information message" in call_args
|
||||
Reference in New Issue
Block a user