ghpsolutions/quadro
Quadro: team boards with roles, history and an MCP endpoint for Claude and ChatGPT
A remix template · 0 remixes · Remix ghpsolutions/quadro
tests/test_doors.py
import json
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from boards.application.use_cases import Boards
from boards.infrastructure.memory import MemoryBoards
from boards.main import create_app
from conftest import MEMBER, OWNER, STRANGER, TODAY, VIEWER, FakeAssistant
ROOT = Path(__file__).resolve().parents[1]
def h(person, **extra):
return {"X-Dodil-User": json.dumps({"sub": person.email, "email": person.email}), **extra}
@pytest.fixture
def client():
svc = Boards(MemoryBoards(), FakeAssistant(), today=lambda: TODAY)
board = svc.create_board(OWNER, "Ops")
svc.invite_member(OWNER, board["id"], MEMBER.email, "member")
svc.invite_member(OWNER, board["id"], VIEWER.email, "viewer")
with TestClient(create_app(svc)) as c:
c.board = board
yield c
class Mcp:
def __init__(self, client, person):
self.c, self.h = client, h(person, accept="application/json, text/event-stream")
init = {"jsonrpc": "2.0", "id": 0, "method": "initialize", "params": {
"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": {"name": "test", "version": "0"}}}
assert self.c.post("/mcp", json=init, headers=self.h).status_code == 200
def tools(self):
r = self.c.post("/mcp", json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"},
headers=self.h)
return [t["name"] for t in r.json()["result"]["tools"]]
def call(self, name, **arguments):
r = self.c.post("/mcp", headers=self.h, json={
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": name, "arguments": arguments}}).json()["result"]
if r.get("isError"):
return {"error": r["content"][0]["text"]}
return r.get("structuredContent", {}).get("result", r.get("structuredContent"))
def test_web_needs_a_person(client):
assert client.get("/api/boards").status_code == 401
page = client.get("/", follow_redirects=False)
assert page.status_code in (302, 307) and page.headers["location"].startswith("/.dodil/auth/login")
assert client.get("/", headers=h(MEMBER)).text.count("Quadro") >= 1
assert client.get("/healthz").json() == {"ok": True}
def test_web_flow_and_roles(client):
bid = client.board["id"]
card = client.post(f"/api/boards/{bid}/cards", headers=h(MEMBER),
json={"title": "Send invoice", "list": "Doing", "due_date": "2026-11-12"}).json()
assert card["ref"] == "OPS-1" and card["list"] == "Doing"
assert client.post(f"/api/boards/{bid}/cards", json={"title": "x"},
headers=h(VIEWER)).status_code == 403
assert client.get(f"/api/boards/{bid}", headers=h(STRANGER)).status_code == 404
assert client.post(f"/api/boards/{bid}/members", json={"email": "a@b.co"},
headers=h(MEMBER)).status_code == 403
done = [l["id"] for l in client.get(f"/api/boards/{bid}", headers=h(MEMBER)).json()["lists"]
if l["done"]][0]
moved = client.post(f"/api/cards/{card['id']}/move", headers=h(MEMBER),
json={"list": done, "index": 0, "version": card["version"]})
assert moved.json()["list"] == "Done"
stale = client.patch(f"/api/cards/{card['id']}", headers=h(OWNER),
json={"title": "old view", "version": card["version"]})
assert stale.status_code == 409
detail = client.get(f"/api/cards/{card['id']}", headers=h(VIEWER)).json()
assert detail["history"][0]["changes"][0] == {"field": "list", "from": "Doing", "to": "Done"}
def test_form_posts_are_refused(client):
r = client.post("/api/boards", data={"title": "x"}, headers=h(MEMBER))
assert r.status_code == 415
def test_mcp_door_starts_sign_in(client):
r = client.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 = client.get("/.well-known/oauth-protected-resource/mcp").json()
assert meta["resource"].endswith("/mcp")
def test_mcp_answers_the_brief_examples(client):
owner, member = Mcp(client, OWNER), Mcp(client, MEMBER)
names = owner.tools()
for tool in ("my_plate", "add_card", "move_card", "overdue_cards", "what_is_blocking",
"suggest_checklist", "similar_cards", "board_summary"):
assert tool in names
assert not any(n.startswith(("sql", "query", "table")) for n in names)
# "add a card 'Send invoice' to Doing on the Ops board"
added = member.call("add_card", board="Ops board", list="Doing", title="Send invoice",
assignee=MEMBER.email, due_date="2026-11-12")
assert added["ref"] == "OPS-1" and added["list"] == "Doing"
owner.call("add_card", board="Ops", title="Old task", assignee=MEMBER.email,
due_date="2026-11-01")
# "what's on my plate this week?"
plate = member.call("my_plate")
assert [c["title"] for c in plate["due_soon"]] == ["Send invoice"]
assert [c["title"] for c in plate["overdue"]] == ["Old task"]
# "what's overdue?"
assert [c["ref"] for c in owner.call("overdue_cards")] == ["OPS-2"]
# "move card 1 to Done"
assert member.call("move_card", card="1", list="Done")["list"] == "Done"
assert member.call("card_details", card="OPS-1")["history"][0]["actor"] == MEMBER.email
def test_mcp_is_held_to_the_person(client):
owner, viewer, stranger = Mcp(client, OWNER), Mcp(client, VIEWER), Mcp(client, STRANGER)
owner.call("add_card", board="Ops", title="Secret plan")
assert stranger.call("my_boards") == []
assert "that you can see" in stranger.call("move_card", card="1", list="Done")["error"]
assert "viewers" in viewer.call("move_card", card="1", list="Done")["error"]
assert "owner" in viewer.call("invite_member", board="Ops", email="x@y.co")["error"]
assert viewer.call("show_board", board="Ops")["lists"][0]["cards"][0]["title"] == "Secret plan"
def test_schema_copies_match():
assert (ROOT / "schema.sql").read_text() == \
(ROOT / "app/boards/infrastructure/schema.sql").read_text()