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