You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

172 lines
6.2 KiB

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<string, unknown>;
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<typeof dbGlobal.insert>) => {
const builder = originalInsert(...args) as { values: (payload: unknown) => Promise<unknown> } & Record<string, unknown>;
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;
}
});
});