test(agent-api): auth-refusal on all six routes, case-insensitive move,
no-double-accept, Inbox-never-accepts, and 404s Adds the whole-branch-review test gaps: a single parametrised test looping over all 6 (method, path, body) combinations asserting 401 for missing and wrong tokens plus no-write-occurred for the 3 write routes; a move-route case-insensitivity assertion (real column name uppercased); a negative test that moving an already-accepted card again does not re-log card_accepted; a negative test that moving a card into Inbox never sets accepted: true; and 404 checks for a nonexistent card id across GET/POST/POST/PATCH. Each new describe block creates and cleans up its own dedicated fixture card(s), since the existing cardId fixtures from earlier describes in this file are already deleted by their own afterAll by the time these run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
76a012bb43
commit
0f2ce3fed8
1 changed files with 274 additions and 0 deletions
|
|
@ -236,3 +236,277 @@ describe("card detail, comments, subtasks, and move", () => {
|
|||
expect(activity.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authentication refuses all six routes", () => {
|
||||
let authCardId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const card = await admin.collection("cards").create({
|
||||
project: projectId,
|
||||
column: inboxColumnId,
|
||||
title: `Agent API auth-refusal probe ${Date.now()}`,
|
||||
description: "",
|
||||
position: 999997,
|
||||
priority: "medium",
|
||||
created_by_role: "owner",
|
||||
accepted: false,
|
||||
archived: false,
|
||||
});
|
||||
authCardId = card.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (authCardId) await admin.collection("cards").delete(authCardId).catch(() => {});
|
||||
});
|
||||
|
||||
it("returns 401 for a missing or wrong token on every route, and writes nothing", async () => {
|
||||
const backlog = await admin.collection("columns").getOne(backlogColumnId);
|
||||
|
||||
type RouteCase = {
|
||||
method: string;
|
||||
path: string;
|
||||
body?: unknown;
|
||||
assertNoWrite?: () => Promise<void>;
|
||||
};
|
||||
|
||||
const routes: RouteCase[] = [
|
||||
{ method: "GET", path: "/api/agent/projects" },
|
||||
{ method: "GET", path: `/api/agent/projects/${projectId}/cards` },
|
||||
{ method: "GET", path: `/api/agent/cards/${authCardId}` },
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/agent/cards/${authCardId}/comments`,
|
||||
body: { body: "should never be created" },
|
||||
assertNoWrite: async () => {
|
||||
const comments = await admin.collection("comments").getFullList({
|
||||
filter: `card = "${authCardId}"`,
|
||||
});
|
||||
expect(comments).toHaveLength(0);
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/agent/cards/${authCardId}/subtasks`,
|
||||
body: { titles: ["should never be created"] },
|
||||
assertNoWrite: async () => {
|
||||
const subtasks = await admin.collection("subtasks").getFullList({
|
||||
filter: `card = "${authCardId}"`,
|
||||
});
|
||||
expect(subtasks).toHaveLength(0);
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "PATCH",
|
||||
path: `/api/agent/cards/${authCardId}/move`,
|
||||
body: { column: backlog.name },
|
||||
assertNoWrite: async () => {
|
||||
const card = await admin.collection("cards").getOne(authCardId);
|
||||
expect(card.column).toBe(inboxColumnId);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const route of routes) {
|
||||
const baseHeaders: HeadersInit | undefined = route.body
|
||||
? { "content-type": "application/json" }
|
||||
: undefined;
|
||||
const requestBody = route.body ? JSON.stringify(route.body) : undefined;
|
||||
|
||||
const noToken = await fetch(`${APP_URL}${route.path}`, {
|
||||
method: route.method,
|
||||
headers: baseHeaders,
|
||||
body: requestBody,
|
||||
});
|
||||
expect(noToken.status, `${route.method} ${route.path} (no token)`).toBe(401);
|
||||
|
||||
const wrongToken = await fetch(`${APP_URL}${route.path}`, {
|
||||
method: route.method,
|
||||
headers: { ...(baseHeaders ?? {}), authorization: "Bearer not-the-real-token" },
|
||||
body: requestBody,
|
||||
});
|
||||
expect(wrongToken.status, `${route.method} ${route.path} (wrong token)`).toBe(401);
|
||||
|
||||
if (route.assertNoWrite) await route.assertNoWrite();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/agent/cards/:cardId/move column-name case-insensitivity", () => {
|
||||
let cardId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const card = await admin.collection("cards").create({
|
||||
project: projectId,
|
||||
column: inboxColumnId,
|
||||
title: `Agent API case-insensitivity probe ${Date.now()}`,
|
||||
description: "",
|
||||
position: 999996,
|
||||
priority: "medium",
|
||||
created_by_role: "owner",
|
||||
accepted: false,
|
||||
archived: false,
|
||||
});
|
||||
cardId = card.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (cardId) await admin.collection("cards").delete(cardId).catch(() => {});
|
||||
});
|
||||
|
||||
it("resolves the destination column regardless of case", async () => {
|
||||
const backlog = await admin.collection("columns").getOne(backlogColumnId);
|
||||
const differentCase =
|
||||
backlog.name === backlog.name.toUpperCase()
|
||||
? backlog.name.toLowerCase()
|
||||
: backlog.name.toUpperCase();
|
||||
expect(differentCase).not.toBe(backlog.name);
|
||||
|
||||
const response = await authed(`/api/agent/cards/${cardId}/move`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ column: differentCase }),
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const updated = await admin.collection("cards").getOne(cardId);
|
||||
expect(updated.column).toBe(backlogColumnId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/agent/cards/:cardId/move does not double-log acceptance", () => {
|
||||
let cardId: string;
|
||||
let secondWorkingColumnId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const secondWorking = await admin
|
||||
.collection("columns")
|
||||
.getFirstListItem(
|
||||
`project = "${projectId}" && is_inbox = false && is_done = false && id != "${backlogColumnId}"`
|
||||
);
|
||||
secondWorkingColumnId = secondWorking.id;
|
||||
|
||||
const card = await admin.collection("cards").create({
|
||||
project: projectId,
|
||||
column: inboxColumnId,
|
||||
title: `Agent API double-accept probe ${Date.now()}`,
|
||||
description: "",
|
||||
position: 999995,
|
||||
priority: "medium",
|
||||
created_by_role: "owner",
|
||||
accepted: false,
|
||||
archived: false,
|
||||
});
|
||||
cardId = card.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (cardId) await admin.collection("cards").delete(cardId).catch(() => {});
|
||||
});
|
||||
|
||||
it("does not add a second card_accepted row when an already-accepted card moves again", async () => {
|
||||
const backlog = await admin.collection("columns").getOne(backlogColumnId);
|
||||
const secondWorking = await admin.collection("columns").getOne(secondWorkingColumnId);
|
||||
|
||||
const firstMove = await authed(`/api/agent/cards/${cardId}/move`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ column: backlog.name }),
|
||||
});
|
||||
expect(firstMove.status).toBe(200);
|
||||
const firstBody = await firstMove.json();
|
||||
expect(firstBody.accepted).toBe(true);
|
||||
|
||||
const activityAfterFirst = await admin.collection("activity").getFullList({
|
||||
filter: `card = "${cardId}" && action = "card_accepted"`,
|
||||
});
|
||||
expect(activityAfterFirst.length).toBe(1);
|
||||
|
||||
const secondMove = await authed(`/api/agent/cards/${cardId}/move`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ column: secondWorking.name }),
|
||||
});
|
||||
expect(secondMove.status).toBe(200);
|
||||
|
||||
const activityAfterSecond = await admin.collection("activity").getFullList({
|
||||
filter: `card = "${cardId}" && action = "card_accepted"`,
|
||||
});
|
||||
expect(activityAfterSecond.length).toBe(activityAfterFirst.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/agent/cards/:cardId/move into Inbox never accepts", () => {
|
||||
let cardId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const card = await admin.collection("cards").create({
|
||||
project: projectId,
|
||||
column: backlogColumnId,
|
||||
title: `Agent API inbox-move probe ${Date.now()}`,
|
||||
description: "",
|
||||
position: 999994,
|
||||
priority: "medium",
|
||||
created_by_role: "owner",
|
||||
accepted: false,
|
||||
archived: false,
|
||||
});
|
||||
cardId = card.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (cardId) await admin.collection("cards").delete(cardId).catch(() => {});
|
||||
});
|
||||
|
||||
it("never sets accepted true when moving a card into the Inbox", async () => {
|
||||
const inbox = await admin.collection("columns").getOne(inboxColumnId);
|
||||
|
||||
const response = await authed(`/api/agent/cards/${cardId}/move`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ column: inbox.name }),
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
expect(body.accepted).toBe(false);
|
||||
|
||||
const updated = await admin.collection("cards").getOne(cardId);
|
||||
expect(updated.column).toBe(inboxColumnId);
|
||||
expect(updated.accepted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("404s for a nonexistent card id", () => {
|
||||
const bogusId = "does-not-exist-xyz";
|
||||
|
||||
it("GET card detail 404s", async () => {
|
||||
const response = await authed(`/api/agent/cards/${bogusId}`);
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it("POST comments 404s", async () => {
|
||||
const response = await authed(`/api/agent/cards/${bogusId}/comments`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ body: "test" }),
|
||||
});
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it("POST subtasks 404s", async () => {
|
||||
const response = await authed(`/api/agent/cards/${bogusId}/subtasks`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ titles: ["test"] }),
|
||||
});
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it("PATCH move 404s", async () => {
|
||||
const response = await authed(`/api/agent/cards/${bogusId}/move`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ column: "Backlog" }),
|
||||
});
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue