mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 21:02:12 +03:00
Escape backslashes before injecting combo stream tags so tagged SSE payloads remain valid JSON, and call the provider models handler directly during sync to avoid internal fetch SSRF warnings. Also restore crypto UUID generation for Gemini request translation, tighten dashboard error sanitization, and update tests to use stricter URL matching and UUID-based fixture keys.
432 lines
12 KiB
JavaScript
432 lines
12 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-search-"));
|
|
|
|
const { handleSearch } = await import("../../open-sse/handlers/search.ts");
|
|
|
|
test("handleSearch builds Serper web requests and normalizes organic results", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let captured;
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
captured = {
|
|
url: String(url),
|
|
headers: init.headers,
|
|
body: JSON.parse(String(init.body || "{}")),
|
|
};
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
organic: [
|
|
{
|
|
title: "Latest AI",
|
|
link: "https://www.example.com/news?id=1",
|
|
snippet: "Top story",
|
|
date: "2026-04-05",
|
|
},
|
|
{
|
|
title: "Second",
|
|
link: "https://docs.example.com/page",
|
|
description: "Fallback description",
|
|
},
|
|
],
|
|
searchParameters: { totalResults: 20 },
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: " latest ai ",
|
|
provider: "serper-search",
|
|
maxResults: 3,
|
|
searchType: "web",
|
|
country: "US",
|
|
language: "en",
|
|
credentials: { apiKey: "serper-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.equal(captured.url, "https://google.serper.dev/search");
|
|
assert.equal(captured.headers["X-API-Key"], "serper-key");
|
|
assert.deepEqual(captured.body, {
|
|
q: "latest ai",
|
|
num: 3,
|
|
gl: "us",
|
|
hl: "en",
|
|
});
|
|
assert.equal(result.data.provider, "serper-search");
|
|
assert.equal(result.data.query, "latest ai");
|
|
assert.equal(result.data.results[0].display_url, "example.com/news");
|
|
assert.equal(result.data.results[0].citation.provider, "serper-search");
|
|
assert.equal(result.data.metrics.total_results_available, 20);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds Brave news requests and normalizes favicon metadata", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let capturedUrl;
|
|
let capturedHeaders;
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
capturedUrl = String(url);
|
|
capturedHeaders = init.headers;
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: [
|
|
{
|
|
title: "Brave item",
|
|
url: "https://news.example.com/story",
|
|
description: "Breaking news",
|
|
age: "2026-04-05",
|
|
meta_url: { favicon: "https://news.example.com/favicon.ico" },
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "market update",
|
|
provider: "brave-search",
|
|
maxResults: 1,
|
|
searchType: "news",
|
|
country: "BR",
|
|
language: "pt",
|
|
credentials: { apiKey: "brave-key" },
|
|
log: null,
|
|
});
|
|
|
|
const url = new URL(capturedUrl);
|
|
assert.equal(url.origin + url.pathname, "https://api.search.brave.com/res/v1/news/search");
|
|
assert.equal(url.searchParams.get("q"), "market update");
|
|
assert.equal(url.searchParams.get("count"), "1");
|
|
assert.equal(url.searchParams.get("country"), "BR");
|
|
assert.equal(url.searchParams.get("search_lang"), "pt");
|
|
assert.equal(capturedHeaders["X-Subscription-Token"], "brave-key");
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results[0].favicon_url, "https://news.example.com/favicon.ico");
|
|
assert.equal(result.data.results[0].published_at, "2026-04-05");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds Perplexity requests, forwards filters, and enforces maxResults", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let captured;
|
|
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
captured = {
|
|
url: String(url),
|
|
headers: init.headers,
|
|
body: JSON.parse(String(init.body || "{}")),
|
|
};
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: [
|
|
{ title: "One", url: "https://one.example.com", snippet: "First", date: "2026-04-05" },
|
|
{ title: "Two", url: "https://two.example.com", snippet: "Second" },
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "ai agents",
|
|
provider: "perplexity-search",
|
|
maxResults: 1,
|
|
searchType: "web",
|
|
country: "US",
|
|
language: "en",
|
|
domainFilter: ["example.com", "-spam.com"],
|
|
credentials: { apiKey: "perplexity-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(captured.url, "https://api.perplexity.ai/search");
|
|
assert.equal(captured.headers.Authorization, "Bearer perplexity-key");
|
|
assert.deepEqual(captured.body, {
|
|
query: "ai agents",
|
|
max_results: 1,
|
|
country: "US",
|
|
search_language_filter: ["en"],
|
|
search_domain_filter: ["example.com", "-spam.com"],
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results.length, 1);
|
|
assert.equal(result.data.usage.queries_used, 1);
|
|
assert.equal(result.data.usage.search_cost_usd, 0.005);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds Exa requests with include/exclude domains and preserves rich result fields", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let captured;
|
|
|
|
globalThis.fetch = async (_url, init = {}) => {
|
|
captured = JSON.parse(String(init.body || "{}"));
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: [
|
|
{
|
|
title: "Exa result",
|
|
url: "https://exa.example.com/page",
|
|
highlights: ["Highlighted snippet"],
|
|
score: 1.2,
|
|
publishedDate: "2026-04-04",
|
|
favicon: "https://exa.example.com/favicon.ico",
|
|
author: "Author",
|
|
image: "https://exa.example.com/image.png",
|
|
text: "Full document text",
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "agentic workflows",
|
|
provider: "exa-search",
|
|
maxResults: 5,
|
|
searchType: "news",
|
|
domainFilter: ["allowed.com", "-blocked.com"],
|
|
credentials: { apiKey: "exa-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.deepEqual(captured, {
|
|
query: "agentic workflows",
|
|
numResults: 5,
|
|
type: "auto",
|
|
text: true,
|
|
highlights: true,
|
|
includeDomains: ["allowed.com"],
|
|
excludeDomains: ["blocked.com"],
|
|
category: "news",
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results[0].snippet, "Highlighted snippet");
|
|
assert.equal(result.data.results[0].score, 1);
|
|
assert.equal(result.data.results[0].content.text, "Full document text");
|
|
assert.equal(result.data.results[0].metadata.author, "Author");
|
|
assert.equal(result.data.results[0].metadata.image_url, "https://exa.example.com/image.png");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch builds Tavily requests with topic and raw content normalization", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let captured;
|
|
|
|
globalThis.fetch = async (_url, init = {}) => {
|
|
captured = JSON.parse(String(init.body || "{}"));
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
results: [
|
|
{
|
|
title: "Tavily result",
|
|
url: "https://tavily.example.com/page",
|
|
content: "Summary",
|
|
score: 0.7,
|
|
published_date: "2026-04-03",
|
|
raw_content: "Long form content",
|
|
},
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "policy changes",
|
|
provider: "tavily-search",
|
|
maxResults: 2,
|
|
searchType: "news",
|
|
country: "us",
|
|
domainFilter: ["good.com", "-bad.com"],
|
|
credentials: { apiKey: "tavily-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.deepEqual(captured, {
|
|
query: "policy changes",
|
|
max_results: 2,
|
|
topic: "news",
|
|
include_domains: ["good.com"],
|
|
exclude_domains: ["bad.com"],
|
|
country: "us",
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.results[0].snippet, "Summary");
|
|
assert.equal(result.data.results[0].content.text, "Long form content");
|
|
assert.equal(result.data.results[0].citation.rank, 1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch rejects queries with invalid control characters", async () => {
|
|
const result = await handleSearch({
|
|
query: "bad\u0000query",
|
|
provider: "serper-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "x" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 400);
|
|
assert.equal(result.error, "Query contains invalid control characters");
|
|
});
|
|
|
|
test("handleSearch rejects queries that become empty after normalization", async () => {
|
|
const result = await handleSearch({
|
|
query: " \n\t ",
|
|
provider: "serper-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "x" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 400);
|
|
assert.equal(result.error, "Query is empty after normalization");
|
|
});
|
|
|
|
test("handleSearch rejects unknown providers", async () => {
|
|
const result = await handleSearch({
|
|
query: "hello",
|
|
provider: "unknown-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "x" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 400);
|
|
assert.equal(result.error, "Unknown search provider: unknown-search");
|
|
});
|
|
|
|
test("handleSearch requires credentials for the configured provider", async () => {
|
|
const result = await handleSearch({
|
|
query: "hello",
|
|
provider: "serper-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: {},
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 401);
|
|
assert.equal(result.error, "No credentials for search provider: serper-search");
|
|
});
|
|
|
|
test("handleSearch fails over to the alternate provider on retriable upstream errors", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const calls = [];
|
|
|
|
globalThis.fetch = async (url) => {
|
|
calls.push(String(url));
|
|
|
|
if (String(url).startsWith("https://google.serper.dev/")) {
|
|
return new Response("upstream broken", { status: 500 });
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
web: {
|
|
results: [
|
|
{
|
|
title: "Fallback result",
|
|
url: "https://fallback.example.com",
|
|
description: "Recovered",
|
|
},
|
|
],
|
|
totalCount: 1,
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "fallback test",
|
|
provider: "serper-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "serper-key" },
|
|
alternateProvider: "brave-search",
|
|
alternateCredentials: { apiKey: "brave-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, true);
|
|
assert.deepEqual(calls, [
|
|
"https://google.serper.dev/search",
|
|
"https://api.search.brave.com/res/v1/web/search?q=fallback+test&count=5",
|
|
]);
|
|
assert.equal(result.data.provider, "brave-search");
|
|
assert.equal(result.data.results[0].title, "Fallback result");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleSearch does not fail over on non-retriable upstream errors", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let callCount = 0;
|
|
|
|
globalThis.fetch = async () => {
|
|
callCount += 1;
|
|
return new Response("unauthorized", { status: 401 });
|
|
};
|
|
|
|
try {
|
|
const result = await handleSearch({
|
|
query: "no failover",
|
|
provider: "serper-search",
|
|
maxResults: 5,
|
|
searchType: "web",
|
|
credentials: { apiKey: "serper-key" },
|
|
alternateProvider: "brave-search",
|
|
alternateCredentials: { apiKey: "brave-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 401);
|
|
assert.equal(result.error, "Search provider serper-search returned 401");
|
|
assert.equal(callCount, 1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|