diff --git a/tests/integration/agent-api.test.ts b/tests/integration/agent-api.test.ts index 3279834..e17a6d1 100644 --- a/tests/integration/agent-api.test.ts +++ b/tests/integration/agent-api.test.ts @@ -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; + }; + + 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); + }); +});