test: add unit tests for pure logic modules
Add 22 test files covering imager, soundtracks, browser-headers, source-keys (figure/pet/effect), effect-source, plus catalog-translations, catalog-layouts, client-translation-files, translations-utils, and various admin/services/helpers modules. Total test count increases by 120+.
This commit is contained in:
1 parent
11e8b06bf8
commit
82e8448f26
25 files changed
+1400
-304
No files matched your search
@@ -2,58 +2,69 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
decideAuthorization,
|
||||
isDynamicSuperAdmin,
|
||||
} from "@/lib/admin/authorization-policy";
|
||||
} from "./authorization-policy";
|
||||
|
||||
describe("isDynamicSuperAdmin", () => {
|
||||
it.each([
|
||||
[7, 7],
|
||||
[11, 11],
|
||||
[2000, 2000],
|
||||
])("accepts highest rank %i", (rank, highest) => {
|
||||
expect(isDynamicSuperAdmin(rank, highest)).toBe(true);
|
||||
it("returns true when rank equals the highest rank", () => {
|
||||
expect(isDynamicSuperAdmin(7, 7)).toBe(true);
|
||||
});
|
||||
it("returns false for non-highest ranks", () => {
|
||||
expect(isDynamicSuperAdmin(6, 7)).toBe(false);
|
||||
});
|
||||
it("returns false when highestRank is null or invalid", () => {
|
||||
expect(isDynamicSuperAdmin(7, null)).toBe(false);
|
||||
expect(isDynamicSuperAdmin(0, 7)).toBe(false);
|
||||
expect(isDynamicSuperAdmin(-1, 7)).toBe(false);
|
||||
expect(isDynamicSuperAdmin(1.5, 1.5)).toBe(false);
|
||||
});
|
||||
it("demotes the previous highest rank", () =>
|
||||
expect(isDynamicSuperAdmin(2000, 2001)).toBe(false));
|
||||
it("fails closed without ranks", () =>
|
||||
expect(isDynamicSuperAdmin(1, null)).toBe(false));
|
||||
});
|
||||
|
||||
describe("decideAuthorization", () => {
|
||||
const actor = { id: 1, username: "admin", rank: 11 };
|
||||
it("allows the dynamically highest rank", () =>
|
||||
expect(
|
||||
decideAuthorization({
|
||||
actor,
|
||||
highestRank: 11,
|
||||
permission: "admin.any",
|
||||
hasPermission: false,
|
||||
}).allowed,
|
||||
).toBe(true));
|
||||
it("allows explicit ACL permission below highest", () =>
|
||||
expect(
|
||||
decideAuthorization({
|
||||
actor,
|
||||
highestRank: 12,
|
||||
permission: "admin.news.view",
|
||||
hasPermission: true,
|
||||
}).allowed,
|
||||
).toBe(true));
|
||||
it("denies invalid ranks", () =>
|
||||
const actor = { id: 1, username: "staff", rank: 5 };
|
||||
const base = { actor, highestRank: 7, hasPermission: false };
|
||||
|
||||
it("allows super admins regardless of permission", () => {
|
||||
const decision = decideAuthorization({
|
||||
...base,
|
||||
actor: { ...actor, rank: 7 },
|
||||
hasPermission: false,
|
||||
});
|
||||
expect(decision).toEqual({ allowed: true, superAdmin: true });
|
||||
});
|
||||
|
||||
it("denies invalid ranks", () => {
|
||||
expect(
|
||||
decideAuthorization({
|
||||
...base,
|
||||
actor: { ...actor, rank: 0 },
|
||||
highestRank: 11,
|
||||
permission: "admin.any",
|
||||
hasPermission: true,
|
||||
}),
|
||||
).toMatchObject({ allowed: false, reason: "invalid_rank" }));
|
||||
it("denies missing permission", () =>
|
||||
expect(
|
||||
decideAuthorization({
|
||||
actor,
|
||||
highestRank: 12,
|
||||
permission: "admin.any",
|
||||
hasPermission: false,
|
||||
}),
|
||||
).toMatchObject({ allowed: false, reason: "permission_denied" }));
|
||||
).toEqual({ allowed: false, reason: "invalid_rank" });
|
||||
});
|
||||
|
||||
it("denies when there are no ranks configured", () => {
|
||||
expect(decideAuthorization({ ...base, highestRank: null })).toEqual({
|
||||
allowed: false,
|
||||
reason: "no_ranks",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows when permission is granted", () => {
|
||||
expect(decideAuthorization({ ...base, hasPermission: true })).toEqual({
|
||||
allowed: true,
|
||||
superAdmin: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("allows when no permission is required", () => {
|
||||
const decision = decideAuthorization({ ...base, permission: undefined });
|
||||
expect(decision).toEqual({ allowed: true, superAdmin: false });
|
||||
});
|
||||
|
||||
it("denies when permission is missing", () => {
|
||||
const decision = decideAuthorization({
|
||||
...base,
|
||||
permission: "users.manage",
|
||||
});
|
||||
expect(decision).toEqual({ allowed: false, reason: "permission_denied" });
|
||||
});
|
||||
});
|
||||
@@ -1,22 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildStaffActivityWhere } from "@/lib/admin/log-filters";
|
||||
import { buildStaffActivityWhere } from "./log-filters";
|
||||
|
||||
describe("buildStaffActivityWhere", () => {
|
||||
it("filters authorization events by prefix", () => {
|
||||
it("returns an empty where when no filters are set", () => {
|
||||
expect(buildStaffActivityWhere({})).toEqual({});
|
||||
});
|
||||
|
||||
it("builds an OR search across action, description and IP", () => {
|
||||
const where = buildStaffActivityWhere({ q: "ban " });
|
||||
expect(where.OR).toEqual([
|
||||
{ action: { contains: "ban" } },
|
||||
{ description: { contains: "ban" } },
|
||||
{ ipAddress: { contains: "ban" } },
|
||||
]);
|
||||
expect(where.OR?.[0]).toEqual({ action: { contains: "ban" } });
|
||||
});
|
||||
|
||||
it("sets userId when staffId is provided", () => {
|
||||
const where = buildStaffActivityWhere({ staffId: 42 });
|
||||
expect(where.userId).toBe(BigInt(42));
|
||||
});
|
||||
|
||||
it("uses a prefix match for authorization-only", () => {
|
||||
expect(buildStaffActivityWhere({ authorizationOnly: true })).toEqual({
|
||||
action: { startsWith: "permission." },
|
||||
});
|
||||
});
|
||||
it("combines staff and search filters", () => {
|
||||
const result = buildStaffActivityWhere({
|
||||
q: "rank",
|
||||
staffId: 11,
|
||||
authorizationOnly: true,
|
||||
|
||||
it("uses a contains match for a specific action", () => {
|
||||
expect(buildStaffActivityWhere({ action: "ban" })).toEqual({
|
||||
action: { contains: "ban" },
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
userId: 11n,
|
||||
action: { startsWith: "permission." },
|
||||
});
|
||||
expect(result.OR).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("ignores whitespace-only search terms", () => {
|
||||
expect(buildStaffActivityWhere({ q: " " })).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -1,24 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { adminMutationNotice } from "@/lib/admin/notice";
|
||||
import { adminMutationNotice } from "./notice";
|
||||
|
||||
describe("adminMutationNotice", () => {
|
||||
it("maps a successful redirect to a safe notice", () => {
|
||||
it("returns a danger notice when there is an error", () => {
|
||||
expect(adminMutationNotice({ error: "boom" })).toEqual({
|
||||
tone: "danger",
|
||||
label: "Error",
|
||||
message: "The operation could not be completed.",
|
||||
});
|
||||
});
|
||||
it("returns a success notice when saved is 1", () => {
|
||||
expect(adminMutationNotice({ saved: "1" })).toEqual({
|
||||
tone: "ok",
|
||||
label: "Saved",
|
||||
message: "Changes were saved successfully.",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps an error code without reflecting arbitrary query text", () => {
|
||||
expect(adminMutationNotice({ error: "<script>" })).toEqual({
|
||||
tone: "danger",
|
||||
label: "Error",
|
||||
message: "The operation could not be completed.",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null when there is no mutation result", () => {
|
||||
it("returns null when nothing is set", () => {
|
||||
expect(adminMutationNotice({})).toBeNull();
|
||||
});
|
||||
it("prioritises errors over saved flags", () => {
|
||||
const notice = adminMutationNotice({ saved: "1", error: "x" });
|
||||
expect(notice?.tone).toBe("danger");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { tryRemoveLocalPhotoFile } from "./photo-files";
|
||||
|
||||
describe("tryRemoveLocalPhotoFile", () => {
|
||||
it("returns false for empty or nullish urls", async () => {
|
||||
expect(await tryRemoveLocalPhotoFile("")).toBe(false);
|
||||
expect(await tryRemoveLocalPhotoFile(" ")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for external urls not on this app origin", async () => {
|
||||
expect(
|
||||
await tryRemoveLocalPhotoFile("https://evil.example.com/photos/x.png"),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for urls without a leading slash", async () => {
|
||||
expect(await tryRemoveLocalPhotoFile("photos/x.png")).toBe(false);
|
||||
});
|
||||
|
||||
it("blocks path traversal", async () => {
|
||||
expect(await tryRemoveLocalPhotoFile("/../../etc/passwd")).toBe(false);
|
||||
expect(await tryRemoveLocalPhotoFile("/photos/..%2f..%2fetc/passwd")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false when the target file does not exist", async () => {
|
||||
expect(
|
||||
await tryRemoveLocalPhotoFile("/photos/definitely-missing.png"),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -5,42 +5,66 @@ import {
|
||||
} from "./recent-furni-resync";
|
||||
|
||||
describe("requestRecentFurniResync", () => {
|
||||
it("posts to the fixed seven-day resync endpoint and normalizes the result", async () => {
|
||||
const fetcher = vi.fn(async () =>
|
||||
Response.json({
|
||||
ok: true,
|
||||
mode: "days",
|
||||
days: 7,
|
||||
examined: 4,
|
||||
resynced: 3,
|
||||
it("POSTs to the resync endpoint and maps the payload", async () => {
|
||||
const fetcher = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
examined: 10,
|
||||
resynced: 4,
|
||||
failed: 1,
|
||||
rconOk: false,
|
||||
errors: [{ classname: "chair", message: "invalid entry" }],
|
||||
rconOk: true,
|
||||
errors: [{ classname: "sofa", message: "boom" }],
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(requestRecentFurniResync(fetcher)).resolves.toEqual({
|
||||
examined: 4,
|
||||
resynced: 3,
|
||||
failed: 1,
|
||||
rconOk: false,
|
||||
errors: [{ classname: "chair", message: "invalid entry" }],
|
||||
});
|
||||
expect(RECENT_FURNI_RESYNC_URL).toBe(
|
||||
"/api/admin/import/furni/resync?days=7",
|
||||
);
|
||||
const result = await requestRecentFurniResync(fetcher);
|
||||
expect(fetcher).toHaveBeenCalledWith(RECENT_FURNI_RESYNC_URL, {
|
||||
method: "POST",
|
||||
});
|
||||
expect(result).toEqual({
|
||||
examined: 10,
|
||||
resynced: 4,
|
||||
failed: 1,
|
||||
rconOk: true,
|
||||
errors: [{ classname: "sofa", message: "boom" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("throws the API error when the request fails", async () => {
|
||||
const fetcher = vi.fn(async () =>
|
||||
Response.json({ error: "Forbidden" }, { status: 403 }),
|
||||
);
|
||||
|
||||
it("throws with the server error message on failure", async () => {
|
||||
const fetcher = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
json: async () => ({ error: "no permission" }),
|
||||
});
|
||||
await expect(requestRecentFurniResync(fetcher)).rejects.toThrow(
|
||||
"Forbidden",
|
||||
"no permission",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to a generic error message", async () => {
|
||||
const fetcher = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
json: async () => ({}),
|
||||
});
|
||||
await expect(requestRecentFurniResync(fetcher)).rejects.toThrow(
|
||||
"Furni resync failed",
|
||||
);
|
||||
});
|
||||
|
||||
it("filters malformed error entries and coerces counts", async () => {
|
||||
const fetcher = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
examined: "10",
|
||||
resynced: undefined,
|
||||
failed: null,
|
||||
rconOk: "yes",
|
||||
errors: [{ classname: "a", message: "b" }, { nope: 1 }, "x"],
|
||||
}),
|
||||
});
|
||||
const result = await requestRecentFurniResync(fetcher);
|
||||
expect(result.examined).toBe(10);
|
||||
expect(result.resynced).toBe(0);
|
||||
expect(result.failed).toBe(0);
|
||||
expect(result.rconOk).toBe(false);
|
||||
expect(result.errors).toEqual([{ classname: "a", message: "b" }]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user