diff --git a/src/app/api/v1/music/generations/route.ts b/src/app/api/v1/music/generations/route.ts index 7e88643dd0..3dce64136b 100644 --- a/src/app/api/v1/music/generations/route.ts +++ b/src/app/api/v1/music/generations/route.ts @@ -72,6 +72,10 @@ export async function POST(request) { } const body = validation.data; + if (typeof body.prompt !== "string" || body.prompt.trim().length === 0) { + return errorResponse(HTTP_STATUS.BAD_REQUEST, "Prompt is required"); + } + // Optional API key validation if (process.env.REQUIRE_API_KEY === "true") { const apiKey = extractApiKey(request); diff --git a/src/app/api/v1/search/route.ts b/src/app/api/v1/search/route.ts index 3b4a415f69..8bd8ac87c1 100644 --- a/src/app/api/v1/search/route.ts +++ b/src/app/api/v1/search/route.ts @@ -69,11 +69,9 @@ async function resolveSearchExecutionCredentials(providerConfig: { id: string; authType: string; }): Promise | null> { - if (providerConfig.authType === "none") { - return {}; - } - - return resolveSearchCredentials(providerConfig.id); + const credentials = await resolveSearchCredentials(providerConfig.id); + if (credentials) return credentials; + return providerConfig.authType === "none" ? {} : null; } // Helper: build domain filter array from filters object diff --git a/src/app/api/v1/videos/generations/route.ts b/src/app/api/v1/videos/generations/route.ts index 702012e793..b406bd629c 100644 --- a/src/app/api/v1/videos/generations/route.ts +++ b/src/app/api/v1/videos/generations/route.ts @@ -72,6 +72,10 @@ export async function POST(request) { } const body = validation.data; + if (typeof body.prompt !== "string" || body.prompt.trim().length === 0) { + return errorResponse(HTTP_STATUS.BAD_REQUEST, "Prompt is required"); + } + // Optional API key validation if (process.env.REQUIRE_API_KEY === "true") { const apiKey = extractApiKey(request); diff --git a/src/lib/dataPaths.js b/src/lib/dataPaths.js index 6f9b0b626f..5078bbe9e6 100644 --- a/src/lib/dataPaths.js +++ b/src/lib/dataPaths.js @@ -1,69 +1,66 @@ -import os from "os"; -import path from "path"; - -export const APP_NAME = "omniroute"; - +"use strict"; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.APP_NAME = void 0; +exports.getLegacyDotDataDir = getLegacyDotDataDir; +exports.getDefaultDataDir = getDefaultDataDir; +exports.resolveDataDir = resolveDataDir; +exports.isSamePath = isSamePath; +const path_1 = __importDefault(require("path")); +const os_1 = __importDefault(require("os")); +exports.APP_NAME = "omniroute"; function fallbackHomeDir() { const envHome = process.env.HOME || process.env.USERPROFILE; if (typeof envHome === "string" && envHome.trim().length > 0) { - return path.resolve(envHome); + return path_1.default.resolve(envHome); } - - return os.tmpdir(); + return os_1.default.tmpdir(); } - function safeHomeDir() { try { - return os.homedir(); + return os_1.default.homedir(); } catch { return fallbackHomeDir(); } } - function normalizeConfiguredPath(dir) { if (typeof dir !== "string") return null; const trimmed = dir.trim(); if (!trimmed) return null; - return path.resolve(trimmed); + return path_1.default.resolve(trimmed); } - -export function getLegacyDotDataDir() { - return path.join(safeHomeDir(), `.${APP_NAME}`); +function getLegacyDotDataDir() { + return path_1.default.join(safeHomeDir(), `.${exports.APP_NAME}`); } - -export function getDefaultDataDir() { +function getDefaultDataDir() { const homeDir = safeHomeDir(); - if (process.platform === "win32") { - const appData = process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"); - return path.join(appData, APP_NAME); + const appData = process.env.APPDATA || path_1.default.join(homeDir, "AppData", "Roaming"); + return path_1.default.join(appData, exports.APP_NAME); } - + // Support XDG on Linux/macOS when explicitly configured. const xdgConfigHome = normalizeConfiguredPath(process.env.XDG_CONFIG_HOME); if (xdgConfigHome) { - return path.join(xdgConfigHome, APP_NAME); + return path_1.default.join(xdgConfigHome, exports.APP_NAME); } - return getLegacyDotDataDir(); } - -export function resolveDataDir({ isCloud = false } = {}) { +function resolveDataDir({ isCloud = false } = {}) { if (isCloud) return "/tmp"; - const configured = normalizeConfiguredPath(process.env.DATA_DIR); if (configured) return configured; - return getDefaultDataDir(); } - -export function isSamePath(a, b) { +function isSamePath(a, b) { if (!a || !b) return false; - const normalizedA = path.resolve(a); - const normalizedB = path.resolve(b); - + const normalizedA = path_1.default.resolve(a); + const normalizedB = path_1.default.resolve(b); if (process.platform === "win32") { return normalizedA.toLowerCase() === normalizedB.toLowerCase(); } - return normalizedA === normalizedB; } diff --git a/tests/unit/prompt-required-routes.test.ts b/tests/unit/prompt-required-routes.test.ts new file mode 100644 index 0000000000..cd5e5c979e --- /dev/null +++ b/tests/unit/prompt-required-routes.test.ts @@ -0,0 +1,49 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-prompt-required-routes-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const musicRoute = await import("../../src/app/api/v1/music/generations/route.ts"); +const videoRoute = await import("../../src/app/api/v1/videos/generations/route.ts"); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("v1 video generation POST rejects requests without a prompt", async () => { + const response = await videoRoute.POST( + new Request("http://localhost/api/v1/videos/generations", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "comfyui/animatediff", + }), + }) + ); + const body = await response.json(); + + assert.equal(response.status, 400); + assert.match(body.error.message, /Prompt is required/); +}); + +test("v1 music generation POST rejects requests without a prompt", async () => { + const response = await musicRoute.POST( + new Request("http://localhost/api/v1/music/generations", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "comfyui/musicgen-medium", + }), + }) + ); + const body = await response.json(); + + assert.equal(response.status, 400); + assert.match(body.error.message, /Prompt is required/); +}); diff --git a/tests/unit/search-route.test.ts b/tests/unit/search-route.test.ts index 98fd1e41d2..05ecfc4911 100644 --- a/tests/unit/search-route.test.ts +++ b/tests/unit/search-route.test.ts @@ -224,6 +224,61 @@ test("v1 search POST accepts authless SearXNG with the built-in default base URL } }); +test("v1 search POST preserves stored SearXNG baseUrl for authless providers", async () => { + await seedConnection("searxng-search", { + apiKey: null, + authType: "none", + providerSpecificData: { + baseUrl: "http://127.0.0.1:9090/custom-search", + }, + }); + + const originalFetch = globalThis.fetch; + let capturedUrl = ""; + + globalThis.fetch = async (url) => { + capturedUrl = String(url); + return new Response( + JSON.stringify({ + results: [ + { + title: "Stored SearXNG result", + url: "https://searx.example/stored", + content: "Stored self-hosted response", + engines: ["duckduckgo"], + }, + ], + }), + { status: 200, headers: { "content-type": "application/json" } } + ); + }; + + try { + const response = await searchRoute.POST( + new Request("http://localhost/api/v1/search", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: "stored self hosted meta search", + provider: "searxng-search", + search_type: "web", + }), + }) + ); + const body = await response.json(); + + assert.equal(response.status, 200); + assert.equal( + capturedUrl, + "http://127.0.0.1:9090/custom-search/search?q=stored+self+hosted+meta+search&format=json&categories=general" + ); + assert.equal(body.provider, "searxng-search"); + assert.equal(body.results[0].title, "Stored SearXNG result"); + } finally { + globalThis.fetch = originalFetch; + } +}); + test("v1 search POST auto-select uses authless SearXNG when no API-key providers are configured", async () => { const originalFetch = globalThis.fetch; let capturedUrl = "";