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_more.py
"""Every route and every MCP tool, through the real HTTP stack with the gateway's identity header:
what a signed-in person gets back, what a viewer and a stranger get back, and what the agent reads."""
import json
import pytest
from conftest import MEMBER, OWNER, STRANGER, VIEWER, headers
from fastapi.testclient import TestClient
from test_doors import mcp
from boards.doors import text
from boards.main import create_app
@pytest.fixture
def client(svc):
s, bid = svc
with TestClient(create_app(s)) as c:
yield c, s, bid
def card(c, bid, who=OWNER, **body):
r = c.post(f"/api/boards/{bid}/cards", json={"title": "Plan", **body}, headers=headers(who))
assert r.status_code == 201, r.text
return r.json()
# ---------------------------------------------------------------- identity
@pytest.mark.parametrize("raw", ["not json", json.dumps({"sub": ""}), json.dumps({})])
def test_an_identity_header_without_a_person_is_no_one(client, raw):
c, _, _ = client
assert c.get("/api/me", headers={"X-Dodil-User": raw}).status_code == 401
def test_who_am_i_and_the_email_is_lowercased(client):
c, _, _ = client
h = {"X-Dodil-User": json.dumps({"sub": "x", "email": " Bo@Cobra.Test "})}
assert c.get("/api/me", headers=h).json() == {"email": MEMBER.email}
def test_the_sign_in_metadata_names_the_app(client):
c, _, _ = client
meta = c.get("/.well-known/oauth-protected-resource").json()
assert meta["resource_name"] == "Cobra Boards" and meta["bearer_methods_supported"] == ["header"]
# ---------------------------------------------------------------- boards & members over the web
def test_web_create_board_and_list_my_boards(client):
c, _, _ = client
r = c.post("/api/boards", json={"title": "Ops"}, headers=headers(MEMBER))
assert r.status_code == 201 and r.json()["lists"] == ["To do", "Doing", "Done"]
mine = c.get("/api/boards", headers=headers(MEMBER)).json()
assert {(b["title"], b["role"]) for b in mine} == {("Launch", "member"), ("Ops", "owner")}
def test_web_a_newcomer_gets_the_sample_board(client):
c, _, _ = client
mine = c.get("/api/boards", headers=headers(STRANGER)).json()
assert [b["title"] for b in mine] == ["Welcome to Cobra 🐍"] and mine[0]["overdue"] == 2
def test_web_only_the_owner_renames(client):
c, _, bid = client
assert c.patch(f"/api/boards/{bid}", json={"title": "Ours"},
headers=headers(MEMBER)).status_code == 403
r = c.patch(f"/api/boards/{bid}", json={"title": "Launch 2027"}, headers=headers(OWNER))
assert r.status_code == 200 and r.json()["title"] == "Launch 2027"
def test_web_invite_and_remove_a_member(client):
c, _, bid = client
card(c, bid, assignee=MEMBER.email)
r = c.put(f"/api/boards/{bid}/members", json={"email": "eve@cobra.test", "role": "viewer"},
headers=headers(OWNER))
assert r.status_code == 201 and r.json()["role"] == "viewer"
r = c.delete(f"/api/boards/{bid}/members/{MEMBER.email}", headers=headers(OWNER))
assert r.status_code == 200 and r.json()["cards_unassigned"] == 1
assert c.get(f"/api/boards/{bid}", headers=headers(MEMBER)).status_code == 404
def test_web_lists_add_and_rename(client):
c, _, bid = client
r = c.post(f"/api/boards/{bid}/lists", json={"name": "Review"}, headers=headers(MEMBER))
assert r.status_code == 201
lid = r.json()["id"]
r = c.patch(f"/api/boards/{bid}/lists/{lid}", json={"name": "QA", "is_done": True},
headers=headers(MEMBER))
assert r.json()["name"] == "QA" and r.json()["is_done"] is True
assert c.post(f"/api/boards/{bid}/lists", json={"name": "qa"},
headers=headers(MEMBER)).status_code == 422
def test_web_activity_and_summary(client):
c, _, bid = client
card(c, bid, due_date="2026-11-01")
act = c.get(f"/api/boards/{bid}/activity", headers=headers(VIEWER)).json()
assert act[0]["summary"].startswith("added #1")
summary = c.get(f"/api/boards/{bid}/summary", headers=headers(VIEWER)).json()
assert summary["facts"]["overdue"] == [1]
# ---------------------------------------------------------------- cards over the web
def test_web_bad_bodies_are_422(client):
c, _, bid = client
h = headers(OWNER)
assert c.post(f"/api/boards/{bid}/cards", json={}, headers=h).status_code == 422
assert c.post(f"/api/boards/{bid}/cards", json={"title": " "}, headers=h).status_code == 422
assert c.post(f"/api/boards/{bid}/cards", json={"title": "x", "due_date": "someday"},
headers=h).status_code == 422
assert c.post(f"/api/boards/{bid}/cards", json={"title": "x", "assignee": STRANGER.email},
headers=h).status_code == 422
def test_web_card_detail_has_everything_the_page_shows(client):
c, _, bid = client
made = card(c, bid, assignee=MEMBER.email, labels=["urgent"], description="Rough")
d = c.get(f"/api/cards/{made['id']}", headers=headers(VIEWER)).json()
assert d["can_edit"] is False and d["people"] == [OWNER.email, MEMBER.email, VIEWER.email]
assert d["labels"] == ["urgent"] and d["history"][0]["action"] == "created"
assert d["checklist"] == [] and d["blocks"] == [] and d["blocked_by_all"] == []
def test_web_stranger_gets_404_on_every_card_route(client):
c, _, bid = client
cid = card(c, bid)["id"]
h = headers(STRANGER)
assert c.get(f"/api/cards/{cid}/history", headers=h).status_code == 404
assert c.get(f"/api/cards/{cid}/blocking", headers=h).status_code == 404
assert c.patch(f"/api/cards/{cid}", json={"title": "x"}, headers=h).status_code == 404
assert c.post(f"/api/cards/{cid}/move", json={"list": "Done"}, headers=h).status_code == 404
assert c.post(f"/api/cards/{cid}/checklist/suggest", headers=h).status_code == 404
def test_web_blockers_add_explain_and_remove(client):
c, _, bid = client
ship, qa = card(c, bid, title="Ship"), card(c, bid, title="QA")
r = c.post(f"/api/cards/{ship['id']}/blockers", json={"blocked_by": qa["id"]},
headers=headers(MEMBER))
assert r.status_code == 201 and r.json() == {"card": 1, "blocked_by": 2}
why = c.get(f"/api/cards/{ship['id']}/blocking", headers=headers(VIEWER)).json()
assert why["blocked"] and why["start_with"][0]["title"] == "QA"
assert c.post(f"/api/cards/{qa['id']}/blockers", json={"blocked_by": ship["id"]},
headers=headers(MEMBER)).status_code == 422 # a loop
r = c.delete(f"/api/cards/{ship['id']}/blockers/{qa['id']}", headers=headers(MEMBER))
assert r.status_code == 200
assert c.get(f"/api/cards/{ship['id']}", headers=headers(OWNER)).json()["blocked_by"] == []
def test_web_checklist_suggest_add_tick_and_delete(client):
c, _, bid = client
cid = card(c, bid)["id"]
h = headers(MEMBER)
sug = c.post(f"/api/cards/{cid}/checklist/suggest", headers=h).json()
assert sug["suggested_steps"] == ["Draft it", "Review it", "Ship it"]
items = c.post(f"/api/cards/{cid}/checklist", json={"items": sug["suggested_steps"][:2]},
headers=h).json()["checklist"]
first = items[0]["id"]
ticked = c.patch(f"/api/cards/{cid}/checklist/{first}", json={"done": True}, headers=h).json()
assert ticked["checklist"][0]["done"] is True
left = c.delete(f"/api/cards/{cid}/checklist/{first}", headers=h).json()["checklist"]
assert [i["text"] for i in left] == ["Review it"]
assert c.patch(f"/api/cards/{cid}/checklist/{first}", json={"done": True},
headers=h).status_code == 404
assert c.post(f"/api/cards/{cid}/checklist", json={"items": ["x"]},
headers=headers(VIEWER)).status_code == 403
def test_web_my_week_overdue_and_search(client):
c, _, bid = client
card(c, bid, title="Send invoice", assignee=MEMBER.email, due_date="2026-11-01")
week = c.get("/api/me/week", headers=headers(MEMBER)).json()
assert [x["title"] for x in week["overdue"]] == ["Send invoice"]
assert [x["title"] for x in c.get("/api/overdue", headers=headers(VIEWER)).json()] == [
"Send invoice"]
found = c.get("/api/search", params={"q": "invoice"}, headers=headers(VIEWER)).json()
assert [x["number"] for x in found] == [1]
assert c.get("/api/search", params={"q": " "}, headers=headers(VIEWER)).status_code == 422
assert c.get("/api/search", headers=headers(VIEWER)).status_code == 422
assert c.get("/api/search", params={"q": "invoice"}, headers=headers(STRANGER)).json() == []
def test_web_similar_by_card_and_by_text(client):
c, _, bid = client
inv = card(c, bid, title="Send invoice to Acme")
card(c, bid, title="Chase unpaid bill")
card(c, bid, title="Design the logo")
by_card = c.post(f"/api/cards/{inv['id']}/similar", headers=headers(MEMBER)).json()
assert by_card["cards"][0]["title"] == "Chase unpaid bill"
by_text = c.post("/api/similar", json={"text": "logo design"}, headers=headers(MEMBER)).json()
assert by_text["cards"][0]["title"] == "Design the logo"
def test_web_move_before_another_card_and_stale_move(client):
c, _, bid = client
a, b = card(c, bid, title="a"), card(c, bid, title="b")
r = c.post(f"/api/cards/{b['id']}/move", json={"list": "To do", "before_card": a["id"]},
headers=headers(MEMBER))
assert r.status_code == 200 and r.json()["position"] < a["position"]
stale = c.post(f"/api/cards/{b['id']}/move",
json={"list": "Done", "expected_version": b["version"]}, headers=headers(OWNER))
assert stale.status_code == 409
# ---------------------------------------------------------------- MCP: the tool list
def tools(c):
h = {**headers(MEMBER), "accept": "application/json, text/event-stream"}
c.post("/mcp", headers=h, json={"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": {"name": "t", "version": "0"}}})
return c.post("/mcp", headers=h, json={"jsonrpc": "2.0", "id": 2,
"method": "tools/list"}).json()["result"]["tools"]
def test_mcp_has_exactly_the_action_tools(client):
c, _, _ = client
assert {t["name"] for t in tools(c)} == {
"my_boards", "show_board", "my_week", "overdue", "find_cards", "show_card",
"card_history", "what_is_blocking", "create_board", "add_list", "invite",
"remove_member", "add_card", "move_card", "assign_card", "edit_card", "add_blocker",
"remove_blocker", "suggest_checklist", "add_checklist_items", "check_item",
"similar_cards", "daily_summary", "add_comment", "my_notifications",
"mark_notifications_read", "archive_card", "restore_card", "archived_cards"}
def test_mcp_every_tool_explains_itself_and_says_if_it_writes(client):
c, _, _ = client
reads = {"my_boards", "show_board", "my_week", "overdue", "find_cards", "show_card",
"card_history", "what_is_blocking", "suggest_checklist", "similar_cards",
"daily_summary", "my_notifications", "archived_cards"}
for t in tools(c):
assert len(t["description"]) > 30, t["name"]
assert t["annotations"]["readOnlyHint"] is (t["name"] in reads), t["name"]
assert t["annotations"]["openWorldHint"] is False
# ---------------------------------------------------------------- MCP: each tool, as the agent reads it
def test_mcp_create_board_and_a_done_list(client):
c, _, _ = client
err, out = mcp(c, MEMBER, "create_board", title="Ops")
assert not err and "Created **Ops**" in out and "You're the owner" in out
err, out = mcp(c, MEMBER, "add_list", board="Ops", name="Shipped", is_done=True)
assert not err and "(a done list)" in out
def test_mcp_my_boards_lists_roles_and_counts(client):
c, s, bid = client
s.add_card(OWNER, bid, "Late", due_date="2026-11-01")
err, out = mcp(c, VIEWER, "my_boards")
assert not err and "**Launch** (viewer) · 1 open · 1 overdue" in out
def test_mcp_invite_and_remove_are_owner_only(client):
c, s, bid = client
s.add_card(OWNER, bid, "Mine", assignee=MEMBER.email)
err, out = mcp(c, MEMBER, "invite", board="Launch", email="eve@cobra.test")
assert err and "Not allowed" in out
err, out = mcp(c, OWNER, "invite", board="Launch", email="eve@cobra.test", role="viewer")
assert not err and "eve@cobra.test is now a viewer on Launch" in out
err, out = mcp(c, OWNER, "remove_member", board="Launch", email=MEMBER.email)
assert not err and "1 card(s) unassigned" in out
def test_mcp_assign_to_me_and_unassign(client):
c, s, bid = client
s.add_card(OWNER, bid, "Send invoice")
err, out = mcp(c, MEMBER, "assign_card", card="1", assignee="me", board="Launch")
assert not err and out.startswith("Assigned #1") and f"@{MEMBER.email}" in out
err, out = mcp(c, MEMBER, "assign_card", card="1", board="Launch")
assert not err and out.startswith("Unassigned #1")
def test_mcp_edit_with_a_stale_version_is_a_conflict(client):
c, s, bid = client
s.add_card(OWNER, bid, "Budget")
err, out = mcp(c, MEMBER, "edit_card", card="1", board="Launch", expected_version=1,
title="Budget v2", due="tomorrow")
assert not err and "Budget v2" in out and "due 2026-11-11" in out and "(version 2)" in out
err, out = mcp(c, OWNER, "edit_card", card="1", board="Launch", expected_version=1,
title="Mine")
assert err and "Conflict" in out and "reload" in out
assert s.card_detail(OWNER, "1", "Launch")["title"] == "Budget v2"
def test_mcp_a_bad_date_is_explained(client):
c, _, _ = client
err, out = mcp(c, MEMBER, "add_card", board="Launch", title="x", due="someday")
assert err and "is not a date" in out and "tomorrow" in out
def test_mcp_find_cards_and_nothing_overdue(client):
c, s, bid = client
s.add_card(OWNER, bid, "Send invoice", labels=["finance"])
err, out = mcp(c, VIEWER, "find_cards", query="finance")
assert not err and "#1 Send invoice" in out and "[Launch › To do]" in out
err, out = mcp(c, VIEWER, "overdue")
assert not err and out == "Nothing is overdue. 🎉"
err, out = mcp(c, VIEWER, "find_cards", query="nothing like this")
assert out == "No card mentions “nothing like this”."
def test_mcp_show_card_reads_like_the_card(client):
c, s, bid = client
a = s.add_card(OWNER, bid, "Ship", description="Go live", assignee=MEMBER.email,
due_date="2026-11-01", labels=["launch"])
b = s.add_card(OWNER, bid, "QA")
s.add_blocker(OWNER, a["id"], b["id"])
s.add_checklist_items(OWNER, a["id"], ["Deploy"])
err, out = mcp(c, VIEWER, "show_card", card="1", board="Launch")
assert not err
for bit in ("**#1 Ship** · Launch › To do", "Go live", f"assignee: {MEMBER.email}",
"due: 2026-11-01 (OVERDUE)", "labels: launch", "Waits on: #2 QA",
"Checklist 0/1", "Recent history"):
assert bit in out, bit
def test_mcp_blockers_add_already_and_remove(client):
c, s, bid = client
s.add_card(OWNER, bid, "Ship")
s.add_card(OWNER, bid, "QA")
_, out = mcp(c, MEMBER, "add_blocker", card="1", blocked_by="2", board="Launch")
assert out == "#1 is now blocked by #2."
_, out = mcp(c, MEMBER, "add_blocker", card="1", blocked_by="2", board="Launch")
assert out == "#1 already waits on #2."
err, out = mcp(c, MEMBER, "add_blocker", card="2", blocked_by="1", board="Launch")
assert err and "forever" in out
_, out = mcp(c, MEMBER, "remove_blocker", card="1", blocked_by="2", board="Launch")
assert out == "#1 no longer waits on #2."
_, out = mcp(c, VIEWER, "what_is_blocking", card="1", board="Launch")
assert out == "Nothing is blocking #1 Ship: it can go ahead."
def test_mcp_check_item_ticks_a_step(client):
c, s, bid = client
a = s.add_card(OWNER, bid, "Launch page")
item = s.add_checklist_items(OWNER, a["id"], ["Draft it"])["checklist"][0]
err, out = mcp(c, MEMBER, "check_item", card="1", item_id=item["id"], board="Launch")
assert not err and "checklist 1/1" in out and "[x] Draft it" in out
err, out = mcp(c, VIEWER, "check_item", card="1", item_id=item["id"], board="Launch",
done=False)
assert err and "Not allowed" in out
def test_mcp_similar_and_daily_summary(client):
c, s, bid = client
s.add_card(OWNER, bid, "Send invoice to Acme")
s.add_card(OWNER, bid, "Chase unpaid bill", due_date="2026-11-01")
err, out = mcp(c, MEMBER, "similar_cards", text_query="invoice payment")
assert not err and "alike" in out and "Send invoice to Acme" in out
err, out = mcp(c, VIEWER, "daily_summary", board="Launch")
assert not err and out.startswith("**Launch** · 2026-11-10") and "Briefing for Launch" in out
def test_mcp_my_week_when_nothing_is_mine(client):
c, _, _ = client
err, out = mcp(c, VIEWER, "my_week")
assert not err and out == "Nothing open is assigned to you (today is 2026-11-10)."
def test_mcp_a_stranger_sees_none_of_the_cards(client):
c, s, bid = client
s.add_card(OWNER, bid, "Secret plan")
for tool, args in (("show_card", {"card": "1"}), ("card_history", {"card": "1"}),
("what_is_blocking", {"card": "1"}), ("daily_summary", {"board": "Launch"}),
("move_card", {"card": "1", "list": "Done"})):
err, out = mcp(c, STRANGER, tool, **args)
assert err and "Not found" in out, tool
err, out = mcp(c, STRANGER, "find_cards", query="secret")
assert not err and "No card" in out
# ---------------------------------------------------------------- the chat text itself
def test_chat_text_for_an_empty_board_and_a_blocked_chain():
b = {"title": "Ops", "role": "viewer", "can_edit": False,
"lists": [{"name": "To do", "is_done": False, "cards": []}],
"members": [{"email": OWNER.email, "role": "owner"}]}
out = text.board(b)
assert "(read only)" in out and "- (empty)" in out and f"{OWNER.email} (owner)" in out
r = {"card": 1, "title": "Ship", "blocked": True, "start_with": [{"number": 3, "title": "Bug"}],
"chain": [{"number": 2, "title": "QA", "depth": 1, "assignee": None, "list": "Doing",
"waits_on": [3]},
{"number": 3, "title": "Bug", "depth": 2, "assignee": "bo@cobra.test",
"list": "To do", "waits_on": []}]}
out = text.blocking(r)
assert "waiting on 2 unfinished cards" in out and "(unassigned) ← waits on #3" in out
assert " - #3 Bug [To do] @bo@cobra.test" in out and out.endswith("Start with: #3 Bug")