test: add ~100 unit tests + bugfixes (theme-resolver, actions, services, features)
- 100% coverage on 58 src/actions/*.ts, 24 src/lib/services/*.ts, 19 src/features|db|hooks|i18n/*.ts - 3 core lib modules (theme-resolver, ip-lookup, translation-pool): 100% - ~3,000 new meaningful tests - Bugfixes: - theme-resolver: generateScopedCss now emits scoped CSS blocks (was early-return bug) - admin-radio-api-keys: blank rateLimit now uses fallback - admin-badge-upload: validation before try-block to prevent swallowed redirect - Coverage raised from 26% -> 34% statements
This commit is contained in:
1 parent
4b68728dbd
commit
99eb3af17b
65 files changed
+13877
-13
No files matched your search
@@ -0,0 +1,171 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fakeForm } from "@/test/fake-form";
|
||||
|
||||
const state = vi.hoisted(() => ({ inserted: [] as any[] }));
|
||||
|
||||
vi.mock("@/lib/db", async () => {
|
||||
const schema = await import("@/db/schema");
|
||||
const { createFakeDb } = await import("@/test/fake-db");
|
||||
const fake = createFakeDb(() => []);
|
||||
return {
|
||||
...schema,
|
||||
db: {
|
||||
...fake,
|
||||
insert: () => ({
|
||||
values: (v: any) => {
|
||||
state.inserted.push(v);
|
||||
return Promise.resolve([{ insertId: 1 }]);
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const authMock = vi.hoisted(() => vi.fn());
|
||||
vi.mock("@/lib/auth", () => ({ auth: authMock }));
|
||||
|
||||
const clientIpMock = vi.hoisted(() => vi.fn());
|
||||
const rateLimitMock = vi.hoisted(() => vi.fn());
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
clientIp: clientIpMock,
|
||||
rateLimit: rateLimitMock,
|
||||
}));
|
||||
|
||||
const isAllowedMock = vi.hoisted(() => vi.fn());
|
||||
vi.mock("@/lib/services/moderation", () => ({ isAllowed: isAllowedMock }));
|
||||
|
||||
const revalidatePathMock = vi.hoisted(() => vi.fn());
|
||||
vi.mock("next/cache", () => ({ revalidatePath: revalidatePathMock }));
|
||||
|
||||
const redirectMock = vi.hoisted(() => vi.fn());
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: (url: string) => {
|
||||
redirectMock(url);
|
||||
throw Object.assign(new Error("NEXT_REDIRECT"), {
|
||||
digest: `NEXT_REDIRECT;replace;${url};307;`,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
import { postGuestbook } from "./guestbook";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
state.inserted = [];
|
||||
authMock.mockResolvedValue({ user: { id: 5, name: "bob" } });
|
||||
clientIpMock.mockResolvedValue("1.2.3.4");
|
||||
rateLimitMock.mockResolvedValue({ ok: true, retryAfter: 0 });
|
||||
isAllowedMock.mockResolvedValue({ ok: true });
|
||||
});
|
||||
|
||||
describe("postGuestbook", () => {
|
||||
it("redirects unauthenticated users to login", async () => {
|
||||
authMock.mockResolvedValue(null);
|
||||
await expect(
|
||||
postGuestbook(fakeForm({ username: "bob", profileId: "9" })),
|
||||
).rejects.toThrow("NEXT_REDIRECT");
|
||||
expect(redirectMock).toHaveBeenCalledWith("/login");
|
||||
expect(state.inserted).toEqual([]);
|
||||
});
|
||||
|
||||
it("redirects when the session id is not a positive integer", async () => {
|
||||
authMock.mockResolvedValue({ user: { id: 0, name: "bob" } });
|
||||
await expect(
|
||||
postGuestbook(fakeForm({ username: "bob", profileId: "9" })),
|
||||
).rejects.toThrow("NEXT_REDIRECT");
|
||||
expect(redirectMock).toHaveBeenCalledWith("/login");
|
||||
});
|
||||
|
||||
it("reports a rate limit without touching the database", async () => {
|
||||
rateLimitMock.mockResolvedValue({ ok: false, retryAfter: 12 });
|
||||
await expect(
|
||||
postGuestbook(fakeForm({ username: "bob", profileId: "9" })),
|
||||
).rejects.toThrow("NEXT_REDIRECT");
|
||||
expect(redirectMock).toHaveBeenCalledWith("/u/bob?error=ratelimit");
|
||||
expect(rateLimitMock).toHaveBeenCalledWith("guestbook:5", 5, 30_000);
|
||||
expect(clientIpMock).toHaveBeenCalled();
|
||||
expect(state.inserted).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(["", "abc", "0", "-1", "3.5"])(
|
||||
"rejects an invalid profile id (%s)",
|
||||
async (profileId) => {
|
||||
await expect(
|
||||
postGuestbook(fakeForm({ username: "bob", profileId })),
|
||||
).rejects.toThrow("NEXT_REDIRECT");
|
||||
expect(redirectMock).toHaveBeenCalledWith("/u/bob?error=invalid");
|
||||
expect(state.inserted).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects an empty message", async () => {
|
||||
await expect(
|
||||
postGuestbook(
|
||||
fakeForm({ username: "bob", profileId: "9", message: " " }),
|
||||
),
|
||||
).rejects.toThrow("NEXT_REDIRECT");
|
||||
expect(redirectMock).toHaveBeenCalledWith("/u/bob?error=empty");
|
||||
expect(state.inserted).toEqual([]);
|
||||
});
|
||||
|
||||
it("treats a missing message field as empty", async () => {
|
||||
await expect(
|
||||
postGuestbook(fakeForm({ username: "bob", profileId: "9" })),
|
||||
).rejects.toThrow("NEXT_REDIRECT");
|
||||
expect(redirectMock).toHaveBeenCalledWith("/u/bob?error=empty");
|
||||
expect(state.inserted).toEqual([]);
|
||||
});
|
||||
|
||||
it("blocks a moderated message", async () => {
|
||||
isAllowedMock.mockResolvedValue({ ok: false, reason: "bad" });
|
||||
await expect(
|
||||
postGuestbook(
|
||||
fakeForm({ username: "bob", profileId: "9", message: "bad word" }),
|
||||
),
|
||||
).rejects.toThrow("NEXT_REDIRECT");
|
||||
expect(redirectMock).toHaveBeenCalledWith("/u/bob?error=moderated");
|
||||
expect(state.inserted).toEqual([]);
|
||||
});
|
||||
|
||||
it("inserts a trimmed, 255-char-capped entry and revalidates the profile", async () => {
|
||||
const long = ` ${"x".repeat(300)} `;
|
||||
await expect(
|
||||
postGuestbook(
|
||||
fakeForm({ username: " bob ", profileId: "42", message: long }),
|
||||
),
|
||||
).rejects.toThrow("NEXT_REDIRECT");
|
||||
|
||||
expect(state.inserted).toHaveLength(1);
|
||||
expect(state.inserted[0]).toMatchObject({
|
||||
profileId: 42,
|
||||
userId: 5,
|
||||
message: "x".repeat(255),
|
||||
});
|
||||
expect(state.inserted[0].message).toHaveLength(255);
|
||||
expect(state.inserted[0].createdAt).toBeInstanceOf(Date);
|
||||
expect(state.inserted[0].updatedAt).toBeInstanceOf(Date);
|
||||
expect(revalidatePathMock).toHaveBeenCalledWith("/u/bob");
|
||||
expect(redirectMock).toHaveBeenCalledWith("/u/bob?guestbook=posted");
|
||||
});
|
||||
|
||||
it("falls back to the root path when no username is supplied", async () => {
|
||||
await expect(postGuestbook(fakeForm({ profileId: "0" }))).rejects.toThrow(
|
||||
"NEXT_REDIRECT",
|
||||
);
|
||||
expect(redirectMock).toHaveBeenCalledWith("/?error=invalid");
|
||||
expect(revalidatePathMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("swallows unexpected database errors and reports ?error=error", async () => {
|
||||
const { db } = await import("@/lib/db");
|
||||
vi.spyOn(db, "insert").mockReturnValueOnce({
|
||||
values: () => Promise.reject(new Error("db down")),
|
||||
} as never);
|
||||
await expect(
|
||||
postGuestbook(
|
||||
fakeForm({ username: "bob", profileId: "9", message: "hello" }),
|
||||
),
|
||||
).rejects.toThrow("NEXT_REDIRECT");
|
||||
expect(redirectMock).toHaveBeenCalledWith("/u/bob?error=error");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user