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,178 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
existing: [{ id: 1 }] as any[],
|
||||
set: null as any,
|
||||
updateError: null as Error | null,
|
||||
inserted: [] as any[],
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/db", async () => {
|
||||
const schema = await import("@/db/schema");
|
||||
const { createFakeDb } = await import("@/test/fake-db");
|
||||
const fake = createFakeDb(() => state.existing);
|
||||
return {
|
||||
...schema,
|
||||
db: {
|
||||
...fake,
|
||||
update: () => ({
|
||||
set: (v: any) => {
|
||||
state.set = v;
|
||||
return {
|
||||
where: () =>
|
||||
state.updateError
|
||||
? Promise.reject(state.updateError)
|
||||
: Promise.resolve([{ affectedRows: 1 }]),
|
||||
};
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const perm = vi.hoisted(() => ({ getCtx: vi.fn(), canAccess: vi.fn() }));
|
||||
vi.mock("@/lib/permissions", async () => ({
|
||||
...(await import("@/lib/permission-slugs")),
|
||||
getApiAdminContext: perm.getCtx,
|
||||
canAccess: perm.canAccess,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({ auth: vi.fn() }));
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
rateLimit: vi.fn().mockResolvedValue({ ok: true, retryAfter: 0 }),
|
||||
clientIp: vi.fn().mockResolvedValue("127.0.0.1"),
|
||||
}));
|
||||
|
||||
const rconMock = vi.hoisted(() => ({ updateCatalog: vi.fn() }));
|
||||
vi.mock("@/lib/services/rcon", () => ({ rcon: rconMock }));
|
||||
|
||||
const logStaffActivityMock = vi.hoisted(() => vi.fn());
|
||||
vi.mock("@/lib/services/staff-activity", () => ({
|
||||
logStaffActivity: logStaffActivityMock,
|
||||
}));
|
||||
|
||||
const revalidatePathMock = vi.hoisted(() => vi.fn());
|
||||
vi.mock("next/cache", () => ({ revalidatePath: revalidatePathMock }));
|
||||
|
||||
import { updateItemsBase } from "./items-base";
|
||||
|
||||
const ctx = {
|
||||
session: { user: { id: 7, username: "admin", rank: 7, name: "admin" } },
|
||||
permissions: { has: () => true },
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
state.existing = [{ id: 1 }];
|
||||
state.set = null;
|
||||
state.updateError = null;
|
||||
perm.getCtx.mockResolvedValue(ctx);
|
||||
perm.canAccess.mockReturnValue(true);
|
||||
rconMock.updateCatalog.mockResolvedValue(true);
|
||||
logStaffActivityMock.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
describe("updateItemsBase", () => {
|
||||
it("returns Unauthorized when no admin context exists", async () => {
|
||||
perm.getCtx.mockResolvedValue(null);
|
||||
const res = await updateItemsBase({ id: 1, fields: { publicName: "x" } });
|
||||
expect(res).toEqual({ ok: false, error: "Unauthorized" });
|
||||
expect(state.set).toBeNull();
|
||||
});
|
||||
|
||||
it("returns Unauthorized when the permission check denies access", async () => {
|
||||
perm.canAccess.mockReturnValue(false);
|
||||
const res = await updateItemsBase({ id: 1, fields: { publicName: "x" } });
|
||||
expect(res).toEqual({ ok: false, error: "Unauthorized" });
|
||||
expect(state.set).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects requests with no whitelisted fields", async () => {
|
||||
const res = await updateItemsBase({
|
||||
id: 1,
|
||||
fields: { notAllowed: 1, other: "x" },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) expect(res.error).toBe("No valid fields to update");
|
||||
expect(state.set).toBeNull();
|
||||
});
|
||||
|
||||
it("reports a missing item", async () => {
|
||||
state.existing = [];
|
||||
const res = await updateItemsBase({ id: 3, fields: { publicName: "x" } });
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) expect(res.error).toBe("Item not found");
|
||||
expect(state.set).toBeNull();
|
||||
});
|
||||
|
||||
it("validates the input schema before running the handler", async () => {
|
||||
const res = await updateItemsBase({ id: 0, fields: { publicName: "x" } });
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) expect(res.error).toBe("Validation failed");
|
||||
expect(state.set).toBeNull();
|
||||
});
|
||||
|
||||
it("persists only allowed fields and coerces numeric values", async () => {
|
||||
const res = await updateItemsBase({
|
||||
id: 1,
|
||||
fields: {
|
||||
publicName: "Chair",
|
||||
width: "2",
|
||||
length: "3",
|
||||
stackHeight: "1.5",
|
||||
spriteId: "44",
|
||||
allowStack: "1",
|
||||
allowSit: "0",
|
||||
interactionModesCount: "4",
|
||||
effectIdMale: "9",
|
||||
customparams: "{}",
|
||||
notAllowed: "nope",
|
||||
itemName: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res).toEqual({ ok: true, data: { id: 1 } });
|
||||
expect(state.set).toMatchObject({
|
||||
publicName: "Chair",
|
||||
width: 2,
|
||||
length: 3,
|
||||
stackHeight: 1.5,
|
||||
spriteId: 44,
|
||||
allowStack: 1,
|
||||
allowSit: 0,
|
||||
interactionModesCount: 4,
|
||||
effectIdMale: 9,
|
||||
customparams: "{}",
|
||||
});
|
||||
expect(state.set).not.toHaveProperty("notAllowed");
|
||||
expect(state.set).not.toHaveProperty("itemName");
|
||||
expect(rconMock.updateCatalog).toHaveBeenCalled();
|
||||
expect(logStaffActivityMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
staffId: 7,
|
||||
action: "items_base_update",
|
||||
targetType: "items_base",
|
||||
targetId: 1,
|
||||
}),
|
||||
);
|
||||
expect(revalidatePathMock).toHaveBeenCalledWith("/admin/items");
|
||||
expect(revalidatePathMock).toHaveBeenCalledWith("/admin/items/1");
|
||||
expect(revalidatePathMock).toHaveBeenCalledWith("/admin/catalog");
|
||||
});
|
||||
|
||||
it("continues when the RCON catalog refresh fails", async () => {
|
||||
rconMock.updateCatalog.mockRejectedValue(new Error("rcon down"));
|
||||
const res = await updateItemsBase({
|
||||
id: 1,
|
||||
fields: { interactionType: "gate" },
|
||||
});
|
||||
expect(res).toEqual({ ok: true, data: { id: 1 } });
|
||||
expect(logStaffActivityMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("maps a database failure to an internal error result", async () => {
|
||||
state.updateError = new Error("boom");
|
||||
const res = await updateItemsBase({ id: 1, fields: { type: "s" } });
|
||||
expect(res).toEqual({ ok: false, error: "Internal server error" });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user