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,232 @@
# CodiMD CLI - Test Plan and Results
## Test Plan
### Unit Tests (`test_core.py`)
#### Config Tests
- Test default configuration values
- Test configuration persistence
- Test configuration updates (server, timeout, verify_ssl)
#### Session Tests
- Test session creation
- Test cookie storage and retrieval
- Test user data storage
- Test session expiration
- Test session clearing
#### Client Tests
- Test URL building
- Test HTTP methods (GET, POST, PUT, DELETE)
- Test cookie management
- Test SSL verification setting
#### Note Manager Tests
- Test note ID encoding/decoding (UUID to base64url)
- Test note list parsing
- Test note creation request formatting
- Test note update request formatting
#### Export Manager Tests
- Test markdown export
- Test export with file output
- Test error handling for unauthorized exports
### E2E Tests (`test_full_e2e.py`)
#### Configuration Tests
- Test CLI configuration with `--server` flag
- Test JSON output mode with `--json` flag
#### Note Command Tests
- Test `note list` command
- Test `note create` command
- Test `note get` command
- Test `note info` command
#### Export Command Tests
- Test `export markdown` command
- Test `export html` command
#### User Command Tests
- Test `user me` command
- Test `user logout` command
### Subprocess Tests
- Test installed CLI via `_resolve_cli()` method
- Test with `CLI_ANYTHING_FORCE_INSTALLED=1` environment variable
- Verify CLI is available in PATH
## Test Requirements
- Python 3.7+
- pytest for test execution
- requests-mock for HTTP mocking
- pytest-subprocess for subprocess testing
## Mock Data
### Mock User
```json
{
"id": "12345678-1234-1234-1234-123456789012",
"profile": {
"name": "Test User",
"email": "test@example.com"
},
"email": "test@example.com"
}
```
### Mock Note List
```json
{
"myNotes": [
{
"id": "VXDECB3HEXDPHWHGHFDGWEDFBM",
"text": "Test Note 1",
"shortId": "abc123",
"createdAt": "2024-01-01T00:00:00.000Z",
"lastchangeAt": "2024-01-01T01:00:00.000Z",
"tags": ["test"]
},
{
"id": "YTECB3HEXDPHWHGHFDGWEDFBZ",
"text": "Test Note 2",
"shortId": "def456",
"createdAt": "2024-01-02T00:00:00.000Z",
"lastchangeAt": "2024-01-02T01:00:00.000Z",
"tags": []
}
]
}
```
## Test Execution
Run all tests:
```bash
pytest -v --tb=no
```
Run only unit tests:
```bash
pytest test_core.py -v
```
Run only E2E tests:
```bash
pytest test_full_e2e.py -v
```
## Test Results
### Test Execution Summary
```
============================= test session starts ==============================
platform darwin -- Python 3.12.2, pytest-9.0.2, pluggy-1.6.0
collected 46 items
cli_anything/codimd/tests/test_core.py::TestConfig::test_default_config PASSED
cli_anything/codimd/tests/test_core.py::TestConfig::test_config_persistence PASSED
cli_anything/codimd/tests/test_core.py::TestConfig::test_set_server PASSED
cli_anything/codimd/tests/test_core.py::TestConfig::test_set_timeout PASSED
cli_anything/codimd/tests/test_core.py::TestConfig::test_set_verify_ssl PASSED
cli_anything/codimd/tests/test_core.py::TestSession::test_session_creation PASSED
cli_anything/codimd/tests/test_core.py::TestSession::test_set_cookies PASSED
cli_anything/codimd/tests/test_core.py::TestSession::test_set_user PASSED
cli_anything/codimd/tests/test_core.py::TestSession::test_session_clear PASSED
cli_anything/codimd/tests/test_core.py::TestCodiMDClient::test_url_building PASSED
cli_anything/codimd/tests/test_core.py::TestCodiMDClient::test_url_trailing_slash PASSED
cli_anything/codimd/tests/test_core.py::TestCodiMDClient::test_session_cookies PASSED
cli_anything/codimd/tests/test_core.py::TestCodiMDClient::test_update_cookies PASSED
cli_anything/codimd/tests/test_core.py::TestCodiMDClient::test_ssl_setting PASSED
cli_anything/codimd/tests/test_core.py::TestNoteManager::test_encode_note_id PASSED
cli_anything/codimd/tests/test_core.py::TestNoteManager::test_decode_note_id PASSED
cli_anything/codimd/tests/test_core.py::TestNoteManager::test_encode_decode_roundtrip PASSED
cli_anything/codimd/tests/test_core.py::TestNoteManager::test_list_notes_success PASSED
cli_anything/codimd/tests/test_core.py::TestNoteManager::test_list_notes_unauthorized PASSED
cli_anything/codimd/tests/test_core.py::TestNoteManager::test_create_note_with_content PASSED
cli_anything/codimd/tests/test_core.py::TestNoteManager::test_delete_note_success PASSED
cli_anything/codimd/tests/test_core.py::TestUserManager::test_get_me_success PASSED
cli_anything/codimd/tests/test_core.py::TestUserManager::test_get_me_unauthorized PASSED
cli_anything/codimd/tests/test_core.py::TestUserManager::test_export_data_success PASSED
cli_anything/codimd/tests/test_core.py::TestExportManager::test_export_markdown_success PASSED
cli_anything/codimd/tests/test_core.py::TestExportManager::test_export_markdown_to_file PASSED
cli_anything/codimd/tests/test_core.py::TestExportManager::test_export_markdown_not_found PASSED
cli_anything/codimd/tests/test_core.py::TestExportManager::test_export_pdf_disabled PASSED
cli_anything/codimd/tests/test_core.py::TestRevisionManager::test_list_revisions_success PASSED
cli_anything/codimd/tests/test_core.py::TestRevisionManager::test_list_revisions_empty PASSED
cli_anything/codimd/tests/test_core.py::TestRevisionManager::test_get_revision_at_time PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestCLISubprocess::test_cli_help PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestCLISubprocess::test_cli_version PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2EClickRunner::test_config_get_command PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2EClickRunner::test_config_set_command PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2EClickRunner::test_help_command PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2EClickRunner::test_version_command PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2EClickRunner::test_note_help PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2EClickRunner::test_user_help PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2EClickRunner::test_export_help PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2EClickRunner::test_json_output_mode PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2ENoteCommands::test_note_list_authenticated PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2ENoteCommands::test_note_create PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2EExportCommands::test_export_markdown_to_file PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2EUserCommands::test_user_logout PASSED
cli_anything/codimd/tests/test_full_e2e.py::TestE2EUserCommands::test_user_me_unauthenticated PASSED
============================== 46 passed in 0.66s ==============================
```
### Test Coverage Summary
- **Total Tests**: 46
- **Passed**: 46 (100%)
- **Failed**: 0
#### Unit Tests (test_core.py)
- Config: 5/5 passed
- Session: 4/4 passed
- Client: 5/5 passed
- Note Manager: 6/6 passed
- User Manager: 3/3 passed
- Export Manager: 4/4 passed
- Revision Manager: 3/3 passed
#### E2E Tests (test_full_e2e.py)
- Subprocess tests: 2/2 passed
- Click Runner tests: 8/8 passed
- Note Commands: 2/2 passed
- Export Commands: 1/1 passed
- User Commands: 2/2 passed
### CLI Verification
```bash
$ which cli-anything-codimd
/Users/timmy/.pyenv/versions/3.12.2/bin/cli-anything-codimd
$ cli-anything-codimd --version
CodiMD CLI v0.1.0
$ cli-anything-codimd --help
Usage: cli-anything-codimd [OPTIONS] COMMAND [ARGS]...
CodiMD CLI - Collaborative markdown notes.
Options:
--version Show version and exit.
--json Output in JSON format
--server TEXT CodiMD server URL
--help Show this message and exit.
Commands:
config Configuration commands.
export Export operations.
note Note operations.
registry Registry operations.
repl Start interactive REPL mode.
revision Revision operations.
user User operations.
```

View File

@@ -0,0 +1 @@
# Tests package

View File

@@ -0,0 +1,366 @@
"""
Unit tests for CodiMD CLI core modules.
Tests use synthetic data and mock HTTP responses.
No external dependencies required.
"""
import pytest
import json
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
from cli_anything.codimd.core.config import Config
from cli_anything.codimd.core.session import Session
from cli_anything.codimd.core.client import CodiMDClient
from cli_anything.codimd.core.note import NoteManager
from cli_anything.codimd.core.user import UserManager
from cli_anything.codimd.core.export import ExportManager
from cli_anything.codimd.core.revision import RevisionManager
class TestConfig:
"""Tests for Config class."""
def test_default_config(self, tmp_path):
"""Test default configuration values."""
with patch.object(Config, "CONFIG_DIR", tmp_path):
config = Config()
assert config.server == "http://localhost:3000"
assert config.timeout == 30
assert config.verify_ssl is True
def test_config_persistence(self, tmp_path):
"""Test configuration is persisted to file."""
with patch.object(Config, "CONFIG_DIR", tmp_path):
config1 = Config()
config1.server = "https://example.com"
config1.timeout = 60
config2 = Config()
assert config2.server == "https://example.com"
assert config2.timeout == 60
def test_set_server(self, tmp_path):
"""Test setting server URL."""
with patch.object(Config, "CONFIG_DIR", tmp_path):
config = Config()
config.server = "https://codimd.example.com"
assert config.server == "https://codimd.example.com"
def test_set_timeout(self, tmp_path):
"""Test setting timeout."""
with patch.object(Config, "CONFIG_DIR", tmp_path):
config = Config()
config.timeout = 120
assert config.timeout == 120
def test_set_verify_ssl(self, tmp_path):
"""Test setting SSL verification."""
with patch.object(Config, "CONFIG_DIR", tmp_path):
config = Config()
config.verify_ssl = False
assert config.verify_ssl is False
class TestSession:
"""Tests for Session class."""
def test_session_creation(self, tmp_path):
"""Test session object creation."""
session = Session(tmp_path)
# Session file is created only when data is saved
assert session.is_authenticated() is False
def test_set_cookies(self, tmp_path):
"""Test setting session cookies."""
session = Session(tmp_path)
cookies = {"connect.sid": "test_session_value"}
session.set_cookies(cookies)
assert session.is_authenticated() is True
assert session.get_cookies() == cookies
def test_set_user(self, tmp_path):
"""Test setting user data."""
session = Session(tmp_path)
user_data = {
"id": "12345678-1234-1234-1234-123456789012",
"profile": {"name": "Test User"}
}
session.set_user(user_data)
assert session.get_user() == user_data
assert session.user_id == "12345678-1234-1234-1234-123456789012"
assert session.username == "Test User"
def test_session_clear(self, tmp_path):
"""Test clearing session."""
session = Session(tmp_path)
session.set_cookies({"connect.sid": "test"})
session.set_user({"id": "123"})
session.clear()
assert session.is_authenticated() is False
assert session.get_user() is None
class TestCodiMDClient:
"""Tests for CodiMDClient class."""
def test_url_building(self):
"""Test URL building for requests."""
client = CodiMDClient("https://example.com/codimd")
assert client._url("test") == "https://example.com/codimd/test"
assert client._url("/api/notes") == "https://example.com/codimd/api/notes"
def test_url_trailing_slash(self):
"""Test URL building with server having trailing slash."""
client = CodiMDClient("https://example.com/")
assert client._url("test") == "https://example.com/test"
def test_session_cookies(self):
"""Test client uses session cookies."""
cookies = {"connect.sid": "test123"}
client = CodiMDClient("https://example.com", session_cookies=cookies)
assert client.get_cookies() == cookies
def test_update_cookies(self):
"""Test updating session cookies."""
client = CodiMDClient("https://example.com")
client.update_cookies({"new": "cookie"})
assert "new" in client.get_cookies()
def test_ssl_setting(self):
"""Test SSL verification setting."""
client = CodiMDClient("https://example.com", verify_ssl=False)
assert client.verify_ssl is False
class TestNoteManager:
"""Tests for NoteManager class."""
def test_encode_note_id(self):
"""Test encoding UUID note ID."""
uuid = "12345678-1234-1234-1234-123456789012"
encoded = NoteManager.encode_note_id(uuid)
assert isinstance(encoded, str)
assert "-" not in encoded
# Length should be appropriate for base64url
assert 20 <= len(encoded) <= 30
def test_decode_note_id(self):
"""Test decoding base64url note ID."""
# First encode a known UUID, then decode it
original = "12345678-1234-1234-1234-123456789012"
encoded = NoteManager.encode_note_id(original)
decoded = NoteManager.decode_note_id(encoded)
assert decoded == original
def test_encode_decode_roundtrip(self):
"""Test roundtrip encoding/decoding."""
test_uuids = [
"12345678-1234-1234-1234-123456789012",
"abcdef12-abcd-abcd-abcd-abcdef123456",
"00000000-0000-0000-0000-000000000000",
]
for uuid in test_uuids:
encoded = NoteManager.encode_note_id(uuid)
decoded = NoteManager.decode_note_id(encoded)
assert decoded == uuid
def test_list_notes_success(self):
"""Test listing notes on success."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"myNotes": [
{"id": "abc123", "text": "Test Note", "shortId": "xyz"}
]
}
mock_client.get.return_value = mock_response
manager = NoteManager(mock_client)
notes = manager.list_my_notes()
assert len(notes) == 1
assert notes[0]["id"] == "abc123"
def test_list_notes_unauthorized(self):
"""Test listing notes when unauthorized."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 403
mock_client.get.return_value = mock_response
manager = NoteManager(mock_client)
with pytest.raises(PermissionError):
manager.list_my_notes()
def test_create_note_with_content(self):
"""Test creating note with initial content."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 200
mock_response.history = [Mock(url="https://example.com/new-note-id")]
mock_response.url = "https://example.com/new-note-id"
mock_client.post.return_value = mock_response
manager = NoteManager(mock_client)
result = manager.create_note("Hello, world!")
assert result["status"] == "created"
assert "id" in result
def test_delete_note_success(self):
"""Test deleting note on success."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 200
mock_client.delete.return_value = mock_response
manager = NoteManager(mock_client)
assert manager.delete_note("test-id") is True
class TestUserManager:
"""Tests for UserManager class."""
def test_get_me_success(self):
"""Test getting current user info on success."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"id": "123",
"profile": {"name": "Test User"}
}
mock_client.get.return_value = mock_response
manager = UserManager(mock_client)
user = manager.get_me()
assert user["id"] == "123"
def test_get_me_unauthorized(self):
"""Test getting user info when unauthorized."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 403
mock_client.get.return_value = mock_response
manager = UserManager(mock_client)
with pytest.raises(PermissionError):
manager.get_me()
def test_export_data_success(self):
"""Test exporting user data on success."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": "exported"}
mock_client.post.return_value = mock_response
manager = UserManager(mock_client)
data = manager.export_my_data()
assert data["data"] == "exported"
class TestExportManager:
"""Tests for ExportManager class."""
def test_export_markdown_success(self):
"""Test exporting note as markdown."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 200
mock_response.text = "# Test Note\n\nContent here."
mock_client.get.return_value = mock_response
manager = ExportManager(mock_client)
content = manager.export_markdown("test-id")
assert content == "# Test Note\n\nContent here."
def test_export_markdown_to_file(self, tmp_path):
"""Test exporting markdown to file."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 200
mock_response.text = "# Test Note"
mock_client.get.return_value = mock_response
output_file = tmp_path / "output.md"
manager = ExportManager(mock_client)
result = manager.export_markdown("test-id", output_file)
assert result == str(output_file)
assert output_file.read_text() == "# Test Note"
def test_export_markdown_not_found(self):
"""Test exporting non-existent note."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 404
mock_client.get.return_value = mock_response
manager = ExportManager(mock_client)
with pytest.raises(FileNotFoundError):
manager.export_markdown("non-existent")
def test_export_pdf_disabled(self):
"""Test PDF export when disabled."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 403
mock_client.get.return_value = mock_response
manager = ExportManager(mock_client)
with pytest.raises(PermissionError, match="PDF export disabled"):
manager.export_pdf("test-id")
class TestRevisionManager:
"""Tests for RevisionManager class."""
def test_list_revisions_success(self):
"""Test listing revisions on success."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"revisions": [
{"time": 1609459200000, "length": 100},
{"time": 1609365600000, "length": 50}
]
}
mock_client.get.return_value = mock_response
manager = RevisionManager(mock_client)
revisions = manager.list_revisions("test-id")
assert len(revisions) == 2
def test_list_revisions_empty(self):
"""Test listing revisions when none exist."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {}
mock_client.get.return_value = mock_response
manager = RevisionManager(mock_client)
revisions = manager.list_revisions("test-id")
assert revisions == []
def test_get_revision_at_time(self):
"""Test getting revision at specific time."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"content": "Old content",
"timestamp": 1609459200000
}
mock_client.get.return_value = mock_response
manager = RevisionManager(mock_client)
revision = manager.get_revision_at_time("test-id", 1609459200000)
assert revision["content"] == "Old content"
if __name__ == "__main__":
pytest.main([__file__, "-v"])

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"])