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().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 { 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); }); });