Restore the community photo after reload and reject empty, oversized, SVG, and spoofed uploads.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-09-10 15:50:32 -06:00
co-authored by Cursor
parent 6ccc1e8c8e
commit 234f3998ad
18 changed files with 1072 additions and 112 deletions
+56 -3
View File
@@ -27,6 +27,7 @@ function multipartRequest(opts: {
purpose?: string;
fileName?: string;
fileContent?: string;
contentType?: string;
}): NextRequest {
const boundary = "----VitestBoundary";
const parts: string[] = [];
@@ -36,8 +37,9 @@ function multipartRequest(opts: {
);
}
if (opts.fileName && opts.fileContent !== undefined) {
const contentType = opts.contentType ?? "image/png";
parts.push(
`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${opts.fileName}"\r\nContent-Type: image/png\r\n\r\n${opts.fileContent}\r\n`,
`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${opts.fileName}"\r\nContent-Type: ${contentType}\r\n\r\n${opts.fileContent}\r\n`,
);
}
parts.push(`--${boundary}--\r\n`);
@@ -56,6 +58,7 @@ beforeEach(() => {
getUploadRootFromEnvMock.mockReset();
isDatabaseConfiguredMock.mockReturnValue(true);
getUploadRootFromEnvMock.mockReturnValue("/tmp/uploads");
getSessionUserMock.mockResolvedValue({ id: "u1", email: "a@b.c" });
});
describe("POST /api/uploads", () => {
@@ -78,7 +81,6 @@ describe("POST /api/uploads", () => {
});
it("returns 500 when UPLOAD_ROOT is unset", async () => {
getSessionUserMock.mockResolvedValueOnce({ id: "u1", email: "a@b.c" });
getUploadRootFromEnvMock.mockReturnValueOnce(null);
const res = await POST(
new NextRequest("https://x.test/api/uploads", { method: "POST" }),
@@ -90,7 +92,6 @@ describe("POST /api/uploads", () => {
});
it("returns 400 when purpose is missing", async () => {
getSessionUserMock.mockResolvedValueOnce({ id: "u1", email: "a@b.c" });
const res = await POST(
multipartRequest({ fileName: "avatar.png", fileContent: "x" }),
undefined,
@@ -99,4 +100,56 @@ describe("POST /api/uploads", () => {
const body = (await res.json()) as { error: { code: string } };
expect(body.error.code).toBe("validation_error");
});
it("returns 400 for an empty file", async () => {
const res = await POST(
multipartRequest({
purpose: "communityAvatar",
fileName: "empty.png",
fileContent: "",
}),
undefined,
);
expect(res.status).toBe(400);
const body = (await res.json()) as {
error: { code: string };
details?: { reason?: string };
};
expect(body.error.code).toBe("validation_error");
expect(body.details?.reason).toBe("empty");
});
it("returns 400 for SVG even when named .png", async () => {
const res = await POST(
multipartRequest({
purpose: "communityAvatar",
fileName: "photo.png",
fileContent: '<svg xmlns="http://www.w3.org/2000/svg"></svg>',
contentType: "image/png",
}),
undefined,
);
expect(res.status).toBe(400);
const body = (await res.json()) as {
details?: { reason?: string };
};
expect(body.details?.reason).toBe("svg");
});
it("returns 400 for a text file spoofed as PNG", async () => {
const res = await POST(
multipartRequest({
purpose: "communityAvatar",
fileName: "photo.png",
fileContent: "not an image",
contentType: "image/png",
}),
undefined,
);
expect(res.status).toBe(400);
const body = (await res.json()) as {
details?: { reason?: string };
};
expect(body.details?.reason).toBe("invalidType");
});
});