179 lines
6.4 KiB
Python
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"
|
|
)
|