Files
outline-sync/tests/conftest.py
2026-03-07 20:54:59 +01:00

271 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 BG)
# ---------------------------------------------------------------------------
@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