diff --git a/src/pages/api/search/searchSuggestion.js b/src/pages/api/search/searchSuggestion.js index 13f3f3015..f03759c2c 100644 --- a/src/pages/api/search/searchSuggestion.js +++ b/src/pages/api/search/searchSuggestion.js @@ -9,6 +9,10 @@ export default async function handler(req, res) { const provider = Object.values(searchProviders).find(({ name }) => name === providerName); + if (!provider) { + return res.json([query, []]); + } + if (provider.name === "Custom") { const widgets = await widgetsFromConfig(); const searchWidget = widgets.find((w) => w.type === "search"); diff --git a/src/pages/api/search/searchSuggestion.test.js b/src/pages/api/search/searchSuggestion.test.js new file mode 100644 index 000000000..bcc24b54f --- /dev/null +++ b/src/pages/api/search/searchSuggestion.test.js @@ -0,0 +1,106 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import createMockRes from "test-utils/create-mock-res"; + +const { providers, getSettings, widgetsFromConfig, cachedRequest } = vi.hoisted(() => ({ + providers: { + custom: { name: "Custom", url: false, suggestionUrl: null }, + google: { name: "Google", url: "https://google?q=", suggestionUrl: "https://google/suggest?q=" }, + empty: { name: "NoSuggest", url: "x", suggestionUrl: null }, + }, + getSettings: vi.fn(), + widgetsFromConfig: vi.fn(), + cachedRequest: vi.fn(), +})); + +vi.mock("components/widgets/search/search", () => ({ + searchProviders: { + custom: providers.custom, + google: providers.google, + empty: providers.empty, + }, +})); + +vi.mock("utils/config/config", () => ({ + getSettings, +})); + +vi.mock("utils/config/widget-helpers", () => ({ + widgetsFromConfig, +})); + +vi.mock("utils/proxy/http", () => ({ + cachedRequest, +})); + +import handler from "./searchSuggestion"; + +describe("pages/api/search/searchSuggestion", () => { + beforeEach(() => { + vi.clearAllMocks(); + + // Reset provider objects since handler mutates the Custom provider. + providers.custom.url = false; + providers.custom.suggestionUrl = null; + }); + + it("returns empty suggestions when providerName is unknown", async () => { + const req = { query: { query: "hello", providerName: "Unknown" } }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.body).toEqual(["hello", []]); + }); + + it("returns empty suggestions when provider has no suggestionUrl", async () => { + const req = { query: { query: "hello", providerName: "NoSuggest" } }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.body).toEqual(["hello", []]); + }); + + it("calls cachedRequest for a standard provider", async () => { + cachedRequest.mockResolvedValueOnce(["q", ["a"]]); + + const req = { query: { query: "hello world", providerName: "Google" } }; + const res = createMockRes(); + + await handler(req, res); + + expect(cachedRequest).toHaveBeenCalledWith("https://google/suggest?q=hello%20world", 5, "Mozilla/5.0"); + expect(res.body).toEqual(["q", ["a"]]); + }); + + it("resolves Custom provider suggestionUrl from widgets.yaml when present", async () => { + widgetsFromConfig.mockResolvedValueOnce([ + { type: "search", options: { url: "https://custom?q=", suggestionUrl: "https://custom/suggest?q=" } }, + ]); + cachedRequest.mockResolvedValueOnce(["q", ["x"]]); + + const req = { query: { query: "hello", providerName: "Custom" } }; + const res = createMockRes(); + + await handler(req, res); + + expect(cachedRequest).toHaveBeenCalledWith("https://custom/suggest?q=hello", 5, "Mozilla/5.0"); + expect(res.body).toEqual(["q", ["x"]]); + }); + + it("falls back to quicklaunch custom settings when no search widget is configured", async () => { + widgetsFromConfig.mockResolvedValueOnce([]); + getSettings.mockReturnValueOnce({ + quicklaunch: { provider: "custom", url: "https://ql?q=", suggestionUrl: "https://ql/suggest?q=" }, + }); + cachedRequest.mockResolvedValueOnce(["q", ["y"]]); + + const req = { query: { query: "hello", providerName: "Custom" } }; + const res = createMockRes(); + + await handler(req, res); + + expect(cachedRequest).toHaveBeenCalledWith("https://ql/suggest?q=hello", 5, "Mozilla/5.0"); + }); +}); diff --git a/src/pages/api/siteMonitor.test.js b/src/pages/api/siteMonitor.test.js new file mode 100644 index 000000000..b580ec772 --- /dev/null +++ b/src/pages/api/siteMonitor.test.js @@ -0,0 +1,103 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import createMockRes from "test-utils/create-mock-res"; + +const { getServiceItem, httpProxy, perf, logger } = vi.hoisted(() => ({ + getServiceItem: vi.fn(), + httpProxy: vi.fn(), + perf: { now: vi.fn() }, + logger: { debug: vi.fn() }, +})); + +vi.mock("perf_hooks", () => ({ + performance: perf, +})); + +vi.mock("utils/config/service-helpers", () => ({ + getServiceItem, +})); + +vi.mock("utils/proxy/http", () => ({ + httpProxy, +})); + +vi.mock("utils/logger", () => ({ + default: () => logger, +})); + +import handler from "./siteMonitor"; + +describe("pages/api/siteMonitor", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns 400 when the service item is missing", async () => { + getServiceItem.mockResolvedValueOnce(null); + + const req = { query: { groupName: "g", serviceName: "s" } }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.statusCode).toBe(400); + expect(res.body.error).toContain("Unable to find service"); + }); + + it("returns 400 when the monitor URL is missing", async () => { + getServiceItem.mockResolvedValueOnce({ siteMonitor: "" }); + + const req = { query: { groupName: "g", serviceName: "s" } }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.statusCode).toBe(400); + expect(res.body.error).toBe("No http monitor URL given"); + }); + + it("uses HEAD and returns status + latency when the response is OK", async () => { + getServiceItem.mockResolvedValueOnce({ siteMonitor: "http://example.com" }); + perf.now.mockReturnValueOnce(1).mockReturnValueOnce(11); + httpProxy.mockResolvedValueOnce([200]); + + const req = { query: { groupName: "g", serviceName: "s" } }; + const res = createMockRes(); + + await handler(req, res); + + expect(httpProxy).toHaveBeenCalledWith("http://example.com", { method: "HEAD" }); + expect(res.statusCode).toBe(200); + expect(res.body.status).toBe(200); + expect(res.body.latency).toBe(10); + }); + + it("falls back to GET when HEAD is rejected", async () => { + getServiceItem.mockResolvedValueOnce({ siteMonitor: "http://example.com" }); + perf.now.mockReturnValueOnce(1).mockReturnValueOnce(2).mockReturnValueOnce(5).mockReturnValueOnce(15); + httpProxy.mockResolvedValueOnce([500]).mockResolvedValueOnce([200]); + + const req = { query: { groupName: "g", serviceName: "s" } }; + const res = createMockRes(); + + await handler(req, res); + + expect(httpProxy).toHaveBeenNthCalledWith(1, "http://example.com", { method: "HEAD" }); + expect(httpProxy).toHaveBeenNthCalledWith(2, "http://example.com"); + expect(res.statusCode).toBe(200); + expect(res.body).toEqual({ status: 200, latency: 10 }); + }); + + it("returns 400 when httpProxy throws", async () => { + getServiceItem.mockResolvedValueOnce({ siteMonitor: "http://example.com" }); + httpProxy.mockRejectedValueOnce(new Error("nope")); + + const req = { query: { groupName: "g", serviceName: "s" } }; + const res = createMockRes(); + + await handler(req, res); + + expect(res.statusCode).toBe(400); + expect(res.body.error).toContain("Error attempting http monitor"); + }); +});