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
+136 -2
View File
@@ -1,9 +1,71 @@
import { describe, it, expect } from "vitest";
import { renderWithProviders as render, screen } from "../utils/test-utils";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
renderWithProviders as render,
screen,
fireEvent,
cleanup,
} from "../utils/test-utils";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom/vitest";
import { CommunityUploadScreen } from "../../app/(app)/create/screens/upload/CommunityUploadScreen";
import {
clearPendingCommunityAvatarFile,
readPendingCommunityAvatarFile,
storePendingCommunityAvatarFile,
} from "../../lib/create/pendingCommunityAvatarUpload";
import { fetchAuthSession } from "../../lib/create/api";
import messages from "../../messages/en/index";
vi.mock("../../lib/create/pendingCommunityAvatarUpload", () => ({
readPendingCommunityAvatarFile: vi.fn().mockResolvedValue(null),
storePendingCommunityAvatarFile: vi.fn().mockResolvedValue(undefined),
clearPendingCommunityAvatarFile: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../../lib/create/api", () => ({
fetchAuthSession: vi.fn().mockResolvedValue({ user: null }),
}));
const pngBytes = Uint8Array.from(
Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
"base64",
),
);
const copy = messages.create.community.communityUpload;
const uploadErrors = messages.create.upload.errors;
function fileInput(): HTMLInputElement {
const input = document.querySelector('input[type="file"]');
expect(input).toBeInstanceOf(HTMLInputElement);
return input as HTMLInputElement;
}
describe("CommunityUploadScreen", () => {
beforeEach(() => {
vi.mocked(readPendingCommunityAvatarFile).mockResolvedValue(null);
vi.mocked(storePendingCommunityAvatarFile).mockResolvedValue(undefined);
vi.mocked(clearPendingCommunityAvatarFile).mockResolvedValue(undefined);
vi.mocked(fetchAuthSession).mockResolvedValue({ user: null });
Object.defineProperty(URL, "createObjectURL", {
value: vi.fn(() => "blob:pending-avatar"),
writable: true,
configurable: true,
});
Object.defineProperty(URL, "revokeObjectURL", {
value: vi.fn(),
writable: true,
configurable: true,
});
});
afterEach(() => {
cleanup();
Reflect.deleteProperty(URL, "createObjectURL");
Reflect.deleteProperty(URL, "revokeObjectURL");
});
it("renders HeaderLockup", () => {
render(<CommunityUploadScreen />);
expect(
@@ -22,4 +84,76 @@ describe("CommunityUploadScreen", () => {
),
).toBeInTheDocument();
});
it("restores a pending IndexedDB photo and lets the user remove it", async () => {
const user = userEvent.setup();
const pending = new File([pngBytes], "avatar.png", { type: "image/png" });
vi.mocked(readPendingCommunityAvatarFile).mockResolvedValue(pending);
render(<CommunityUploadScreen />);
const preview = await screen.findByRole("img", { name: copy.previewAlt });
expect(preview).toHaveAttribute("src", "blob:pending-avatar");
const remove = screen.getByRole("button", {
name: copy.clearPendingUploadAriaLabel,
});
await user.click(remove);
expect(clearPendingCommunityAvatarFile).toHaveBeenCalled();
expect(screen.getByRole("button", { name: "Upload" })).toBeInTheDocument();
expect(
screen.queryByRole("img", { name: copy.previewAlt }),
).not.toBeInTheDocument();
});
it("rejects an empty file with a clear error and does not stage it", async () => {
render(<CommunityUploadScreen />);
await screen.findByText(copy.signInToUploadNote);
fireEvent.change(fileInput(), {
target: { files: [new File([], "empty.png", { type: "image/png" })] },
});
expect(await screen.findByRole("alert")).toHaveTextContent(
uploadErrors.empty,
);
expect(storePendingCommunityAvatarFile).not.toHaveBeenCalled();
});
it("rejects a text file renamed as PNG", async () => {
render(<CommunityUploadScreen />);
await screen.findByText(copy.signInToUploadNote);
fireEvent.change(fileInput(), {
target: {
files: [new File(["hello"], "photo.png", { type: "image/png" })],
},
});
expect(await screen.findByRole("alert")).toHaveTextContent(
uploadErrors.invalidType,
);
expect(storePendingCommunityAvatarFile).not.toHaveBeenCalled();
});
it("rejects SVG uploads", async () => {
render(<CommunityUploadScreen />);
await screen.findByText(copy.signInToUploadNote);
fireEvent.change(fileInput(), {
target: {
files: [
new File(
['<svg xmlns="http://www.w3.org/2000/svg"></svg>'],
"photo.png",
{ type: "image/png" },
),
],
},
});
expect(await screen.findByRole("alert")).toHaveTextContent(uploadErrors.svg);
expect(storePendingCommunityAvatarFile).not.toHaveBeenCalled();
});
});