refactor: 统一管理页面布局 — FilterToolbar + usePageSearchParams + parseListParams
This commit is contained in:
134
tests/server/list-params.test.ts
Normal file
134
tests/server/list-params.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import type { RuntimeMode } from "../../src/shared/api";
|
||||
|
||||
import { parseListParams } from "../../src/server/helpers/list-params";
|
||||
|
||||
const mode: RuntimeMode = "test";
|
||||
|
||||
function makeUrl(params: Record<string, string> = {}): URL {
|
||||
const sp = new URLSearchParams(params);
|
||||
return new URL(`http://localhost/api/test?${sp.toString()}`);
|
||||
}
|
||||
|
||||
describe("parseListParams", () => {
|
||||
test("returns defaults when no params provided", () => {
|
||||
const url = makeUrl();
|
||||
const result = parseListParams(url, mode);
|
||||
if (result instanceof Response) throw new Error("Should not return Response");
|
||||
expect(result.page).toBe(1);
|
||||
expect(result.pageSize).toBe(20);
|
||||
expect(result.keyword).toBeUndefined();
|
||||
expect(result.sortBy).toBeUndefined();
|
||||
expect(result.sortOrder).toBeUndefined();
|
||||
});
|
||||
|
||||
test("parses valid pagination params", () => {
|
||||
const url = makeUrl({ page: "2", pageSize: "50" });
|
||||
const result = parseListParams(url, mode);
|
||||
if (result instanceof Response) throw new Error("Should not return Response");
|
||||
expect(result.page).toBe(2);
|
||||
expect(result.pageSize).toBe(50);
|
||||
});
|
||||
|
||||
test("parses keyword param", () => {
|
||||
const url = makeUrl({ keyword: "test" });
|
||||
const result = parseListParams(url, mode);
|
||||
if (result instanceof Response) throw new Error("Should not return Response");
|
||||
expect(result.keyword).toBe("test");
|
||||
});
|
||||
|
||||
test("keyword empty string becomes undefined", () => {
|
||||
const url = makeUrl({ keyword: "" });
|
||||
const result = parseListParams(url, mode);
|
||||
if (result instanceof Response) throw new Error("Should not return Response");
|
||||
expect(result.keyword).toBeUndefined();
|
||||
});
|
||||
|
||||
test("parses valid sort params", () => {
|
||||
const url = makeUrl({ sortBy: "name", sortOrder: "asc" });
|
||||
const result = parseListParams(url, mode);
|
||||
if (result instanceof Response) throw new Error("Should not return Response");
|
||||
expect(result.sortBy).toBe("name");
|
||||
expect(result.sortOrder).toBe("asc");
|
||||
});
|
||||
|
||||
test("parses desc sortOrder", () => {
|
||||
const url = makeUrl({ sortBy: "createdAt", sortOrder: "desc" });
|
||||
const result = parseListParams(url, mode);
|
||||
if (result instanceof Response) throw new Error("Should not return Response");
|
||||
expect(result.sortOrder).toBe("desc");
|
||||
});
|
||||
|
||||
test("sortBy without sortOrder returns undefined sortOrder", () => {
|
||||
const url = makeUrl({ sortBy: "name" });
|
||||
const result = parseListParams(url, mode);
|
||||
if (result instanceof Response) throw new Error("Should not return Response");
|
||||
expect(result.sortBy).toBe("name");
|
||||
expect(result.sortOrder).toBeUndefined();
|
||||
});
|
||||
|
||||
test("rejects invalid page", () => {
|
||||
const url = makeUrl({ page: "0" });
|
||||
const result = parseListParams(url, mode);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect((result as Response).status).toBe(400);
|
||||
});
|
||||
|
||||
test("rejects negative page", () => {
|
||||
const url = makeUrl({ page: "-1" });
|
||||
const result = parseListParams(url, mode);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
});
|
||||
|
||||
test("rejects non-integer page", () => {
|
||||
const url = makeUrl({ page: "1.5" });
|
||||
const result = parseListParams(url, mode);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
});
|
||||
|
||||
test("rejects pageSize over 200", () => {
|
||||
const url = makeUrl({ pageSize: "201" });
|
||||
const result = parseListParams(url, mode);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
});
|
||||
|
||||
test("rejects invalid sortOrder", () => {
|
||||
const url = makeUrl({ sortBy: "name", sortOrder: "invalid" });
|
||||
const result = parseListParams(url, mode);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
});
|
||||
|
||||
test("rejects sortBy not in whitelist", () => {
|
||||
const url = makeUrl({ sortBy: "evil" });
|
||||
const result = parseListParams(url, mode, { allowedSortBy: ["name", "createdAt"] });
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
});
|
||||
|
||||
test("allows sortBy in whitelist", () => {
|
||||
const url = makeUrl({ sortBy: "name" });
|
||||
const result = parseListParams(url, mode, { allowedSortBy: ["name", "createdAt"] });
|
||||
if (result instanceof Response) throw new Error("Should not return Response");
|
||||
expect(result.sortBy).toBe("name");
|
||||
});
|
||||
|
||||
test("allows sortBy when no whitelist provided", () => {
|
||||
const url = makeUrl({ sortBy: "anything" });
|
||||
const result = parseListParams(url, mode);
|
||||
if (result instanceof Response) throw new Error("Should not return Response");
|
||||
expect(result.sortBy).toBe("anything");
|
||||
});
|
||||
|
||||
test("parses all params together", () => {
|
||||
const url = makeUrl({ keyword: "hello", page: "3", pageSize: "10", sortBy: "createdAt", sortOrder: "desc" });
|
||||
const result = parseListParams(url, mode, { allowedSortBy: ["createdAt"] });
|
||||
if (result instanceof Response) throw new Error("Should not return Response");
|
||||
expect(result).toEqual({
|
||||
keyword: "hello",
|
||||
page: 3,
|
||||
pageSize: 10,
|
||||
sortBy: "createdAt",
|
||||
sortOrder: "desc",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -130,6 +130,48 @@ describe("models API routes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("GET /api/models sortBy + sortOrder", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const p = seedProvider(db, "SortP");
|
||||
createTestModel(db, "Beta", p);
|
||||
createTestModel(db, "Alpha", p);
|
||||
|
||||
const req = new Request("http://localhost/api/models?page=1&pageSize=20&sortBy=name&sortOrder=asc");
|
||||
const res = await listModelsViaHandler(req, db);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { items: Model[] };
|
||||
expect(body.items[0]!.name).toBe("Alpha");
|
||||
expect(body.items[1]!.name).toBe("Beta");
|
||||
});
|
||||
});
|
||||
|
||||
test("GET /api/models filter by capabilities", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const p = seedProvider(db, "CapP");
|
||||
createModel(db, { capabilities: ["text"], modelId: "text-1", name: "TextModel", providerId: p }, LOG);
|
||||
createModel(
|
||||
db,
|
||||
{ capabilities: ["reasoning"], modelId: "reasoning-1", name: "ReasoningModel", providerId: p },
|
||||
LOG,
|
||||
);
|
||||
|
||||
const req = new Request("http://localhost/api/models?page=1&pageSize=20&capabilities=text");
|
||||
const res = await listModelsViaHandler(req, db);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { items: Model[]; total: number };
|
||||
expect(body.total).toBe(1);
|
||||
expect(body.items[0]!.name).toBe("TextModel");
|
||||
});
|
||||
});
|
||||
|
||||
test("GET /api/models rejects invalid sortBy", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const req = new Request("http://localhost/api/models?page=1&pageSize=20&sortBy=evil");
|
||||
const res = await listModelsViaHandler(req, db);
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
test("GET /api/models filter by providerId", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const p1 = seedProvider(db, "P1");
|
||||
|
||||
@@ -131,6 +131,55 @@ describe("供应商 API 路由", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("GET /api/providers sortBy + sortOrder", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
createTestProvider(db, "Beta");
|
||||
createTestProvider(db, "Alpha");
|
||||
|
||||
const req = new Request("http://localhost/api/providers?page=1&pageSize=20&sortBy=name&sortOrder=asc");
|
||||
const res = await listProvidersViaHandler(req, db);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { items: Provider[] };
|
||||
expect(body.items[0]!.name).toBe("Alpha");
|
||||
expect(body.items[1]!.name).toBe("Beta");
|
||||
});
|
||||
});
|
||||
|
||||
test("GET /api/providers filter by type", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
createTestProvider(db, "OpenAI Provider");
|
||||
const compatResult = createProvider(
|
||||
db,
|
||||
{ apiKey: "sk-test", baseUrl: "https://compat.test.com", name: "Compat", type: "openai-compatible" },
|
||||
LOG,
|
||||
);
|
||||
if ("error" in compatResult) throw new Error(compatResult.error);
|
||||
|
||||
const req = new Request("http://localhost/api/providers?page=1&pageSize=20&type=openai");
|
||||
const res = await listProvidersViaHandler(req, db);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { items: Provider[]; total: number };
|
||||
expect(body.total).toBe(1);
|
||||
expect(body.items[0]!.name).toBe("OpenAI Provider");
|
||||
});
|
||||
});
|
||||
|
||||
test("GET /api/providers rejects invalid sortBy", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const req = new Request("http://localhost/api/providers?page=1&pageSize=20&sortBy=evil");
|
||||
const res = await listProvidersViaHandler(req, db);
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
test("GET /api/providers rejects invalid sortOrder", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
const req = new Request("http://localhost/api/providers?page=1&pageSize=20&sortBy=name&sortOrder=invalid");
|
||||
const res = await listProvidersViaHandler(req, db);
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
test("GET /api/providers/options 返回最小字段", async () => {
|
||||
await withRouteDb(async (db) => {
|
||||
createTestProvider(db, "选项供应商");
|
||||
|
||||
Reference in New Issue
Block a user