import type { IncomingMessage, ServerResponse } from "node:http"; import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("./handleTokenVerification", () => ({ handleTokenVerification: vi.fn(), })); vi.mock("./rankSearchResults", () => ({ rankSearchResults: vi.fn(), })); vi.mock("./rerankerService", () => ({ getRerankerStatus: vi.fn(), })); vi.mock("./searchesSinceLastRestart", () => ({ incrementTextualSearchesSinceLastRestart: vi.fn(), incrementGraphicalSearchesSinceLastRestart: vi.fn(), })); vi.mock("./webSearchService", () => ({ fetchSearXNG: vi.fn(), })); import { handleTokenVerification } from "./handleTokenVerification"; import { rankSearchResults } from "./rankSearchResults"; import { getRerankerStatus } from "./rerankerService"; import { searchEndpointServerHook } from "./searchEndpointServerHook"; import { incrementGraphicalSearchesSinceLastRestart, incrementTextualSearchesSinceLastRestart, } from "./searchesSinceLastRestart"; import { fetchSearXNG } from "./webSearchService"; const mockFetch = vi.fn(); vi.stubGlobal("fetch", mockFetch); function createRequest(url: string): IncomingMessage { return { url, headers: { host: "localhost:3000" }, } as unknown as IncomingMessage; } function createResponse() { return { statusCode: 200, setHeader: vi.fn(), end: vi.fn(), } as unknown as ServerResponse & { setHeader: ReturnType; end: ReturnType; }; } function getRegisteredHandler() { const use = vi.fn(); searchEndpointServerHook({ middlewares: { use }, } as unknown as Parameters[0]); return use.mock.calls[0][0] as ( request: IncomingMessage, response: ServerResponse, next: () => void, ) => Promise; } describe("searchEndpointServerHook", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(handleTokenVerification).mockResolvedValue({ shouldContinue: true, }); vi.mocked(getRerankerStatus).mockResolvedValue(false); vi.mocked(rankSearchResults).mockImplementation( async (_query, results) => results, ); }); it("passes through requests that aren't under /search/", async () => { const handler = getRegisteredHandler(); const next = vi.fn(); await handler(createRequest("/status"), createResponse(), next); expect(next).toHaveBeenCalled(); expect(fetchSearXNG).not.toHaveBeenCalled(); }); it("responds 400 when the query parameter is missing", async () => { const handler = getRegisteredHandler(); const response = createResponse(); await handler(createRequest("/search/text?token=abc"), response, vi.fn()); expect(response.statusCode).toBe(400); expect(response.end).toHaveBeenCalledWith( JSON.stringify({ error: "Missing query parameter" }), ); expect(fetchSearXNG).not.toHaveBeenCalled(); }); it("stops processing when token verification fails", async () => { vi.mocked(handleTokenVerification).mockResolvedValue({ shouldContinue: false, }); const handler = getRegisteredHandler(); await handler( createRequest("/search/text?q=cats&token=bad"), createResponse(), vi.fn(), ); expect(fetchSearXNG).not.toHaveBeenCalled(); }); it("returns ranked text results and increments the textual search counter", async () => { vi.mocked(fetchSearXNG).mockResolvedValue([ ["Title", "Snippet", "https://example.com"], ]); const handler = getRegisteredHandler(); const response = createResponse(); await handler( createRequest("/search/text?q=cats&token=abc"), response, vi.fn(), ); expect(fetchSearXNG).toHaveBeenCalledWith("cats", "text", 30); expect(incrementTextualSearchesSinceLastRestart).toHaveBeenCalled(); expect(response.setHeader).toHaveBeenCalledWith( "Content-Type", "application/json", ); expect(response.end).toHaveBeenCalledWith( JSON.stringify([["Title", "Snippet", "https://example.com"]]), ); }); it("reranks results when the reranker is healthy and returns its reordered output", async () => { vi.mocked(fetchSearXNG).mockResolvedValue([ ["A", "snippet a", "https://a.com"], ["B", "snippet b", "https://b.com"], ]); vi.mocked(getRerankerStatus).mockResolvedValue(true); vi.mocked(rankSearchResults).mockResolvedValue([ ["B", "snippet b", "https://b.com"], ["A", "snippet a", "https://a.com"], ]); const handler = getRegisteredHandler(); const response = createResponse(); await handler( createRequest("/search/text?q=test&token=abc&limit=5"), response, vi.fn(), ); expect(fetchSearXNG).toHaveBeenCalledWith("test", "text", 5); expect(rankSearchResults).toHaveBeenCalledWith( "test", [ ["A", "snippet a", "https://a.com"], ["B", "snippet b", "https://b.com"], ], true, ); expect(response.end).toHaveBeenCalledWith( JSON.stringify([ ["B", "snippet b", "https://b.com"], ["A", "snippet a", "https://a.com"], ]), ); }); it("returns image results with thumbnails converted to data URLs and increments the graphical counter", async () => { vi.mocked(fetchSearXNG).mockResolvedValue([ [ "Cat picture", "https://example.com/cat.jpg", "https://thumb.example.com/cat.jpg", "https://example.com/cat", ], ]); mockFetch.mockResolvedValue( new Response(new Uint8Array([1, 2, 3]), { status: 200, headers: { "content-type": "image/jpeg" }, }), ); const handler = getRegisteredHandler(); const response = createResponse(); await handler( createRequest("/search/images?q=cats&token=abc"), response, vi.fn(), ); expect(incrementGraphicalSearchesSinceLastRestart).toHaveBeenCalled(); const [body] = response.end.mock.calls[0]; expect(JSON.parse(body)).toEqual([ [ "Cat picture", "https://example.com/cat.jpg", `data:image/jpeg;base64,${Buffer.from([1, 2, 3]).toString("base64")}`, "https://example.com/cat", ], ]); }); it("drops image results whose thumbnail fails to download", async () => { vi.mocked(fetchSearXNG).mockResolvedValue([ [ "Cat picture", "https://example.com/cat.jpg", "https://thumb.example.com/cat.jpg", "https://example.com/cat", ], ]); mockFetch.mockResolvedValue(new Response(null, { status: 404 })); const handler = getRegisteredHandler(); const response = createResponse(); await handler( createRequest("/search/images?q=cats&token=abc"), response, vi.fn(), ); const [body] = response.end.mock.calls[0]; expect(JSON.parse(body)).toEqual([]); }); it("responds 500 when an unexpected error is thrown", async () => { vi.mocked(fetchSearXNG).mockResolvedValue([ ["Title", "Snippet", "https://example.com"], ]); vi.mocked(getRerankerStatus).mockRejectedValue(new Error("reranker down")); const handler = getRegisteredHandler(); const response = createResponse(); await handler( createRequest("/search/text?q=cats&token=abc"), response, vi.fn(), ); expect(response.statusCode).toBe(500); expect(response.end).toHaveBeenCalledWith( JSON.stringify({ error: "Internal server error" }), ); }); });