Files
claude-code-action/test/actor.test.ts
T
Vlad MocanuandClaude Opus 4.6 6b0302d226 fix(prd-7): M3 security review — self-trigger guard, actor permissions, trigger bypass, platform validation
- Add self-mention/bot loop prevention in actor.ts (blocks actor matching bot_name)
- Switch Forgejo permission check to actor-level via getCollaboratorPermissionLevel
  with fallback to token-level repos.get() when collaborator API returns 403
- Close empty-body PR review comment trigger bypass (now returns false instead of true)
- Add platform detection cross-validation (reject PLATFORM=forgejo on github.com)
- Document URL builder defense-in-depth rationale in comment-logic.ts
- Update/add tests across 6 test files (actor, permissions, trigger, platform, mcp, m7)

Progress: 50% complete (3/6 milestones) — next: M4 test coverage review

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:31:12 +02:00

210 lines
6.9 KiB
TypeScript

#!/usr/bin/env bun
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { checkHumanActor } from "../src/github/validation/actor";
import { _resetPlatformCache } from "../src/platform/detector";
import type { Octokit } from "@octokit/rest";
import { createMockContext } from "./mockContext";
function createMockOctokit(userType: string): Octokit {
return {
users: {
getByUsername: async () => ({
data: {
type: userType,
},
}),
},
} as unknown as Octokit;
}
describe("checkHumanActor", () => {
test("should pass for human actor", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
context.actor = "human-user";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should throw error for bot actor when not allowed", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "test-bot[bot]";
context.inputs.allowedBots = "";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Workflow initiated by non-human actor: test-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
);
});
test("should pass for bot actor when all bots allowed", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "test-bot[bot]";
context.inputs.allowedBots = "*";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should pass for specific bot when in allowed list", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "dependabot[bot]";
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should pass for specific bot when in allowed list (without [bot])", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "dependabot[bot]";
context.inputs.allowedBots = "dependabot,renovate";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should throw error for bot not in allowed list", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "other-bot[bot]";
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
);
});
test("should throw error for bot not in allowed list (without [bot])", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "other-bot[bot]";
context.inputs.allowedBots = "dependabot,renovate";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
);
});
test("should treat undefined actor type as human (GitHub compatibility)", async () => {
// Edge case: GitHub API returns no `type` field
const mockOctokit = {
users: {
getByUsername: async () => ({
data: {},
}),
},
} as unknown as Octokit;
const context = createMockContext();
context.actor = "unknown-user";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
describe("Forgejo platform", () => {
let savedPlatform: string | undefined;
let savedServerUrl: string | undefined;
beforeEach(() => {
_resetPlatformCache();
savedPlatform = process.env.PLATFORM;
savedServerUrl = process.env.GITHUB_SERVER_URL;
process.env.PLATFORM = "forgejo";
// Ensure cross-validation doesn't reject PLATFORM=forgejo
process.env.GITHUB_SERVER_URL = "https://git.example.com";
});
afterEach(() => {
if (savedPlatform !== undefined) process.env.PLATFORM = savedPlatform;
else delete process.env.PLATFORM;
if (savedServerUrl !== undefined)
process.env.GITHUB_SERVER_URL = savedServerUrl;
else delete process.env.GITHUB_SERVER_URL;
_resetPlatformCache();
});
test("should skip GitHub API actor check on Forgejo for regular users", async () => {
let apiCalled = false;
const mockOctokit = {
users: {
getByUsername: async () => {
apiCalled = true;
return { data: {} };
},
},
} as unknown as Octokit;
const context = createMockContext();
context.actor = "forgejo-user";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
expect(apiCalled).toBe(false);
});
test("should skip actor check for bot-like names that don't match bot_name", async () => {
let apiCalled = false;
const mockOctokit = {
users: {
getByUsername: async () => {
apiCalled = true;
return { data: { type: "Bot" } };
},
},
} as unknown as Octokit;
const context = createMockContext();
context.actor = "some-bot[bot]";
context.inputs.allowedBots = "";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
expect(apiCalled).toBe(false);
});
test("should block self-trigger when actor matches bot_name (with [bot] suffix)", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
// Default botName is "claude[bot]" from CLAUDE_BOT_LOGIN
context.actor = "claude[bot]";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Blocking self-trigger on Forgejo",
);
});
test("should block self-trigger when actor matches bot_name without [bot] suffix", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
context.inputs.botName = "claude[bot]";
context.actor = "claude";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Blocking self-trigger on Forgejo",
);
});
test("should block self-trigger with custom bot_name", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
context.inputs.botName = "my-assistant";
context.actor = "my-assistant";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Blocking self-trigger on Forgejo",
);
});
});
});