返回 DeepSeek-Reasonix
index.test.ts
根目录 / workers / forum / src / index.test.ts
1 import { describe, expect, it } from "vitest";
2 import app from "./index";
3 import type { Bindings } from "./env";
4
5 function bindings(db: D1Database): Bindings {
6 return {
7 DB: db,
8 APP_ORIGIN: "https://reasonix.io",
9 ALLOWED_ORIGINS: "https://reasonix.io",
10 ID_ORIGIN: "https://id.reasonix.io",
11 };
12 }
13
14 describe("forum public API", () => {
15 it("maps invalid query input to a client error", async () => {
16 const db = { prepare: () => { throw new Error("database should not be reached"); } } as unknown as D1Database;
17 const response = await app.request("https://forum.reasonix.io/topics?sort=invalid", {}, bindings(db));
18 expect(response.status).toBe(422);
19 await expect(response.json()).resolves.toMatchObject({ error: { code: "invalid_input" } });
20 });
21
22 it("selects public handles instead of stored author emails", async () => {
23 const queries: string[] = [];
24 const rows = [{
25 id: 1,
26 title: "Safe public topic",
27 slug: "safe-public-topic",
28 status: "open",
29 pinned: 0,
30 replyCount: 0,
31 viewCount: 0,
32 author: "alice",
33 createdAt: "2026-08-05T00:00:00.000Z",
34 lastPostAt: "2026-08-05T00:00:00.000Z",
35 category: "help",
36 categoryName: "Help & Support",
37 }];
38 const statement = {
39 bind() { return this; },
40 async all() { return { results: rows }; },
41 };
42 const db = {
43 prepare(query: string) {
44 queries.push(query);
45 return statement;
46 },
47 } as unknown as D1Database;
48
49 const response = await app.request("https://forum.reasonix.io/topics", {}, bindings(db));
50 expect(response.status).toBe(200);
51 const payload = await response.json();
52 expect(payload).toEqual({ topics: rows });
53 expect(queries[0]).toContain("m.handle, 'deleted') AS author");
54 expect(queries[0]).not.toMatch(/\bt\.author\s*,/);
55 expect(JSON.stringify(payload)).not.toContain("example.test");
56 });
57
58 it("paginates topic posts with a stable created-at and id cursor", async () => {
59 const queries: string[] = [];
60 const topic = { id: 7, title: "A topic", status: "open", pinned: 0, acceptedPostId: null, replyCount: 2, viewCount: 0, createdAt: "2026-08-05T00:00:00.000Z", category: "help" };
61 const posts = [
62 { id: 2, author: "alice", handle: "alice", body: "first", status: "visible", likeCount: 0, createdAt: "2026-08-05T00:01:00.000Z", editedAt: null, trust: 0, role: "member", liked: 0 },
63 { id: 3, author: "bob", handle: "bob", body: "second", status: "visible", likeCount: 0, createdAt: "2026-08-05T00:02:00.000Z", editedAt: null, trust: 0, role: "member", liked: 0 },
64 ];
65 const db = {
66 prepare(query: string) {
67 queries.push(query);
68 const statement = {
69 bind() { return statement; },
70 async first<T>() { return query.includes("FROM topics") ? topic as T : null; },
71 async run() { return { meta: { changes: 1 } }; },
72 async all<T>() { return { results: (query.includes("FROM posts") ? posts : []) as T[] }; },
73 };
74 return statement;
75 },
76 } as unknown as D1Database;
77
78 const response = await app.request(
79 "https://forum.reasonix.io/topics/7?limit=1&after=2026-08-05T00%3A00%3A00.000Z&afterId=1",
80 {},
81 bindings(db),
82 );
83 expect(response.status).toBe(200);
84 const payload = await response.json() as { posts: typeof posts; pageInfo: Record<string, unknown> };
85 expect(payload.posts).toHaveLength(1);
86 expect(payload.posts[0].id).toBe(2);
87 expect(payload.pageInfo).toMatchObject({ limit: 1, hasMore: true, nextAfter: posts[0].createdAt, nextAfterId: 2 });
88 expect(queries.find((sql) => sql.includes("FROM posts"))).toContain("LIMIT ?5");
89 expect(queries.find((sql) => sql.includes("FROM posts"))).toContain("p.created_at > ?3");
90 });
91 });
92
92 lines TYPESCRIPT