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

179 lines
6.4 KiB
Python

"""
Phase B — Read-Only Dashboard Tests
Tests for GET / (dashboard HTML) and GET /status (JSON API).
All tests use the FastAPI test client and mock git subprocess calls.
"""
import json
import subprocess
import textwrap
from unittest.mock import MagicMock, patch
import pytest
import pytest_asyncio
pytestmark = pytest.mark.asyncio
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def make_git_output(stdout: str = "", returncode: int = 0) -> MagicMock:
m = MagicMock()
m.stdout = stdout
m.returncode = returncode
return m
# ---------------------------------------------------------------------------
# US-B1 — Dashboard page renders
# ---------------------------------------------------------------------------
class TestDashboardPage:
async def test_get_root_returns_200(self, client):
r = await client.get("/")
assert r.status_code == 200
async def test_dashboard_returns_html(self, client):
r = await client.get("/")
assert "text/html" in r.headers.get("content-type", "")
async def test_dashboard_contains_status_badge(self, client):
r = await client.get("/")
body = r.text.lower()
# At minimum one of these status words must appear
assert any(word in body for word in ("clean", "dirty", "conflict", "pending")), (
"Dashboard must show a vault status badge"
)
async def test_dashboard_contains_pull_button(self, client):
r = await client.get("/")
body = r.text.lower()
assert "pull" in body or "get from outline" in body
async def test_dashboard_contains_push_button(self, client):
r = await client.get("/")
body = r.text.lower()
assert "push" in body or "send to outline" in body
# ---------------------------------------------------------------------------
# US-B2 — Status endpoint returns structured JSON
# ---------------------------------------------------------------------------
class TestStatusEndpoint:
async def test_status_returns_200(self, client):
r = await client.get("/status")
assert r.status_code == 200
async def test_status_returns_json(self, client):
r = await client.get("/status")
assert "application/json" in r.headers.get("content-type", "")
data = r.json()
assert isinstance(data, dict)
async def test_status_has_required_fields(self, client):
r = await client.get("/status")
data = r.json()
assert "vault_status" in data, "Missing 'vault_status' field"
assert "pending_count" in data, "Missing 'pending_count' field"
assert "conflicts" in data, "Missing 'conflicts' field"
assert "last_pull" in data, "Missing 'last_pull' field"
assert "last_push" in data, "Missing 'last_push' field"
async def test_status_pending_count_is_integer(self, client):
r = await client.get("/status")
data = r.json()
assert isinstance(data["pending_count"], int)
assert data["pending_count"] >= 0
async def test_status_conflicts_is_integer(self, client):
r = await client.get("/status")
data = r.json()
assert isinstance(data["conflicts"], int)
assert data["conflicts"] >= 0
async def test_status_clean_vault(self, client, populated_vault):
"""
After a clean merge, pending_count should be 0 and conflicts 0.
"""
r = await client.get("/status")
data = r.json()
assert data["pending_count"] == 0
assert data["conflicts"] == 0
assert data["vault_status"] in ("clean", "ok", "synced")
async def test_status_with_pending_changes(self, client, vault_with_pending):
"""
vault_with_pending has local edits on main — pending_count must be > 0.
"""
r = await client.get("/status")
data = r.json()
assert data["pending_count"] > 0
async def test_status_vault_status_dirty_when_pending(self, client, vault_with_pending):
r = await client.get("/status")
data = r.json()
assert data["vault_status"] != "clean"
# ---------------------------------------------------------------------------
# US-B3 — Conflict warning badge
# ---------------------------------------------------------------------------
class TestConflictBadge:
async def test_no_conflict_badge_when_clean(self, client, populated_vault):
r = await client.get("/")
# Should NOT contain a prominent conflict warning
# (checking for the absence of conflict count > 0 in dashboard)
data = (await client.get("/status")).json()
assert data["conflicts"] == 0
async def test_conflict_badge_visible_when_conflicts_exist(
self, client, vault_with_conflict
):
status = (await client.get("/status")).json()
assert status["conflicts"] > 0, "Expected conflict count > 0"
r = await client.get("/")
body = r.text.lower()
assert "conflict" in body, "Dashboard must show conflict warning when conflicts exist"
async def test_conflict_badge_links_to_conflicts_page(self, client, vault_with_conflict):
r = await client.get("/")
assert "/conflicts" in r.text, "Conflict badge must link to /conflicts"
# ---------------------------------------------------------------------------
# US-B4 — Pending count reflects git diff
# ---------------------------------------------------------------------------
class TestPendingCount:
async def test_pending_count_zero_on_clean_vault(self, client, populated_vault):
r = await client.get("/status")
assert r.json()["pending_count"] == 0
async def test_pending_count_increases_with_local_edits(
self, client, vault_with_pending
):
r = await client.get("/status")
# We added 1 modified file + 1 new file in vault_with_pending fixture
assert r.json()["pending_count"] >= 2
async def test_pending_count_shown_in_push_button(self, client, vault_with_pending):
"""Push button label should reflect pending count."""
r = await client.get("/")
body = r.text
status = (await client.get("/status")).json()
pending = status["pending_count"]
# The pending count must appear somewhere near the push button
assert str(pending) in body, (
f"Push button should show pending count ({pending}) in label"
)