Add CLI source code and update .gitignore
This commit is contained in:
232
cli_anything/codimd/tests/TEST.md
Normal file
232
cli_anything/codimd/tests/TEST.md
Normal 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.
|
||||
```
|
||||
1
cli_anything/codimd/tests/__init__.py
Normal file
1
cli_anything/codimd/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Tests package
|
||||
366
cli_anything/codimd/tests/test_core.py
Normal file
366
cli_anything/codimd/tests/test_core.py
Normal 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"])
|
||||
307
cli_anything/codimd/tests/test_full_e2e.py
Normal file
307
cli_anything/codimd/tests/test_full_e2e.py
Normal 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"])
|
||||
Reference in New Issue
Block a user