dmslanes/lanes
Lanes - team task boards with roles, history and an MCP endpoint for Claude and ChatGPT
A remix template · 0 remixes · Remix dmslanes/lanes
tests/test_doors.py
"""Both doors over one app: the web routes and the MCP tools obey the same rules."""
import json
import os
import pytest
os.environ["DEV_ALLOW_ANON"] = "1"
os.environ["LANES_STORAGE"] = "memory"
os.environ["LANES_NO_AUTOAPP"] = "1"
from fastapi.testclient import TestClient # noqa: E402
from app.main import create_app # noqa: E402
OWNER, MEMBER, VIEWER, STRANGER = "olga@team.io", "mike@team.io", "vera@team.io", "sam@else.io"
def as_(email):
return {"x-dev-user": email}
@pytest.fixture
def client():
with TestClient(create_app(), base_url="http://localhost") as c:
yield c
@pytest.fixture
def board(client):
ws = client.post("/api/workspaces", json={"name": "Acme"}, headers=as_(OWNER)).json()
for email, role in ((MEMBER, "member"), (VIEWER, "viewer")):
assert client.post(f"/api/workspaces/{ws['id']}/members", json={"email": email, "role": role},
headers=as_(OWNER)).status_code == 201
return client.post("/api/boards", json={"workspace_id": ws["id"], "name": "Ops"}, headers=as_(OWNER)).json()
def mcp(client, email, tool, **args):
r = client.post("/mcp", headers={**as_(email), "accept": "application/json, text/event-stream",
"content-type": "application/json"},
json={"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": tool, "arguments": args}})
assert r.status_code == 200, r.text
result = r.json()["result"]
text = result["content"][0]["text"] if result.get("content") else ""
sc = result.get("structuredContent")
if sc is None:
err = result.get("isError", False)
return err, text if err else json.loads(text)
return result.get("isError", False), sc["result"] if set(sc) == {"result"} else sc
def test_web_roles(client, board):
r = client.post(f"/api/boards/{board['id']}/cards", json={"list": "To do", "title": "Send invoice"}, headers=as_(MEMBER))
assert r.status_code == 201
n = r.json()["number"]
assert client.post(f"/api/cards/{n}/move", json={"to_list": "Done"}, headers=as_(VIEWER)).status_code == 403
assert client.get(f"/api/boards/{board['id']}", headers=as_(STRANGER)).status_code == 404
assert client.get("/api/boards", headers=as_(STRANGER)).json() == []
assert client.post(f"/api/cards/{n}/move", json={"to_list": "Done"}, headers=as_(MEMBER)).status_code == 200
assert [h["action"] for h in client.get(f"/api/cards/{n}/history", headers=as_(OWNER)).json()] == ["created", "moved"]
def test_mcp_actions_like_an_assistant(client, board):
err, card = mcp(client, MEMBER, "add_card", board="Ops board", list="Doing", title="Send invoice",
assignee=MEMBER, due_date="2000-01-01")
assert not err and card["list"] == "Doing"
err, plate = mcp(client, MEMBER, "my_cards", days=7)
assert [c["title"] for c in plate] == ["Send invoice"]
err, late = mcp(client, VIEWER, "overdue_cards")
assert [c["number"] for c in late] == [card["number"]]
err, moved = mcp(client, MEMBER, "move_card", card=str(card["number"]), to_list="Done")
assert not err and moved["list"] == "Done"
err, msg = mcp(client, VIEWER, "move_card", card=str(card["number"]), to_list="Doing")
assert err and "forbidden" in msg
err, boards = mcp(client, STRANGER, "list_boards")
assert boards == []
err, msg = mcp(client, STRANGER, "show_board", board="Ops")
assert err and "not_found" in msg
def test_mcp_without_identity_asks_to_sign_in(monkeypatch):
monkeypatch.setenv("DEV_ALLOW_ANON", "0")
monkeypatch.setenv("DODIL_USER_POOL", "acme/lanes-users")
with TestClient(create_app(), base_url="http://localhost") as c:
r = c.post("/mcp", json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
assert r.status_code == 401
assert "oauth-protected-resource/mcp" in r.headers["www-authenticate"]
meta = c.get("/.well-known/oauth-protected-resource/mcp").json()
assert meta["authorization_servers"] == ["https://appid.dodil.io/acme/lanes-users"]
assert c.get("/api/me").status_code == 401
assert c.get("/healthz").status_code == 200
def test_forged_dev_header_is_ignored_when_deployed(monkeypatch):
monkeypatch.setenv("DEV_ALLOW_ANON", "0")
with TestClient(create_app(), base_url="http://localhost") as c:
assert c.get("/api/me", headers=as_(OWNER)).status_code == 401
who = {"x-dodil-user": json.dumps({"sub": "1", "email": "Olga@Team.io"})}
assert c.get("/api/me", headers=who).json()["email"] == OWNER