From c2e4e577bfa0c465dd008afd2b7f0f1424a54c1f Mon Sep 17 00:00:00 2001 From: xmtp-coder-agent <> Date: Thu, 9 Apr 2026 20:12:42 +0000 Subject: [PATCH] test: verify permission check uses assigner, not issue author Add explicit tests verifying that the write-access permission check for task creation uses the sender (the person who assigned the bot), not the issue author. This ensures issues created by external users without write access still work when a maintainer assigns the bot. Resolves https://github.com/xmtplabs/coder-action/issues/91 Co-Authored-By: Claude Opus 4.6 --- src/handler-dispatcher.test.ts | 12 +++++++ src/handlers/create-task.test.ts | 57 ++++++++++++++++++++++++++++++++ src/webhook-router.test.ts | 23 +++++++++++++ 3 files changed, 92 insertions(+) diff --git a/src/handler-dispatcher.test.ts b/src/handler-dispatcher.test.ts index 95833d2..2559d98 100644 --- a/src/handler-dispatcher.test.ts +++ b/src/handler-dispatcher.test.ts @@ -113,6 +113,18 @@ describe("HandlerDispatcher", () => { expect(coder.getCoderUserByGitHubId).toHaveBeenCalledWith(67890); }); + test("resolves coder user from sender (assigner), not issue author", async () => { + // senderId represents the person who assigned the bot, not the issue author + const ctx = { + ...createTaskContext, + senderLogin: "org-maintainer", + senderId: 99999, + }; + const result = makeDispatchedResult("create_task", ctx); + await dispatcher.dispatch(result); + expect(coder.getCoderUserByGitHubId).toHaveBeenCalledWith(99999); + }); + test("creates a task and returns ActionOutputs", async () => { coder.getTask.mockResolvedValue(null); coder.createTask.mockResolvedValue(mockTask); diff --git a/src/handlers/create-task.test.ts b/src/handlers/create-task.test.ts index f1f558a..02ae8f5 100644 --- a/src/handlers/create-task.test.ts +++ b/src/handlers/create-task.test.ts @@ -80,6 +80,63 @@ describe("CreateTaskHandler", () => { expect(coder.createTask).not.toHaveBeenCalled(); }); + // Permission check must use the sender (assigner), not the issue author. + // This covers the case where an external user without write access creates + // the issue and a maintainer with write access assigns the bot. + test("checks permission for senderLogin (the assigner)", async () => { + github.checkActorPermission.mockResolvedValue(true); + coder.getTask.mockResolvedValue(null); + coder.createTask.mockResolvedValue(mockTask); + + const ctx = { + ...issueContext, + senderLogin: "maintainer-who-assigned", + }; + const handler = new CreateTaskHandler( + coder, + github as unknown as import("../github-client").GitHubClient, + baseInputs, + ctx, + logger, + ); + await handler.run(); + + expect(github.checkActorPermission).toHaveBeenCalledWith( + ctx.owner, + ctx.repo, + "maintainer-who-assigned", + ); + }); + + test("creates task when assigner has write access regardless of issue author", async () => { + github.checkActorPermission.mockResolvedValue(true); + coder.getTask.mockResolvedValue(null); + coder.createTask.mockResolvedValue(mockTask); + + // senderLogin is the maintainer who assigned the bot, not the issue author + const ctx = { + ...issueContext, + senderLogin: "org-maintainer", + }; + const handler = new CreateTaskHandler( + coder, + github as unknown as import("../github-client").GitHubClient, + baseInputs, + ctx, + logger, + ); + const result = await handler.run(); + + expect(result.skipped).toBe(false); + expect(coder.createTask).toHaveBeenCalledTimes(1); + // Verify permission was checked for the assigner, not anyone else + expect(github.checkActorPermission).toHaveBeenCalledWith( + "xmtp", + "libxmtp", + "org-maintainer", + ); + }); + // AC #4: Issue URL appended to prompt test("appends issue URL to prompt", async () => { github.checkActorPermission.mockResolvedValue(true); diff --git a/src/webhook-router.test.ts b/src/webhook-router.test.ts index 26f24ce..b554309 100644 --- a/src/webhook-router.test.ts +++ b/src/webhook-router.test.ts @@ -75,6 +75,29 @@ describe("WebhookRouter", () => { expect(ctx.senderId).toBe(65710); }); + test("issues.assigned extracts senderLogin from sender, not issue author", async () => { + // Simulate: external user created the issue, maintainer assigned the bot + const payload = { + ...issuesAssigned, + issue: { + ...issuesAssigned.issue, + user: { login: "external-user", id: 11111 }, + }, + sender: { login: "org-maintainer", id: 22222 }, + }; + const result = await router.handleWebhook( + "issues", + "delivery-001b", + payload, + ); + + expect(result.dispatched).toBe(true); + if (!result.dispatched) throw new Error("expected dispatched"); + const ctx = result.context as CreateTaskContext; + expect(ctx.senderLogin).toBe("org-maintainer"); + expect(ctx.senderId).toBe(22222); + }); + test("issues.assigned with non-matching assignee login → skipped", async () => { const payload = { ...issuesAssigned,