Add CLI source code and update .gitignore

This commit is contained in:
2026-04-07 09:38:12 +08:00
parent 6ef2dddf3f
commit 9e17f1bf29
18 changed files with 2663 additions and 1 deletions

View File

@@ -0,0 +1,307 @@
"""
End-to-end tests for CodiMD CLI.
Tests use real files and simulate full CLI workflows.
"""
import pytest
import json
import subprocess
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch
import sys
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from cli_anything.codimd.core import Config, Session, CodiMDClient
from cli_anything.codimd.codimd_cli import cli
from click.testing import CliRunner
class TestCLISubprocess:
"""Tests for installed CLI via subprocess."""
@staticmethod
def _resolve_cli(cli_name: str) -> str:
"""Resolve CLI command path.
If CLI_ANYTHING_FORCE_INSTALLED is set, use the command name directly.
Otherwise, use the Python module path.
"""
if "CLI_ANYTHING_FORCE_INSTALLED" in str(__import__("os").environ):
return cli_name
# For testing, use python -m to run the module
return f"python -m cli_anything.codimd.codimd_cli"
def test_cli_help(self):
"""Test CLI help command."""
result = subprocess.run(
["python", "-m", "cli_anything.codimd.codimd_cli", "--help"],
capture_output=True,
text=True
)
assert result.returncode == 0
assert "CodiMD CLI" in result.stdout
def test_cli_version(self):
"""Test CLI version command."""
result = subprocess.run(
["python", "-m", "cli_anything.codimd.codimd_cli", "--version"],
capture_output=True,
text=True
)
assert result.returncode == 0
assert "0.1.0" in result.stdout
class TestE2EClickRunner:
"""End-to-end tests using Click CliRunner."""
def setup_method(self):
"""Set up test environment."""
self.runner = CliRunner()
self.temp_dir = tempfile.mkdtemp()
def teardown_method(self):
"""Clean up test environment."""
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
@patch("cli_anything.codimd.codimd_cli.Config")
def test_config_get_command(self, mock_config_class):
"""Test config get command."""
mock_config = Mock()
mock_config.server = "http://localhost:3000"
mock_config.timeout = 30
mock_config.verify_ssl = True
mock_config_class.return_value = mock_config
result = self.runner.invoke(cli, ["config", "get"])
assert result.exit_code == 0
assert "Server:" in result.output or "localhost:3000" in result.output
@patch("cli_anything.codimd.codimd_cli.Config")
def test_config_set_command(self, mock_config_class):
"""Test config set command."""
mock_config = Mock()
mock_config.server = "http://localhost:3000"
mock_config_class.return_value = mock_config
result = self.runner.invoke(cli, ["config", "set", "timeout", "60"])
assert result.exit_code == 0
def test_help_command(self):
"""Test help command."""
result = self.runner.invoke(cli, ["--help"])
assert result.exit_code == 0
assert "CodiMD CLI" in result.output
assert "note" in result.output
assert "user" in result.output
assert "export" in result.output
def test_version_command(self):
"""Test version command."""
result = self.runner.invoke(cli, ["--version"])
assert result.exit_code == 0
assert "0.1.0" in result.output
def test_note_help(self):
"""Test note group help."""
result = self.runner.invoke(cli, ["note", "--help"])
assert result.exit_code == 0
assert "list" in result.output
assert "get" in result.output
assert "create" in result.output
def test_user_help(self):
"""Test user group help."""
result = self.runner.invoke(cli, ["user", "--help"])
assert result.exit_code == 0
assert "me" in result.output
assert "login" in result.output
assert "logout" in result.output
def test_export_help(self):
"""Test export group help."""
result = self.runner.invoke(cli, ["export", "--help"])
assert result.exit_code == 0
assert "markdown" in result.output
assert "pdf" in result.output
assert "html" in result.output
@patch("cli_anything.codimd.codimd_cli.Config")
@patch("cli_anything.codimd.codimd_cli.Session")
def test_json_output_mode(self, mock_session_class, mock_config_class):
"""Test JSON output mode."""
mock_config = Mock()
mock_config.config_dir = Path(self.temp_dir)
mock_config_class.return_value = mock_config
mock_session = Mock()
mock_session.is_authenticated.return_value = False
mock_session_class.return_value = mock_session
result = self.runner.invoke(cli, ["--json", "config", "get"])
# Should output JSON
assert '"status"' in result.output or '{"server"' in result.output or '"timeout"' in result.output
class TestE2ENoteCommands:
"""End-to-end tests for note commands."""
def setup_method(self):
"""Set up test environment."""
self.runner = CliRunner()
self.temp_dir = tempfile.mkdtemp()
def teardown_method(self):
"""Clean up test environment."""
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
@patch("cli_anything.codimd.codimd_cli.get_client")
@patch("cli_anything.codimd.codimd_cli.Config")
@patch("cli_anything.codimd.codimd_cli.Session")
def test_note_list_authenticated(self, mock_session_class, mock_config_class, mock_get_client):
"""Test listing notes when authenticated."""
mock_config = Mock()
mock_config.config_dir = Path(self.temp_dir)
mock_config_class.return_value = mock_config
mock_session = Mock()
mock_session.is_authenticated.return_value = True
mock_session_class.return_value = mock_session
mock_client = Mock()
mock_note_manager = Mock()
mock_note_manager.list_my_notes.return_value = [
{"id": "abc123", "text": "Test Note"}
]
mock_get_client.return_value = (mock_client, mock_session)
with patch("cli_anything.codimd.codimd_cli.NoteManager", return_value=mock_note_manager):
result = self.runner.invoke(cli, ["note", "list"])
assert result.exit_code == 0
@patch("cli_anything.codimd.codimd_cli.get_client")
@patch("cli_anything.codimd.codimd_cli.Config")
@patch("cli_anything.codimd.codimd_cli.Session")
def test_note_create(self, mock_session_class, mock_config_class, mock_get_client):
"""Test creating a note."""
mock_config = Mock()
mock_config.config_dir = Path(self.temp_dir)
mock_config_class.return_value = mock_config
mock_session = Mock()
mock_session_class.return_value = mock_session
mock_client = Mock()
mock_note_manager = Mock()
mock_note_manager.create_note.return_value = {
"id": "new-id",
"url": "http://example.com/new-id",
"status": "created"
}
mock_get_client.return_value = (mock_client, mock_session)
with patch("cli_anything.codimd.codimd_cli.NoteManager", return_value=mock_note_manager):
result = self.runner.invoke(cli, ["note", "create", "--content", "Test content"])
assert result.exit_code == 0
assert "created" in result.output
class TestE2EExportCommands:
"""End-to-end tests for export commands."""
def setup_method(self):
"""Set up test environment."""
self.runner = CliRunner()
self.temp_dir = tempfile.mkdtemp()
def teardown_method(self):
"""Clean up test environment."""
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
@patch("cli_anything.codimd.codimd_cli.get_client")
@patch("cli_anything.codimd.codimd_cli.Config")
@patch("cli_anything.codimd.codimd_cli.Session")
def test_export_markdown_to_file(self, mock_session_class, mock_config_class, mock_get_client):
"""Test exporting markdown to file."""
mock_config = Mock()
mock_config.config_dir = Path(self.temp_dir)
mock_config_class.return_value = mock_config
mock_session = Mock()
mock_session_class.return_value = mock_session
mock_client = Mock()
mock_export_manager = Mock()
mock_export_manager.export_markdown.return_value = "# Test Note"
mock_get_client.return_value = (mock_client, mock_session)
output_file = Path(self.temp_dir) / "output.md"
with patch("cli_anything.codimd.codimd_cli.ExportManager", return_value=mock_export_manager):
result = self.runner.invoke(
cli,
["export", "markdown", "test-id", "--output", str(output_file)]
)
assert result.exit_code == 0
class TestE2EUserCommands:
"""End-to-end tests for user commands."""
def setup_method(self):
"""Set up test environment."""
self.runner = CliRunner()
self.temp_dir = tempfile.mkdtemp()
def teardown_method(self):
"""Clean up test environment."""
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
@patch("cli_anything.codimd.codimd_cli.get_client")
@patch("cli_anything.codimd.codimd_cli.Config")
@patch("cli_anything.codimd.codimd_cli.Session")
def test_user_logout(self, mock_session_class, mock_config_class, mock_get_client):
"""Test user logout."""
mock_config = Mock()
mock_config.config_dir = Path(self.temp_dir)
mock_config_class.return_value = mock_config
mock_session = Mock()
mock_session_class.return_value = mock_session
mock_get_client.return_value = (Mock(), mock_session)
result = self.runner.invoke(cli, ["user", "logout"])
assert result.exit_code == 0
assert "logged out" in result.output.lower()
@patch("cli_anything.codimd.codimd_cli.get_client")
@patch("cli_anything.codimd.codimd_cli.Config")
@patch("cli_anything.codimd.codimd_cli.Session")
def test_user_me_unauthenticated(self, mock_session_class, mock_config_class, mock_get_client):
"""Test getting user info when not authenticated."""
mock_config = Mock()
mock_config.config_dir = Path(self.temp_dir)
mock_config_class.return_value = mock_config
mock_session = Mock()
mock_session.is_authenticated.return_value = False
mock_session_class.return_value = mock_session
mock_get_client.return_value = (Mock(), mock_session)
result = self.runner.invoke(cli, ["user", "me"])
assert result.exit_code == 1
assert "authenticated" in result.output.lower()
if __name__ == "__main__":
pytest.main([__file__, "-v"])