eduard/cobra-boards

Cobra Boards, team task boards you can run from Claude or ChatGPT. FastAPI + DataK3 + appid + Dodil Models + MCP.

A remix template · 1 remixes · Remix eduard/cobra-boards

tests/test_doors.py

"""The two doors share one set of rules. These tests go through HTTP and through MCP exactly as
the browser and Claude do, with the gateway's identity header, and check that a viewer is
refused and a stranger sees nothing through either door."""
import pytest
from conftest import MEMBER, OWNER, STRANGER, VIEWER, headers
from fastapi.testclient import TestClient

from boards.main import create_app

MCP_ACCEPT = {"accept": "application/json, text/event-stream"}


@pytest.fixture
def client(svc):
    s, bid = svc
    with TestClient(create_app(s)) as c:
        yield c, s, bid


def mcp(c, person, tool, **args):
    h = {**headers(person), **MCP_ACCEPT}
    init = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
        "protocolVersion": "2025-06-18", "capabilities": {},
        "clientInfo": {"name": "test", "version": "0"}}}
    assert c.post("/mcp", json=init, headers=h).status_code == 200
    out = c.post("/mcp", headers=h, json={"jsonrpc": "2.0", "id": 2, "method": "tools/call",
                                          "params": {"name": tool, "arguments": args}}).json()
    res = out["result"]
    return res.get("isError", False), "".join(x.get("text", "") for x in res["content"])


# ---------------------------------------------------------------- sign-in
def test_nothing_without_a_signed_in_person(client):
    c, _, bid = client
    assert c.get("/api/boards").status_code == 401
    assert c.get(f"/api/boards/{bid}").status_code == 401
    r = c.get("/", follow_redirects=False)
    assert r.status_code == 303 and "/.dodil/auth/login" in r.headers["location"]
    r = c.post("/mcp", json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
    assert r.status_code == 401 and "resource_metadata" in r.headers["www-authenticate"]
    meta = c.get("/.well-known/oauth-protected-resource/mcp").json()
    assert meta["resource"].endswith("/mcp") and meta["authorization_servers"]


def test_health_and_self_check_need_no_person(client):
    c, _, _ = client
    assert c.get("/healthz").json() == {"ok": True}
    assert c.post("/").json()["ok"] is True


# ---------------------------------------------------------------- web
def test_web_roles_map_to_status_codes(client):
    c, s, bid = client
    card = c.post(f"/api/boards/{bid}/cards", json={"title": "Write copy"},
                  headers=headers(MEMBER)).json()
    assert card["number"] == 1 and card["list"] == "To do"
    assert c.post(f"/api/boards/{bid}/cards", json={"title": "x"},
                  headers=headers(VIEWER)).status_code == 403
    assert c.get(f"/api/boards/{bid}", headers=headers(STRANGER)).status_code == 404
    assert c.get(f"/api/cards/{card['id']}", headers=headers(STRANGER)).status_code == 404
    assert c.put(f"/api/boards/{bid}/members", json={"email": "x@cobra.test"},
                 headers=headers(MEMBER)).status_code == 403
    assert c.post(f"/api/cards/{card['id']}/move", json={"list": "Nowhere"},
                  headers=headers(MEMBER)).status_code == 422


def test_web_stale_edit_is_409(client):
    c, _, bid = client
    card = c.post(f"/api/boards/{bid}/cards", json={"title": "Budget"},
                  headers=headers(OWNER)).json()
    v = card["version"]
    ok = c.patch(f"/api/cards/{card['id']}", json={"expected_version": v, "title": "Budget v2"},
                 headers=headers(MEMBER))
    assert ok.status_code == 200 and ok.json()["version"] == v + 1
    stale = c.patch(f"/api/cards/{card['id']}", json={"expected_version": v, "title": "mine"},
                    headers=headers(OWNER))
    assert stale.status_code == 409


def test_web_partial_edit_keeps_other_fields(client):
    c, _, bid = client
    card = c.post(f"/api/boards/{bid}/cards", headers=headers(OWNER),
                  json={"title": "Plan", "assignee": MEMBER.email, "due_date": "2026-11-20"}).json()
    out = c.patch(f"/api/cards/{card['id']}", json={"title": "Plan B"},
                  headers=headers(OWNER)).json()
    assert out["assignee"] == MEMBER.email and out["due_date"] == "2026-11-20"
    out = c.patch(f"/api/cards/{card['id']}", json={"assignee": None},
                  headers=headers(OWNER)).json()
    assert out["assignee"] is None and out["due_date"] == "2026-11-20"


def test_web_drag_and_drop_and_history(client):
    c, _, bid = client
    a = c.post(f"/api/boards/{bid}/cards", json={"title": "a", "list": "Doing"},
               headers=headers(OWNER)).json()
    b = c.post(f"/api/boards/{bid}/cards", json={"title": "b"}, headers=headers(OWNER)).json()
    moved = c.post(f"/api/cards/{b['id']}/move", json={"list": "Doing", "before_card": a["id"]},
                   headers=headers(MEMBER)).json()
    assert moved["list"] == "Doing" and moved["position"] < a["position"]
    hist = c.get(f"/api/cards/{b['id']}/history", headers=headers(VIEWER)).json()
    assert hist[0]["actor"] == MEMBER.email and hist[0]["changes"]["list"] == ["To do", "Doing"]


def test_web_page_and_static_files(client):
    c, _, _ = client
    page = c.get("/", headers=headers(OWNER))
    assert page.status_code == 200 and "Cobra" in page.text
    assert c.get("/static/../main.py", headers=headers(OWNER)).status_code == 404


# ---------------------------------------------------------------- MCP
def test_mcp_lists_action_tools_not_tables(client):
    c, _, _ = client
    h = {**headers(MEMBER), **MCP_ACCEPT}
    c.post("/mcp", headers=h, json={"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
        "protocolVersion": "2025-06-18", "capabilities": {},
        "clientInfo": {"name": "t", "version": "0"}}})
    tools = c.post("/mcp", headers=h, json={"jsonrpc": "2.0", "id": 2,
                                            "method": "tools/list"}).json()["result"]["tools"]
    names = {t["name"] for t in tools}
    assert {"my_week", "add_card", "move_card", "overdue", "what_is_blocking", "show_board",
            "suggest_checklist", "similar_cards", "daily_summary"} <= names
    assert not any(n.startswith(("select", "insert", "update_", "delete")) for n in names)
    hints = {t["name"]: t.get("annotations", {}) for t in tools}
    assert hints["my_week"]["readOnlyHint"] is True and hints["move_card"]["readOnlyHint"] is False


def test_mcp_story_add_move_and_my_week(client):
    c, s, bid = client
    err, out = mcp(c, MEMBER, "add_card", board="Launch", title="Send invoice", list="Doing",
                   assignee=MEMBER.email, due="2026-11-12")
    assert not err and "#1 Send invoice" in out and "Doing" in out
    err, out = mcp(c, MEMBER, "my_week")
    assert not err and "Due in the next 7 days" in out and "#1 Send invoice" in out
    err, out = mcp(c, MEMBER, "move_card", card="1", list="done", board="launch")
    assert not err and "Done" in out
    err, out = mcp(c, OWNER, "card_history", card="1", board="Launch")
    assert "moved from Doing to Done" in out


def test_mcp_viewer_and_stranger_are_refused(client):
    c, s, bid = client
    s.add_card(OWNER, bid, "Ship v1")
    err, out = mcp(c, VIEWER, "move_card", card="1", list="Done", board="Launch")
    assert err and "Not allowed" in out
    err, out = mcp(c, VIEWER, "show_board", board="Launch")
    assert not err and "read only" in out and "#1 Ship v1" in out
    err, out = mcp(c, STRANGER, "show_board", board="Launch")
    assert err and "Not found" in out
    err, out = mcp(c, STRANGER, "my_boards")
    assert not err and "not on any board" in out
    assert s.card_detail(OWNER, "1", "Launch")["list"] == "To do"


def test_mcp_what_is_blocking_the_launch(client):
    c, s, bid = client
    ship = s.add_card(OWNER, bid, "Launch the site")
    s.add_card(OWNER, bid, "QA pass")
    s.add_card(OWNER, bid, "Fix login bug")
    mcp(c, MEMBER, "add_blocker", card="1", blocked_by="2", board="Launch")
    mcp(c, MEMBER, "add_blocker", card="2", blocked_by="3", board="Launch")
    err, out = mcp(c, VIEWER, "what_is_blocking", card=str(ship["number"]), board="Launch")
    assert not err and "#2 QA pass" in out and "Start with: #3 Fix login bug" in out


def test_mcp_ambiguity_is_a_question_not_a_guess(client):
    c, s, _ = client
    ops = s.create_board(OWNER, "Ops")
    s.add_card(OWNER, ops["id"], "Ops card")
    s.add_card(OWNER, s.my_boards(OWNER)[0]["id"], "Launch card")
    err, out = mcp(c, OWNER, "move_card", card="1", list="Doing")
    assert err and "several of your boards" in out


def test_mcp_checklist_suggestion_saves_nothing(client):
    c, s, bid = client
    s.add_card(OWNER, bid, "Launch page")
    err, out = mcp(c, MEMBER, "suggest_checklist", card="1", board="Launch")
    assert not err and "not saved" in out and "Draft it" in out
    assert s.card_detail(OWNER, "1", "Launch")["checklist"] == []
    err, out = mcp(c, MEMBER, "add_checklist_items", card="1", board="Launch",
                   items=["Draft it"])
    assert not err and "0/1" in out