import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"; import { eq, sql } from "drizzle-orm"; process.env.DATABASE_URL ??= "file:./db.sqlite"; (globalThis as { createError?: (input: unknown) => unknown }).createError = (input: unknown) => { if (input instanceof Error) { return input; } const payload = (input ?? {}) as { statusMessage?: string }; const error = new Error(payload.statusMessage ?? "Error") as Error & Record; Object.assign(error, payload); return error; }; const { dbGlobal } = await import("drizzle-pkg/database/sqlite/db-bun"); mock.module("drizzle-pkg/lib/db", () => ({ dbGlobal })); const { users } = await import("drizzle-pkg/lib/schema/auth"); const { quickNotes } = await import("drizzle-pkg/lib/schema/content"); const { QUICK_NOTE_MAX_LENGTH, getQuickNoteByUserId, isQuickNoteUniqueViolation, normalizeQuickNoteContent, upsertQuickNoteByUserId, validateQuickNoteContentLength, } = await import("./index"); const TEST_USER = { id: 920001, username: "quick_note_u1", password: "pw", }; async function resetRows() { await dbGlobal.delete(quickNotes).where(eq(quickNotes.userId, TEST_USER.id)); await dbGlobal.delete(users).where(eq(users.id, TEST_USER.id)); } describe("quick-note service", () => { beforeAll(async () => { await dbGlobal.run(sql` CREATE TABLE IF NOT EXISTS quick_notes ( id INTEGER PRIMARY KEY NOT NULL, user_id INTEGER NOT NULL, content TEXT NOT NULL, created_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL, updated_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL ) `); await dbGlobal.run(sql` CREATE UNIQUE INDEX IF NOT EXISTS quick_notes_user_id_unique ON quick_notes (user_id) `); await resetRows(); }); beforeEach(async () => { await resetRows(); await dbGlobal.insert(users).values(TEST_USER); }); test("exports max length constant", () => { expect(QUICK_NOTE_MAX_LENGTH).toBe(200000); }); test("normalizeQuickNoteContent converts CRLF to LF", () => { expect(normalizeQuickNoteContent("a\r\nb\r\n")).toBe("a\nb\n"); }); test("validateQuickNoteContentLength throws 400 when too long", () => { const tooLong = "a".repeat(QUICK_NOTE_MAX_LENGTH + 1); expect(() => validateQuickNoteContentLength(tooLong)).toThrow("速记内容过长"); try { validateQuickNoteContentLength(tooLong); } catch (error) { expect(error).toMatchObject({ statusCode: 400, statusMessage: "速记内容过长" }); } }); test("validateQuickNoteContentLength allows content at max length", () => { const maxLengthContent = "a".repeat(QUICK_NOTE_MAX_LENGTH); expect(() => validateQuickNoteContentLength(maxLengthContent)).not.toThrow(); }); test("isQuickNoteUniqueViolation returns true for quick_notes user unique conflict", () => { expect( isQuickNoteUniqueViolation(new Error("UNIQUE constraint failed: quick_notes.user_id")), ).toBe(true); }); test("isQuickNoteUniqueViolation returns false for non-quick-note conflict", () => { expect( isQuickNoteUniqueViolation(new Error("UNIQUE constraint failed: posts.user_id, posts.slug")), ).toBe(false); expect(isQuickNoteUniqueViolation(new Error("random error"))).toBe(false); }); test("getQuickNoteByUserId returns null when not found", async () => { const row = await getQuickNoteByUserId(TEST_USER.id); expect(row).toBeNull(); }); test("upsertQuickNoteByUserId inserts then updates and normalizes newlines", async () => { const inserted = await upsertQuickNoteByUserId(TEST_USER.id, "line1\r\nline2"); expect(inserted.userId).toBe(TEST_USER.id); expect(inserted.content).toBe("line1\nline2"); const updated = await upsertQuickNoteByUserId(TEST_USER.id, "next\r\nvalue"); expect(updated.id).toBe(inserted.id); expect(updated.content).toBe("next\nvalue"); const fromDb = await getQuickNoteByUserId(TEST_USER.id); expect(fromDb?.id).toBe(inserted.id); expect(fromDb?.content).toBe("next\nvalue"); }); test("upsertQuickNoteByUserId supports empty string content", async () => { const saved = await upsertQuickNoteByUserId(TEST_USER.id, ""); expect(saved.content).toBe(""); const fromDb = await getQuickNoteByUserId(TEST_USER.id); expect(fromDb).not.toBeNull(); expect(fromDb?.content).toBe(""); }); test("upsertQuickNoteByUserId normalizes all CRLF to LF", async () => { const raw = "\r\nline1\r\nline2\r\n"; const saved = await upsertQuickNoteByUserId(TEST_USER.id, raw); expect(saved.content).toBe("\nline1\nline2\n"); const fromDb = await getQuickNoteByUserId(TEST_USER.id); expect(fromDb?.content).toBe("\nline1\nline2\n"); }); test("upsertQuickNoteByUserId validates length after normalize", async () => { const raw = "a\r\n".repeat(100000); expect(raw.length).toBeGreaterThan(QUICK_NOTE_MAX_LENGTH); expect(normalizeQuickNoteContent(raw).length).toBe(QUICK_NOTE_MAX_LENGTH); const saved = await upsertQuickNoteByUserId(TEST_USER.id, raw); expect(saved.content.length).toBe(QUICK_NOTE_MAX_LENGTH); }); test("upsertQuickNoteByUserId falls back to update on unique conflict and returns row", async () => { const originalInsert = dbGlobal.insert.bind(dbGlobal); let injected = false; (dbGlobal as { insert: typeof dbGlobal.insert }).insert = ((...args: Parameters) => { const builder = originalInsert(...args) as { values: (payload: unknown) => Promise } & Record; return { ...builder, values: async (payload: unknown) => { const result = await builder.values(payload); if (!injected) { injected = true; throw new Error("UNIQUE constraint failed: quick_notes.user_id"); } return result; }, }; }) as typeof dbGlobal.insert; try { const saved = await upsertQuickNoteByUserId(TEST_USER.id, "fallback\r\nok"); expect(saved.userId).toBe(TEST_USER.id); expect(saved.content).toBe("fallback\nok"); expect(injected).toBe(true); } finally { (dbGlobal as { insert: typeof dbGlobal.insert }).insert = originalInsert as typeof dbGlobal.insert; } }); });