367 lines
13 KiB
Python
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"])
|