* 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
185 lines
5.7 KiB
TypeScript
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");
|
|
});
|
|
});
|
|
});
|