Files
Kordant/web/src/server/api/routers/darkwatch.test.ts

203 lines
7.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { initTRPC, TRPCError } from "@trpc/server";
import { wrap } from "@typeschema/valibot";
import {
AddWatchlistItemSchema,
RemoveWatchlistItemSchema,
ExposureFilterSchema,
ExposureDetailsSchema,
RunScanSchema,
ReportFilterSchema,
} from "../schemas/darkwatch";
vi.mock("~/server/services/darkwatch.service", () => ({
getWatchlistItems: vi.fn(),
addWatchlistItem: vi.fn(),
removeWatchlistItem: vi.fn(),
getExposures: vi.fn(),
getExposureDetails: vi.fn(),
runScan: vi.fn(),
getScanStatus: vi.fn(),
getReports: vi.fn(),
}));
import * as darkwatchService from "~/server/services/darkwatch.service";
const mockGetWatchlistItems = vi.mocked(darkwatchService.getWatchlistItems);
const mockAddWatchlistItem = vi.mocked(darkwatchService.addWatchlistItem);
const mockRemoveWatchlistItem = vi.mocked(darkwatchService.removeWatchlistItem);
const mockGetExposures = vi.mocked(darkwatchService.getExposures);
const mockGetExposureDetails = vi.mocked(darkwatchService.getExposureDetails);
const mockRunScan = vi.mocked(darkwatchService.runScan);
const mockGetScanStatus = vi.mocked(darkwatchService.getScanStatus);
const mockGetReports = vi.mocked(darkwatchService.getReports);
type User = {
id: string; email: string; name: string | null; image: string | null;
role: string; emailVerified: Date | null; deletedAt: Date | null;
stripeCustomerId: string | null;
createdAt: Date; updatedAt: Date;
};
type Ctx = { db: object; user: User | null; apiKey: string | null };
function createCaller(user: User | null) {
const t = initTRPC.context<Ctx>().create();
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" });
return next({ ctx: { ...ctx, user: ctx.user } });
});
const router = t.router({
getWatchlist: t.procedure.use(isAuthed).query(async ({ ctx }) => {
return mockGetWatchlistItems(ctx.user.id);
}),
addWatchlistItem: t.procedure.use(isAuthed)
.input(wrap(AddWatchlistItemSchema))
.mutation(async ({ ctx, input }) => {
return mockAddWatchlistItem(ctx.user.id, input.type, input.value);
}),
removeWatchlistItem: t.procedure.use(isAuthed)
.input(wrap(RemoveWatchlistItemSchema))
.mutation(async ({ ctx, input }) => {
return mockRemoveWatchlistItem(ctx.user.id, input.itemId);
}),
getExposures: t.procedure.use(isAuthed)
.input(wrap(ExposureFilterSchema))
.query(async ({ ctx, input }) => {
return mockGetExposures(ctx.user.id, input);
}),
getExposureDetails: t.procedure.use(isAuthed)
.input(wrap(ExposureDetailsSchema))
.query(async ({ ctx, input }) => {
return mockGetExposureDetails(ctx.user.id, input.exposureId);
}),
runScan: t.procedure.use(isAuthed)
.input(wrap(RunScanSchema))
.mutation(async ({ ctx }) => {
return mockRunScan(ctx.user.id);
}),
getScanStatus: t.procedure.use(isAuthed).query(async ({ ctx }) => {
return mockGetScanStatus(ctx.user.id);
}),
getReports: t.procedure.use(isAuthed)
.input(wrap(ReportFilterSchema))
.query(async ({ ctx, input }) => {
return mockGetReports(ctx.user.id, input);
}),
});
const caller = t.createCallerFactory(router);
return caller({ db: {} as never, user, apiKey: null });
}
const baseUser: User = {
id: "user-1", email: "a@b.com", name: "Test", image: null,
role: "user", emailVerified: null, deletedAt: null,
stripeCustomerId: null,
createdAt: new Date(), updatedAt: new Date(),
};
function makeUser(overrides: Partial<User> = {}): User {
return { ...baseUser, ...overrides };
}
beforeEach(() => {
vi.clearAllMocks();
});
describe("darkwatch.getWatchlist", () => {
it("returns watchlist items for authenticated user", async () => {
const items = [{ id: "w1", type: "email", value: "test@example.com" }];
mockGetWatchlistItems.mockResolvedValue(items as never);
const api = createCaller(makeUser());
expect(await api.getWatchlist()).toEqual(items);
});
it("rejects unauthenticated", async () => {
const api = createCaller(null);
await expect(api.getWatchlist()).rejects.toThrow(TRPCError);
});
});
describe("darkwatch.addWatchlistItem", () => {
it("adds a watchlist item", async () => {
const item = { id: "w1", type: "email", value: "test@example.com" };
mockAddWatchlistItem.mockResolvedValue(item as never);
const api = createCaller(makeUser());
const result = await api.addWatchlistItem({ type: "email", value: "test@example.com" });
expect(result).toEqual(item);
});
it("rejects invalid type", async () => {
const api = createCaller(makeUser());
await expect(
api.addWatchlistItem({ type: "invalid" as never, value: "test" }),
).rejects.toThrow();
});
});
describe("darkwatch.removeWatchlistItem", () => {
it("removes a watchlist item", async () => {
mockRemoveWatchlistItem.mockResolvedValue({ id: "w1", isActive: false } as never);
const api = createCaller(makeUser());
const result = await api.removeWatchlistItem({ itemId: "w1" });
expect(result.isActive).toBe(false);
});
});
describe("darkwatch.getExposures", () => {
it("returns exposures with pagination", async () => {
const data = { items: [], total: 0, page: 1, limit: 20, totalPages: 0 };
mockGetExposures.mockResolvedValue(data);
const api = createCaller(makeUser());
const result = await api.getExposures({ page: 1, limit: 20 });
expect(result.total).toBe(0);
});
it("passes severity filter", async () => {
const data = { items: [], total: 0, page: 1, limit: 20, totalPages: 0 };
mockGetExposures.mockResolvedValue(data);
const api = createCaller(makeUser());
await api.getExposures({ severity: "critical" });
expect(mockGetExposures).toHaveBeenCalledWith("user-1", { severity: "critical", page: 1, limit: 20 });
});
});
describe("darkwatch.getExposureDetails", () => {
it("returns exposure details", async () => {
const exposure = { id: "e1", identifier: "test@example.com", watchlistItem: null };
mockGetExposureDetails.mockResolvedValue(exposure as never);
const api = createCaller(makeUser());
const result = await api.getExposureDetails({ exposureId: "e1" });
expect(result.id).toBe("e1");
});
});
describe("darkwatch.runScan", () => {
it("triggers a scan", async () => {
mockRunScan.mockResolvedValue({ scanId: "s1", queued: false });
const api = createCaller(makeUser());
const result = await api.runScan({});
expect(result.scanId).toBe("s1");
});
});
describe("darkwatch.getScanStatus", () => {
it("returns scan status", async () => {
mockGetScanStatus.mockResolvedValue({ status: "idle", scanId: null, startedAt: null, completedAt: null, progress: 0, currentSource: null, error: null });
const api = createCaller(makeUser());
const result = await api.getScanStatus();
expect(result.status).toBe("idle");
});
});
describe("darkwatch.getReports", () => {
it("returns reports", async () => {
const data = { items: [], total: 0, page: 1, limit: 20, totalPages: 0 };
mockGetReports.mockResolvedValue(data);
const api = createCaller(makeUser());
const result = await api.getReports({ page: 1, limit: 20 });
expect(result.total).toBe(0);
});
});