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