akif/arclet-boards
Arclet Boards — private Kanban workspaces with AppID, DataK3, shared web and MCP actions.
A remix template · 1 remixes · Remix akif/arclet-boards
tests/test_application.py
from copy import deepcopy
from datetime import date
from uuid import uuid4
import pytest
from app.domain import model as m
from tests.support import OWNER, MEMBER, VIEWER, OUTSIDER, call, fixture_board
@pytest.fixture
def fixture():
return fixture_board()
def add(service, board, person=OWNER, title="Send launch notes", **kwargs):
return call(service, "add_card", person, board_id=board["id"], list_id=board["lists"][0]["id"],
title=title, **kwargs)
def test_owner_creates_workspace_board_and_ordered_lists(fixture):
service, workspace, board = fixture
assert workspace["owner_sub"] == OWNER.sub
assert board["role"] == "owner"
assert [(row["title"], row["position"], row["is_done"]) for row in board["lists"]] == [
("To do", 0, False), ("In progress", 1, False), ("Done", 2, True)]
def test_member_can_create_and_edit_card(fixture):
service, _, board = fixture
card = add(service, board, MEMBER)
edited = call(service, "update_card", MEMBER, card_id=card["id"], title="Updated",
description="A clear description", assignee=MEMBER.email, due_date="2026-12-01",
labels=[{"name": "Design", "color": "indigo"}], version=card["version"])
assert (edited["title"], edited["assignee"], edited["due_date"]) == ("Updated", MEMBER.email, "2026-12-01")
assert call(service, "get_card", MEMBER, card_id=card["id"])["labels"] == [{"name": "Design", "color": "indigo"}]
@pytest.mark.parametrize("action,extra", [
("create_list", {"title": "Review"}),
("update_board", {"title": "Changed", "version": 3}),
("invite_member", {"email": "new@arclet.test", "role": "member"}),
("change_member_role", {"email": MEMBER.email, "role": "viewer"}),
("remove_member", {"email": MEMBER.email}),
])
def test_viewer_is_read_only_for_board_mutations(fixture, action, extra):
service, _, board = fixture
with pytest.raises(m.Forbidden):
call(service, action, VIEWER, board_id=board["id"], **extra)
def test_viewer_reads_but_cannot_create_card(fixture):
service, _, board = fixture
assert call(service, "get_board", VIEWER, board_id=board["id"])["role"] == "viewer"
with pytest.raises(m.Forbidden):
add(service, board, VIEWER)
def test_member_cannot_manage_members(fixture):
service, _, board = fixture
with pytest.raises(m.Forbidden):
call(service, "invite_member", MEMBER, board_id=board["id"], email="new@arclet.test")
def test_outsider_and_missing_board_are_indistinguishable(fixture):
service, _, board = fixture
messages = []
for bid in (board["id"], "missing"):
with pytest.raises(m.NotFound) as error:
call(service, "get_board", OUTSIDER, board_id=bid)
messages.append(str(error.value))
assert messages[0] == messages[1]
assert call(service, "list_boards", OUTSIDER) == []
assert call(service, "list_workspaces", OUTSIDER) == []
def test_workspace_membership_does_not_disclose_private_boards(fixture):
service, workspace, board = fixture
call(service, "invite_workspace_member", workspace_id=workspace["id"], email=OUTSIDER.email)
assert call(service, "list_workspaces", OUTSIDER)
assert call(service, "list_boards", OUTSIDER) == []
with pytest.raises(m.NotFound):
call(service, "get_board", OUTSIDER, board_id=board["id"])
def test_workspace_viewer_cannot_create_board(fixture):
service, workspace, _ = fixture
call(service, "invite_workspace_member", workspace_id=workspace["id"], email=OUTSIDER.email, role="viewer")
with pytest.raises(m.Forbidden):
call(service, "create_board", OUTSIDER, workspace_id=workspace["id"], title="No")
def test_only_workspace_owner_can_invite_workspace_members(fixture):
service, workspace, _ = fixture
call(service, "invite_workspace_member", workspace_id=workspace["id"], email=MEMBER.email)
with pytest.raises(m.Forbidden):
call(service, "invite_workspace_member", MEMBER, workspace_id=workspace["id"], email=OUTSIDER.email)
def test_board_member_can_see_shared_workspace_but_not_manage_it(fixture):
service, workspace, _ = fixture
result = call(service, "get_workspace", MEMBER, workspace_id=workspace["id"])
assert result["role"] == "guest"
assert "members" not in result
def test_owner_updates_board_and_stale_version_conflicts(fixture):
service, _, board = fixture
updated = call(service, "update_board", board_id=board["id"], title="Release", version=board["version"])
assert updated["title"] == "Release"
with pytest.raises(m.Conflict):
call(service, "update_board", board_id=board["id"], title="Old edit", version=board["version"])
def test_card_creation_is_logged_with_actor_and_complete_initial_values(fixture):
service, _, board = fixture
card = add(service, board, MEMBER, description="Context", assignee=OWNER.email)
activity = call(service, "get_card_activity", VIEWER, card_id=card["id"])
assert len(activity) == 1
assert activity[0]["actor_sub"] == MEMBER.sub
assert activity[0]["changes"]["card"]["after"]["description"] == "Context"
def test_cross_list_move_persists_contiguous_order_and_history(fixture):
service, _, board = fixture
cards = [add(service, board, title=f"Task {index}") for index in range(3)]
current = call(service, "get_board", board_id=board["id"])
moved = call(service, "move_card", MEMBER, card_id=cards[1]["id"], list_id=board["lists"][1]["id"],
index=0, version=1, board_version=current["version"])
fresh = call(service, "get_board", board_id=board["id"])
origin = [card for card in fresh["cards"] if card["list_id"] == board["lists"][0]["id"]]
assert [(card["title"], card["position"]) for card in origin] == [("Task 0", 0), ("Task 2", 1)]
assert moved["list_id"] == board["lists"][1]["id"]
assert call(service, "get_card_activity", card_id=cards[2]["id"])[-1]["action"] == "reordered"
def test_same_list_card_reordering(fixture):
service, _, board = fixture
cards = [add(service, board, title=f"Task {index}") for index in range(3)]
current = call(service, "get_board", board_id=board["id"])
call(service, "move_card", card_id=cards[2]["id"], list_id=cards[2]["list_id"], index=0,
version=1, board_version=current["version"])
result = call(service, "get_board", board_id=board["id"])
assert [card["title"] for card in result["cards"]] == ["Task 2", "Task 0", "Task 1"]
def test_cross_board_move_is_404_and_does_not_mutate(fixture):
service, workspace, board = fixture
card = add(service, board)
other = call(service, "create_board", workspace_id=workspace["id"], title="Other")
other = call(service, "get_board", board_id=other["id"])
current = call(service, "get_board", board_id=board["id"])
with pytest.raises(m.NotFound):
call(service, "move_card", card_id=card["id"], list_id=other["lists"][0]["id"], index=0,
version=1, board_version=current["version"])
assert call(service, "get_card", card_id=card["id"])["list_id"] == card["list_id"]
def test_move_refuses_invalid_index_and_stale_board(fixture):
service, _, board = fixture
card = add(service, board)
with pytest.raises(m.Conflict):
call(service, "move_card", card_id=card["id"], list_id=card["list_id"], index=0,
version=1, board_version=board["version"])
current = call(service, "get_board", board_id=board["id"])
with pytest.raises(m.Invalid):
call(service, "move_card", card_id=card["id"], list_id=card["list_id"], index=7,
version=1, board_version=current["version"])
def test_lists_can_be_created_renamed_and_reordered(fixture):
service, _, board = fixture
created = call(service, "create_list", MEMBER, board_id=board["id"], title="Review")
changed = call(service, "update_list", MEMBER, list_id=created["id"], title="Reviewed", is_done=True, version=1)
assert changed["is_done"] is True
fresh = call(service, "get_board", board_id=board["id"])
ids = [row["id"] for row in reversed(fresh["lists"])]
call(service, "reorder_lists", MEMBER, board_id=board["id"], list_ids=ids, version=fresh["version"])
assert [row["id"] for row in call(service, "get_board", board_id=board["id"])["lists"]] == ids
def test_list_order_requires_exact_board_list_set(fixture):
service, _, board = fixture
with pytest.raises(m.Invalid):
call(service, "reorder_lists", board_id=board["id"], list_ids=[board["lists"][0]["id"]]*3,
version=board["version"])
def test_assignment_must_be_to_board_member(fixture):
service, _, board = fixture
card = add(service, board)
with pytest.raises(m.Invalid):
call(service, "assign_card", card_id=card["id"], assignee=OUTSIDER.email, version=1)
assigned = call(service, "assign_card", card_id=card["id"], assignee=VIEWER.email, version=1)
assert assigned["assignee"] == VIEWER.email
def test_due_date_validation_is_atomic(fixture):
service, _, board = fixture
card = add(service, board)
with pytest.raises(m.Invalid):
call(service, "set_due_date", card_id=card["id"], due_date="2026-02-31", version=1)
assert call(service, "get_card", card_id=card["id"])["version"] == 1
def test_overdue_uses_utc_day_and_excludes_completed_lists():
assert m.overdue({"due_date": "2026-10-30"}, False, date(2026,10,31))
assert not m.overdue({"due_date": "2026-10-31"}, False, date(2026,10,31))
assert not m.overdue({"due_date": "2026-10-30"}, True, date(2026,10,31))
assert not m.overdue({"due_date": None}, False, date(2026,10,31))
def test_task_and_overdue_queries_do_not_leak_private_boards(fixture):
service, _, board = fixture
card = add(service, board, assignee=MEMBER.email, due_date="2020-01-01")
assert call(service, "list_my_cards", MEMBER)[0]["id"] == card["id"]
assert call(service, "list_overdue", MEMBER)[0]["id"] == card["id"]
assert call(service, "list_overdue", OUTSIDER) == []
def test_stale_card_edit_preserves_newer_values(fixture):
service, _, board = fixture
card = add(service, board)
call(service, "set_due_date", card_id=card["id"], due_date="2026-12-01", version=1)
with pytest.raises(m.Conflict):
call(service, "assign_card", card_id=card["id"], assignee=MEMBER.email, version=1)
fresh = call(service, "get_card", card_id=card["id"])
assert fresh["due_date"] == "2026-12-01" and fresh["assignee"] is None
def test_all_field_changes_have_before_and_after_history(fixture):
service, _, board = fixture
card = add(service, board)
call(service, "update_card", card_id=card["id"], title="A new title", description="New context",
assignee=MEMBER.email, due_date="2026-12-01", labels=[{"name":"Urgent","color":"rose"}], version=1)
history = call(service, "get_card_activity", card_id=card["id"])
assert [row["action"] for row in history] == ["created", "updated", "labels_updated"]
assert set(history[1]["changes"]) == {"title", "description", "assignee", "due_date"}
assert history[1]["changes"]["title"] == {"before":"Send launch notes","after":"A new title"}
def test_duplicate_labels_rejected_without_partial_write(fixture):
service, _, board = fixture
card = add(service, board)
with pytest.raises(m.Invalid):
call(service, "update_card", card_id=card["id"], title="Should roll back", description="",
assignee=None, due_date=None, labels=[{"name":"A","color":"rose"},{"name":"a","color":"blue"}], version=1)
assert call(service, "get_card", card_id=card["id"])["title"] == card["title"]
def test_removing_member_clears_assignments_with_history(fixture):
service, _, board = fixture
card = add(service, board, assignee=MEMBER.email)
call(service, "remove_member", board_id=board["id"], email=MEMBER.email)
assert call(service, "get_card", card_id=card["id"])["assignee"] is None
assert call(service, "get_card_activity", card_id=card["id"])[-1]["action"] == "member_removed"
with pytest.raises(m.NotFound):
call(service, "get_board", MEMBER, board_id=board["id"])
def test_owner_can_change_member_to_viewer(fixture):
service, _, board = fixture
call(service, "change_member_role", board_id=board["id"], email=MEMBER.email, role="viewer")
with pytest.raises(m.Forbidden):
add(service, board, MEMBER)
def test_accepted_invitation_binds_immutable_subject(fixture):
service, _, board = fixture
call(service, "get_board", MEMBER, board_id=board["id"])
impostor = m.Person("different-sub", MEMBER.email)
with pytest.raises(m.NotFound):
call(service, "get_board", impostor, board_id=board["id"])
def test_request_replay_is_exactly_once(fixture):
service, _, board = fixture
key = uuid4().hex
args = {"board_id":board["id"],"list_id":board["lists"][0]["id"],"title":"Once"}
first = service.call("add_card", OWNER, args, key)
second = service.call("add_card", OWNER, args, key)
assert first == second
assert len(call(service, "get_board", board_id=board["id"])["cards"]) == 1
assert len(call(service, "get_card_activity", card_id=first["id"])) == 1
def test_request_key_cannot_be_reused_for_different_payload(fixture):
service, _, board = fixture
key = uuid4().hex
args = {"board_id":board["id"],"list_id":board["lists"][0]["id"],"title":"Once"}
service.call("add_card", OWNER, args, key)
with pytest.raises(m.Conflict):
service.call("add_card", OWNER, {**args,"title":"Other"}, key)
def test_replay_rechecks_revoked_permissions(fixture):
service, _, board = fixture
key = uuid4().hex
args = {"board_id":board["id"],"list_id":board["lists"][0]["id"],"title":"Once"}
service.call("add_card", MEMBER, args, key)
call(service, "remove_member", board_id=board["id"], email=MEMBER.email)
with pytest.raises(m.NotFound):
service.call("add_card", MEMBER, args, key)
def test_commit_failure_rolls_back_card_history_and_idempotency(fixture):
service, _, board = fixture
before = deepcopy(service.store.rows)
service.store.fail_after = m.Unavailable("Storage interrupted")
with pytest.raises(m.Unavailable):
add(service, board)
assert service.store.rows == before
def test_checklist_changes_are_authorized_versioned_and_logged(fixture):
service, _, board = fixture
card = add(service, board)
item = call(service, "add_checklist_item", MEMBER, card_id=card["id"], title="Review copy", version=1)
with pytest.raises(m.Forbidden):
call(service, "complete_checklist_item", VIEWER, item_id=item["id"], completed=True, version=1)
call(service, "complete_checklist_item", MEMBER, item_id=item["id"], completed=True, version=1)
fresh = call(service, "get_card", card_id=card["id"])
assert fresh["checklist"][0]["completed"] is True
assert [row["action"] for row in fresh["activity"]][-2:] == ["checklist_item_added","checklist_item_changed"]
def test_userless_mutation_and_identity_arguments_are_rejected(fixture):
service, _, _ = fixture
with pytest.raises(m.Unauthenticated):
service.call("create_workspace", None, {"title":"No"}, uuid4().hex)
with pytest.raises(m.Invalid):
call(service, "create_workspace", title="No", user_sub=OWNER.sub)
@pytest.mark.parametrize("title", ["", " " * 10, "X" * 161, "Bad\x00title"])
def test_invalid_card_titles_are_rejected(fixture, title):
service, _, board = fixture
with pytest.raises(m.Invalid):
add(service, board, title=title)