Files

367 lines
13 KiB
Python

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