fix(housekeeping): address task 14 review round 1

This commit is contained in:
2026-08-30 10:53:50 +02:00
parent fd68819d9b
commit d09eaa33d6
23 changed files with 2503 additions and 488 deletions
@@ -10,23 +10,33 @@ const fsMocks = vi.hoisted(() => ({
writeFile: vi.fn(),
}));
const dbMocks = vi.hoisted(() => {
const onDuplicateKeyUpdate = vi.fn(async () => undefined);
const values = vi.fn(() => ({ onDuplicateKeyUpdate }));
const insert = vi.fn(() => ({ values }));
const where = vi.fn(async () => undefined);
const deleteFn = vi.fn(() => ({ where }));
return { deleteFn, insert, onDuplicateKeyUpdate, values, where };
});
const siteMocks = vi.hoisted(() => ({
get: vi.fn(),
reload: vi.fn(),
update: vi.fn(),
}));
vi.mock("node:fs/promises", () => fsMocks);
vi.mock("drizzle-orm", () => ({ eq: vi.fn() }));
vi.mock("@/lib/db", () => ({
CameraWeb: {},
EmulatorSettings: {},
WebsiteSetting: {},
db: {},
db: { delete: dbMocks.deleteFn, insert: dbMocks.insert },
}));
vi.mock("@/lib/services/rcon", () => ({
rcon: { updateConfig: vi.fn() },
}));
vi.mock("@/lib/services/site-settings", () => ({
siteSettings: {
get: vi.fn(),
reload: vi.fn(),
update: vi.fn(),
},
siteSettings: siteMocks,
}));
vi.mock("@/lib/services/staff-activity", () => ({
logStaffActivity: vi.fn(),
@@ -51,10 +61,15 @@ const context = {
describe("Content external mutation runtime", () => {
beforeEach(() => {
vi.clearAllMocks();
siteMocks.get.mockResolvedValue(undefined);
});
it("preserves media upload bytes, type, size, and canonical public URL", async () => {
const file = new File(["image"], "photo.PNG", { type: "image/png" });
const file = new File(
[new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])],
"photo.png",
{ type: "image/png" },
);
const snapshot = await executeContentExternalMutation(
"media.upload",
{ file },
@@ -75,7 +90,11 @@ describe("Content external mutation runtime", () => {
it("propagates a real storage failure before optimistic success", async () => {
fsMocks.writeFile.mockRejectedValueOnce(new Error("disk offline"));
const file = new File(["image"], "photo.png", { type: "image/png" });
const file = new File(
[new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])],
"photo.png",
{ type: "image/png" },
);
await expect(
executeContentExternalMutation("media.upload", { file }, context),
@@ -95,4 +114,103 @@ describe("Content external mutation runtime", () => {
} satisfies Partial<ContentMutationFailure>);
expect(fsMocks.unlink).not.toHaveBeenCalled();
});
it("rejects declared MIME, filename extension, and actual bytes that disagree", async () => {
const disguisedSvg = new File(
['<svg xmlns="http://www.w3.org/2000/svg"></svg>'],
"photo.svg",
{ type: "image/png" },
);
await expect(
executeContentExternalMutation(
"media.upload",
{ file: disguisedSvg },
context,
),
).rejects.toMatchObject({ code: "VALIDATION" });
expect(fsMocks.writeFile).not.toHaveBeenCalled();
});
it("rejects an oversized logo before reading its bytes", async () => {
const arrayBuffer = vi.fn();
const file = {
name: "logo.png",
type: "image/png",
size: 5 * 1024 * 1024 + 1,
arrayBuffer,
};
await expect(
executeContentExternalMutation("logo.save", { file }, context),
).rejects.toMatchObject({ code: "VALIDATION" });
expect(arrayBuffer).not.toHaveBeenCalled();
expect(fsMocks.writeFile).not.toHaveBeenCalled();
});
it("removes a newly written favicon when the database write fails", async () => {
dbMocks.onDuplicateKeyUpdate.mockRejectedValueOnce(
new Error("database offline"),
);
const file = new File(
[new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])],
"favicon.png",
{ type: "image/png" },
);
await expect(
executeContentExternalMutation("favicon.save", { file }, context),
).rejects.toThrow("database offline");
const writtenPath = fsMocks.writeFile.mock.calls[0]?.[0];
expect(fsMocks.unlink).toHaveBeenCalledWith(writtenPath);
});
it("reports a partial favicon result when database failure compensation also fails", async () => {
dbMocks.onDuplicateKeyUpdate.mockRejectedValueOnce(
new Error("database offline"),
);
fsMocks.unlink.mockRejectedValueOnce(new Error("cleanup failed"));
const file = new File(
[new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])],
"favicon.png",
{ type: "image/png" },
);
await expect(
executeContentExternalMutation("favicon.save", { file }, context),
).rejects.toMatchObject({
name: "ContentCommittedExternalFailure",
snapshot: {
before: { value: null },
after: { value: null },
output: { compensation: "failed" },
},
});
});
it("removes a newly written logo when the database write fails", async () => {
dbMocks.onDuplicateKeyUpdate.mockRejectedValueOnce(
new Error("database offline"),
);
const file = new File(
[new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])],
"logo.png",
{ type: "image/png" },
);
await expect(
executeContentExternalMutation("logo.save", { file }, context),
).rejects.toThrow("database offline");
expect(fsMocks.unlink).toHaveBeenCalledWith(
fsMocks.writeFile.mock.calls[0]?.[0],
);
});
it("does not delete the existing favicon before the database delete commits", async () => {
siteMocks.get.mockResolvedValueOnce("/api/media/favicon/old.png");
dbMocks.where.mockRejectedValueOnce(new Error("database offline"));
await expect(
executeContentExternalMutation("favicon.delete", {}, context),
).rejects.toThrow("database offline");
expect(fsMocks.unlink).not.toHaveBeenCalled();
});
});