# Ensure the project root (webui.py) is importable before any test collection. import sys from pathlib import Path _ROOT = Path(__file__).parent.parent if str(_ROOT) not in sys.path: sys.path.insert(0, str(_ROOT)) """ Shared fixtures for the Outline Sync Web UI test suite. All tests work against the FastAPI app defined in webui.py (Phase B+). Phase A tests (WebDAV) are integration tests marked with @pytest.mark.integration and require a running WebDAV container — they are skipped in unit-test runs. Run unit tests only: pytest tests/ -m "not integration" Run integration tests (requires running WebDAV container): pytest tests/ -m integration Run everything: pytest tests/ """ import json import subprocess import textwrap from pathlib import Path from typing import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest import pytest_asyncio from httpx import ASGITransport, AsyncClient # --------------------------------------------------------------------------- # pytest configuration # --------------------------------------------------------------------------- pytest_plugins = ["pytest_asyncio"] def pytest_configure(config): config.addinivalue_line("markers", "integration: requires running infrastructure (WebDAV, Outline)") config.addinivalue_line("markers", "slow: longer-running tests") # --------------------------------------------------------------------------- # Git helpers # --------------------------------------------------------------------------- def git(vault: Path, *args) -> str: result = subprocess.run( ["git", "-C", str(vault), *args], check=True, capture_output=True, text=True, ) return result.stdout.strip() def git_config(vault: Path): git(vault, "config", "user.email", "test@sync.local") git(vault, "config", "user.name", "Test Runner") def commit_all(vault: Path, message: str): """Stage everything in vault and commit.""" git(vault, "add", "-A") try: git(vault, "commit", "-m", message) except subprocess.CalledProcessError: pass # nothing to commit — that's fine # --------------------------------------------------------------------------- # Auto-reset webui module state between tests # --------------------------------------------------------------------------- @pytest.fixture(autouse=True) def reset_webui_state(): """Clear _jobs and _active_job on webui module after every test.""" yield try: import webui as _webui _webui._jobs.clear() _webui._active_job = None except ImportError: pass # --------------------------------------------------------------------------- # Core fixtures # --------------------------------------------------------------------------- @pytest.fixture def vault_dir(tmp_path) -> Path: """ Temporary directory simulating /outline-vault/. Initialised as a git repo with 'outline' and 'main' branches. """ vault = tmp_path / "outline-vault" vault.mkdir() subprocess.run(["git", "init", str(vault)], check=True, capture_output=True) git_config(vault) git(vault, "checkout", "-b", "outline") # Initial commit so branches can diverge (vault / ".gitkeep").touch() commit_all(vault, "initial") git(vault, "checkout", "-b", "main") return vault @pytest.fixture def populated_vault(vault_dir: Path) -> Path: """ Vault pre-loaded with sample markdown files on both branches, simulating a state after a previous successful sync. """ # Create files on 'outline' branch — last known Outline state git(vault_dir, "checkout", "outline") (vault_dir / "Bewerbungen").mkdir() (vault_dir / "Bewerbungen" / "CV.md").write_text(textwrap.dedent("""\ --- outline_id: doc-cv-001 outline_collection_id: col-bew-001 outline_updated_at: 2026-01-10T12:00:00Z --- # CV Original content. """)) (vault_dir / "Infra").mkdir() (vault_dir / "Infra" / "HomeLab.md").write_text(textwrap.dedent("""\ --- outline_id: doc-hl-002 outline_collection_id: col-inf-001 outline_updated_at: 2026-01-10T12:00:00Z --- # HomeLab Server setup notes. """)) commit_all(vault_dir, "outline: sync 2026-01-10") # Merge into 'main' so both branches are identical at start git(vault_dir, "checkout", "main") git(vault_dir, "merge", "outline", "--no-ff", "-m", "merge outline into main") return vault_dir @pytest.fixture def settings_file(vault_dir: Path, tmp_path: Path) -> Path: """settings.json pointing at the temp vault and a test Outline URL.""" settings = { "source": { "url": "http://outline:3000", "token": "test-api-token-abc123", }, "sync": { "vault_dir": str(vault_dir), "allow_deletions": False, }, } path = tmp_path / "settings.json" path.write_text(json.dumps(settings, indent=2)) return path @pytest.fixture def sync_log(vault_dir: Path) -> Path: """Pre-populate _sync_log.md with a few history entries.""" log = vault_dir / "_sync_log.md" log.write_text(textwrap.dedent("""\ # Sync Log | Timestamp | Direction | Files | Status | |-----------|-----------|-------|--------| | 2026-03-04 08:00 | pull | 0 changes | ok | | 2026-03-05 09:10 | push | 2 updated, 1 created | ok | | 2026-03-06 14:32 | pull | 3 updated | ok | """)) return log # --------------------------------------------------------------------------- # App fixtures (Phases B–G) # --------------------------------------------------------------------------- @pytest.fixture def app(vault_dir: Path, settings_file: Path): """ FastAPI app instance with VAULT_DIR and SETTINGS_PATH overridden. Skips the fixture (and all tests using it) if webui.py is not yet written. """ webui = pytest.importorskip("webui", reason="webui.py not yet implemented") webui.VAULT_DIR = vault_dir webui.SETTINGS_PATH = settings_file return webui.app @pytest_asyncio.fixture async def client(app) -> Generator[AsyncClient, None, None]: """Async HTTP test client bound to the FastAPI app.""" async with AsyncClient( transport=ASGITransport(app=app), base_url="http://testserver" ) as c: yield c # --------------------------------------------------------------------------- # Vault state helpers # --------------------------------------------------------------------------- @pytest.fixture def vault_with_pending(populated_vault: Path) -> Path: """ Vault where main branch has local edits not yet pushed to Outline. Simulates Obsidian having written files via WebDAV. """ cv = populated_vault / "Bewerbungen" / "CV.md" cv.write_text(cv.read_text() + "\n## New Section\nAdded in Obsidian.\n") new_file = populated_vault / "Projekte" / "NewNote.md" new_file.parent.mkdir(exist_ok=True) new_file.write_text("# New Note\nWritten in Obsidian.\n") commit_all(populated_vault, "obsidian: local edits") return populated_vault @pytest.fixture def vault_with_conflict(populated_vault: Path) -> Path: """ Vault in a post-merge-conflict state: CV.md has conflict markers on main. """ cv = populated_vault / "Bewerbungen" / "CV.md" # Edit on outline branch git(populated_vault, "checkout", "outline") cv.write_text(textwrap.dedent("""\ --- outline_id: doc-cv-001 outline_collection_id: col-bew-001 outline_updated_at: 2026-03-06T11:03:00Z --- # CV Outline version with contact info updated. """)) commit_all(populated_vault, "outline: CV contact update") # Conflicting edit on main branch git(populated_vault, "checkout", "main") cv.write_text(textwrap.dedent("""\ --- outline_id: doc-cv-001 outline_collection_id: col-bew-001 outline_updated_at: 2026-01-10T12:00:00Z --- # CV Local version with new section added. """)) commit_all(populated_vault, "obsidian: CV new section") # Attempt merge — this will produce a conflict try: git(populated_vault, "merge", "outline") except subprocess.CalledProcessError: pass # expected: merge conflict return populated_vault