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()