#!/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", ); }); }); });