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
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;
|
|
}
|
|
});
|
|
});
|
|
|