fix(housekeeping): address task 14 review round 1
This commit is contained in:
+126
-8
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user