Files
calendar/packages/features/bot-detection/BotDetectionService.test.ts
T
sean-brydonandGitHub 7799b191ec feat: botid enabled on api/book/event api route (#24207)
* wip

* WIP

* restore event

* testing without instrument client

* Add conditional for botID init

* Bump BotID version + pass in header

* botID yarn lock changes

* feat: Add feature flag checks + tidy up into service

* rely on env var also

* use eventType repo instead of passing in prisma

* remove slug from audit

* rename botId feature to botid

* add unit tests for bot service
2025-10-06 11:16:13 +01:00

185 lines
5.7 KiB
TypeScript

import type { IncomingHttpHeaders } from "http";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { HttpError } from "@calcom/lib/http-error";
import type { EventTypeRepository } from "@calcom/lib/server/repository/eventTypeRepository";
import { BotDetectionService } from "./BotDetectionService";
// Mock the botid/server module
vi.mock("botid/server", () => ({
checkBotId: vi.fn(),
}));
// Mock the logger
vi.mock("@calcom/lib/logger", () => ({
default: {
getSubLogger: vi.fn(() => ({
warn: vi.fn(),
info: vi.fn(),
error: vi.fn(),
})),
},
}));
describe("BotDetectionService", () => {
let botDetectionService: BotDetectionService;
let mockFeaturesRepository: FeaturesRepository;
let mockEventTypeRepository: EventTypeRepository;
let mockHeaders: IncomingHttpHeaders;
beforeEach(() => {
// Reset all mocks before each test
vi.clearAllMocks();
// Setup mock repositories
mockFeaturesRepository = {
checkIfTeamHasFeature: vi.fn(),
} as unknown as FeaturesRepository;
mockEventTypeRepository = {
getTeamIdByEventTypeId: vi.fn(),
} as unknown as EventTypeRepository;
mockHeaders = {
"user-agent": "Mozilla/5.0",
"x-forwarded-for": "192.168.1.1",
};
botDetectionService = new BotDetectionService(mockFeaturesRepository, mockEventTypeRepository);
// Reset environment variable
delete process.env.NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER;
});
describe("checkBotDetection", () => {
it("should return early if BotID is not enabled at instance level", async () => {
process.env.NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER = "0";
await botDetectionService.checkBotDetection({
eventTypeId: 123,
headers: mockHeaders,
});
expect(mockEventTypeRepository.getTeamIdByEventTypeId).not.toHaveBeenCalled();
});
it("should return early if no eventTypeId is provided", async () => {
process.env.NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER = "1";
await botDetectionService.checkBotDetection({
headers: mockHeaders,
});
expect(mockEventTypeRepository.getTeamIdByEventTypeId).not.toHaveBeenCalled();
});
it("should return early if event type has no teamId", async () => {
process.env.NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER = "1";
vi.mocked(mockEventTypeRepository.getTeamIdByEventTypeId).mockResolvedValue({
teamId: null,
});
await botDetectionService.checkBotDetection({
eventTypeId: 123,
headers: mockHeaders,
});
expect(mockFeaturesRepository.checkIfTeamHasFeature).not.toHaveBeenCalled();
});
it("should return early if BotID feature is not enabled for the team", async () => {
process.env.NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER = "1";
vi.mocked(mockEventTypeRepository.getTeamIdByEventTypeId).mockResolvedValue({
teamId: 456,
});
vi.mocked(mockFeaturesRepository.checkIfTeamHasFeature).mockResolvedValue(false);
const { checkBotId } = await import("botid/server");
await botDetectionService.checkBotDetection({
eventTypeId: 123,
headers: mockHeaders,
});
expect(checkBotId).not.toHaveBeenCalled();
});
it("should throw HttpError when a bot is detected", async () => {
process.env.NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER = "1";
vi.mocked(mockEventTypeRepository.getTeamIdByEventTypeId).mockResolvedValue({
teamId: 456,
});
vi.mocked(mockFeaturesRepository.checkIfTeamHasFeature).mockResolvedValue(true);
const { checkBotId } = await import("botid/server");
vi.mocked(checkBotId).mockResolvedValue({
isBot: true,
isHuman: false,
isVerifiedBot: false,
verifiedBotName: undefined,
verifiedBotCategory: undefined,
bypassed: false,
classificationReason: "suspicious-patterns",
});
await expect(
botDetectionService.checkBotDetection({
eventTypeId: 123,
headers: mockHeaders,
})
).rejects.toThrow(HttpError);
await expect(
botDetectionService.checkBotDetection({
eventTypeId: 123,
headers: mockHeaders,
})
).rejects.toThrow("Access denied");
});
it("should pass when a human is detected", async () => {
process.env.NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER = "1";
vi.mocked(mockEventTypeRepository.getTeamIdByEventTypeId).mockResolvedValue({
teamId: 456,
});
vi.mocked(mockFeaturesRepository.checkIfTeamHasFeature).mockResolvedValue(true);
const { checkBotId } = await import("botid/server");
vi.mocked(checkBotId).mockResolvedValue({
isBot: false,
isHuman: true,
isVerifiedBot: false,
verifiedBotName: undefined,
verifiedBotCategory: undefined,
bypassed: false,
classificationReason: "human-behavior",
});
await expect(
botDetectionService.checkBotDetection({
eventTypeId: 123,
headers: mockHeaders,
})
).resolves.not.toThrow();
});
it("should check feature flag with correct teamId", async () => {
const teamId = 789;
process.env.NEXT_PUBLIC_VERCEL_USE_BOTID_IN_BOOKER = "1";
vi.mocked(mockEventTypeRepository.getTeamIdByEventTypeId).mockResolvedValue({
teamId,
});
vi.mocked(mockFeaturesRepository.checkIfTeamHasFeature).mockResolvedValue(false);
await botDetectionService.checkBotDetection({
eventTypeId: 123,
headers: mockHeaders,
});
expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(teamId, "booker-botid");
});
});
});