Keep a guest community logo on review and on the published rule.
Sign-in was clearing the staged file before it uploaded, and review only read the server URL, so a chosen logo disappeared after the upload step.
This commit is contained in:
1 parent
2348a9e94f
commit
8212423ec3
19 files changed
+780
-61
No files matched your search
@@ -0,0 +1,61 @@
|
||||
import { act } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { CreateFlowPendingAvatarFlush } from "../../app/(app)/create/components/CreateFlowPendingAvatarFlush";
|
||||
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
||||
import { flushPendingCommunityAvatar } from "../../lib/create/flushPendingCommunityAvatar";
|
||||
import {
|
||||
renderWithProviders as render,
|
||||
screen,
|
||||
} from "../utils/test-utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
vi.mock("../../lib/create/flushPendingCommunityAvatar", () => ({
|
||||
flushPendingCommunityAvatar: vi.fn(),
|
||||
}));
|
||||
|
||||
function AvatarProbe() {
|
||||
const { state } = useCreateFlow();
|
||||
return <div data-testid="avatar">{state.communityAvatarUrl ?? ""}</div>;
|
||||
}
|
||||
|
||||
describe("CreateFlowPendingAvatarFlush", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.mocked(flushPendingCommunityAvatar).mockReset();
|
||||
});
|
||||
|
||||
it("retries a transient upload and then stores the url", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.mocked(flushPendingCommunityAvatar)
|
||||
.mockResolvedValueOnce({ status: "failed" })
|
||||
.mockResolvedValueOnce({
|
||||
status: "uploaded",
|
||||
url: "/api/uploads/retry",
|
||||
});
|
||||
|
||||
render(
|
||||
<>
|
||||
<CreateFlowPendingAvatarFlush
|
||||
sessionUser={{ id: "user-1", email: "maya@example.com" }}
|
||||
sessionResolved
|
||||
/>
|
||||
<AvatarProbe />
|
||||
</>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(flushPendingCommunityAvatar).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByTestId("avatar")).toHaveTextContent("");
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
});
|
||||
|
||||
expect(flushPendingCommunityAvatar).toHaveBeenCalledTimes(2);
|
||||
expect(screen.getByTestId("avatar")).toHaveTextContent(
|
||||
"/api/uploads/retry",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
||||
import { PostLoginDraftTransfer } from "../../app/(app)/create/PostLoginDraftTransfer";
|
||||
import {
|
||||
CREATE_FLOW_ANONYMOUS_KEY,
|
||||
CREATE_FLOW_TRANSFER_PENDING_KEY,
|
||||
} from "../../app/(app)/create/utils/anonymousDraftStorage";
|
||||
import { saveDraftToServer } from "../../lib/create/api";
|
||||
import { flushPendingCommunityAvatar } from "../../lib/create/flushPendingCommunityAvatar";
|
||||
import { clearPendingCommunityAvatarFile } from "../../lib/create/pendingCommunityAvatarUpload";
|
||||
import {
|
||||
renderWithProviders as render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "../utils/test-utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
vi.mock("../../lib/create/api", () => ({
|
||||
fetchDraftFromServer: vi.fn().mockResolvedValue(null),
|
||||
saveDraftToServer: vi.fn().mockResolvedValue({ ok: true }),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/create/flushPendingCommunityAvatar", () => ({
|
||||
flushPendingCommunityAvatar: vi.fn(),
|
||||
forgetFlushedCommunityAvatarUrl: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/create/pendingCommunityAvatarUpload", () => ({
|
||||
clearPendingCommunityAvatarFile: vi.fn().mockResolvedValue(undefined),
|
||||
readPendingCommunityAvatarFile: vi.fn().mockResolvedValue(null),
|
||||
storePendingCommunityAvatarFile: vi.fn(),
|
||||
}));
|
||||
|
||||
function mockSearchParams(value: string) {
|
||||
vi.mocked(useSearchParams).mockReturnValue(
|
||||
new URLSearchParams(value) as NonNullable<
|
||||
ReturnType<typeof useSearchParams>
|
||||
>,
|
||||
);
|
||||
}
|
||||
|
||||
function StateProbe() {
|
||||
const { state } = useCreateFlow();
|
||||
return (
|
||||
<div>
|
||||
<span data-testid="title">{state.title ?? ""}</span>
|
||||
<span data-testid="avatar">{state.communityAvatarUrl ?? ""}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe("PostLoginDraftTransfer", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
localStorage.setItem(
|
||||
CREATE_FLOW_ANONYMOUS_KEY,
|
||||
JSON.stringify({ title: "Garden Club" }),
|
||||
);
|
||||
localStorage.setItem(CREATE_FLOW_TRANSFER_PENDING_KEY, "1");
|
||||
mockSearchParams("syncDraft=1");
|
||||
vi.mocked(flushPendingCommunityAvatar).mockReset();
|
||||
vi.mocked(saveDraftToServer).mockClear();
|
||||
vi.mocked(clearPendingCommunityAvatarFile).mockClear();
|
||||
});
|
||||
|
||||
it("uploads the staged logo before clearing the anonymous draft", async () => {
|
||||
let draftStillPresent = false;
|
||||
vi.mocked(flushPendingCommunityAvatar).mockImplementation(async () => {
|
||||
draftStillPresent =
|
||||
localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY) != null;
|
||||
return { status: "uploaded", url: "/api/uploads/from-guest" };
|
||||
});
|
||||
|
||||
render(
|
||||
<>
|
||||
<PostLoginDraftTransfer
|
||||
sessionUser={{ id: "user-1", email: "maya@example.com" }}
|
||||
/>
|
||||
<StateProbe />
|
||||
</>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("avatar")).toHaveTextContent(
|
||||
"/api/uploads/from-guest",
|
||||
);
|
||||
});
|
||||
expect(draftStillPresent).toBe(true);
|
||||
expect(localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBeNull();
|
||||
expect(saveDraftToServer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "Garden Club",
|
||||
communityAvatarUrl: "/api/uploads/from-guest",
|
||||
}),
|
||||
);
|
||||
expect(clearPendingCommunityAvatarFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the staged file when the upload fails", async () => {
|
||||
vi.mocked(flushPendingCommunityAvatar).mockResolvedValue({
|
||||
status: "failed",
|
||||
});
|
||||
|
||||
render(
|
||||
<>
|
||||
<PostLoginDraftTransfer
|
||||
sessionUser={{ id: "user-1", email: "maya@example.com" }}
|
||||
/>
|
||||
<StateProbe />
|
||||
</>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("title")).toHaveTextContent("Garden Club");
|
||||
});
|
||||
expect(clearPendingCommunityAvatarFile).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId("avatar")).toHaveTextContent("");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, it, expect } from "vitest";
|
||||
import { beforeEach, describe, it, expect, vi } from "vitest";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
renderWithProviders as render,
|
||||
@@ -9,6 +9,13 @@ import "@testing-library/jest-dom/vitest";
|
||||
import { CommunityReviewScreen } from "../../app/(app)/create/screens/review/CommunityReviewScreen";
|
||||
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
||||
import { testRouter } from "../mocks/navigation";
|
||||
import { readPendingCommunityAvatarFile } from "../../lib/create/pendingCommunityAvatarUpload";
|
||||
|
||||
vi.mock("../../lib/create/pendingCommunityAvatarUpload", () => ({
|
||||
readPendingCommunityAvatarFile: vi.fn().mockResolvedValue(null),
|
||||
storePendingCommunityAvatarFile: vi.fn(),
|
||||
clearPendingCommunityAvatarFile: vi.fn(),
|
||||
}));
|
||||
|
||||
function ReviewWithTitle({ title }: { title: string }) {
|
||||
const { state, updateState } = useCreateFlow();
|
||||
@@ -43,6 +50,8 @@ describe("CommunityReviewScreen", () => {
|
||||
beforeEach(() => {
|
||||
testRouter.replace.mockReset();
|
||||
testRouter.push.mockReset();
|
||||
vi.mocked(readPendingCommunityAvatarFile).mockReset();
|
||||
vi.mocked(readPendingCommunityAvatarFile).mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it("renders without crashing", () => {
|
||||
@@ -89,6 +98,33 @@ describe("CommunityReviewScreen", () => {
|
||||
expect(screen.getByText("Garden Club")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a guest logo staged in the browser on the rule card", async () => {
|
||||
const pending = new File([new Uint8Array([1, 2, 3])], "logo.png", {
|
||||
type: "image/png",
|
||||
});
|
||||
vi.mocked(readPendingCommunityAvatarFile).mockResolvedValue(pending);
|
||||
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,
|
||||
});
|
||||
|
||||
render(<ReviewWithTitle title="Garden Club" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("img", { name: "Garden Club" })).toHaveAttribute(
|
||||
"src",
|
||||
"blob:pending-avatar",
|
||||
);
|
||||
});
|
||||
expect(readPendingCommunityAvatarFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("omits the Rule description when the user has not entered community context", () => {
|
||||
render(<ReviewWithTitle title="Garden Club" />);
|
||||
expect(
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const clearPendingCommunityAvatarFile = vi.fn();
|
||||
const forgetFlushedCommunityAvatarUrl = vi.fn();
|
||||
|
||||
vi.mock("../../lib/create/pendingCommunityAvatarUpload", () => ({
|
||||
clearPendingCommunityAvatarFile: () => clearPendingCommunityAvatarFile(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/create/flushPendingCommunityAvatar", () => ({
|
||||
forgetFlushedCommunityAvatarUrl: () => forgetFlushedCommunityAvatarUrl(),
|
||||
}));
|
||||
|
||||
import {
|
||||
CREATE_FLOW_ANONYMOUS_KEY,
|
||||
CREATE_FLOW_TRANSFER_PENDING_KEY,
|
||||
clearAnonymousCreateFlowStorage,
|
||||
} from "../../app/(app)/create/utils/anonymousDraftStorage";
|
||||
|
||||
describe("clearAnonymousCreateFlowStorage", () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
clearPendingCommunityAvatarFile.mockReset();
|
||||
forgetFlushedCommunityAvatarUrl.mockReset();
|
||||
localStorage.setItem(CREATE_FLOW_ANONYMOUS_KEY, "{\"title\":\"Garden\"}");
|
||||
localStorage.setItem(CREATE_FLOW_TRANSFER_PENDING_KEY, "1");
|
||||
});
|
||||
|
||||
it("removes the draft and the staged logo", () => {
|
||||
clearAnonymousCreateFlowStorage();
|
||||
expect(localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBeNull();
|
||||
expect(localStorage.getItem(CREATE_FLOW_TRANSFER_PENDING_KEY)).toBeNull();
|
||||
expect(clearPendingCommunityAvatarFile).toHaveBeenCalledTimes(1);
|
||||
expect(forgetFlushedCommunityAvatarUrl).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps the staged logo when a transfer upload still needs to retry", () => {
|
||||
clearAnonymousCreateFlowStorage({ preservePendingAvatar: true });
|
||||
expect(localStorage.getItem(CREATE_FLOW_ANONYMOUS_KEY)).toBeNull();
|
||||
expect(clearPendingCommunityAvatarFile).not.toHaveBeenCalled();
|
||||
expect(forgetFlushedCommunityAvatarUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { CreateFlowUploadValidationError } from "../../lib/create/createFlowUploadValidation";
|
||||
|
||||
const readPendingCommunityAvatarFile = vi.fn();
|
||||
const clearPendingCommunityAvatarFile = vi.fn();
|
||||
const uploadCreateFlowFile = vi.fn();
|
||||
|
||||
vi.mock("../../lib/create/pendingCommunityAvatarUpload", () => ({
|
||||
readPendingCommunityAvatarFile: (...args: unknown[]) =>
|
||||
readPendingCommunityAvatarFile(...args),
|
||||
clearPendingCommunityAvatarFile: (...args: unknown[]) =>
|
||||
clearPendingCommunityAvatarFile(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/create/uploadToServer", () => ({
|
||||
uploadCreateFlowFile: (...args: unknown[]) => uploadCreateFlowFile(...args),
|
||||
}));
|
||||
|
||||
import {
|
||||
flushPendingCommunityAvatar,
|
||||
forgetFlushedCommunityAvatarUrl,
|
||||
stateWithPendingCommunityAvatar,
|
||||
} from "../../lib/create/flushPendingCommunityAvatar";
|
||||
|
||||
const pending = new File([new Uint8Array([1, 2, 3])], "logo.png", {
|
||||
type: "image/png",
|
||||
});
|
||||
|
||||
describe("flushPendingCommunityAvatar", () => {
|
||||
beforeEach(() => {
|
||||
readPendingCommunityAvatarFile.mockReset();
|
||||
clearPendingCommunityAvatarFile.mockReset();
|
||||
uploadCreateFlowFile.mockReset();
|
||||
clearPendingCommunityAvatarFile.mockResolvedValue(undefined);
|
||||
forgetFlushedCommunityAvatarUrl();
|
||||
});
|
||||
|
||||
it("does nothing when no file is staged", async () => {
|
||||
readPendingCommunityAvatarFile.mockResolvedValue(null);
|
||||
await expect(flushPendingCommunityAvatar()).resolves.toEqual({
|
||||
status: "none",
|
||||
});
|
||||
expect(uploadCreateFlowFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uploads once and remembers the url after the file is cleared", async () => {
|
||||
readPendingCommunityAvatarFile
|
||||
.mockResolvedValueOnce(pending)
|
||||
.mockResolvedValue(null);
|
||||
uploadCreateFlowFile.mockResolvedValue({ url: "/api/uploads/logo" });
|
||||
|
||||
const [first, second] = await Promise.all([
|
||||
flushPendingCommunityAvatar(),
|
||||
flushPendingCommunityAvatar(),
|
||||
]);
|
||||
|
||||
expect(first).toEqual({ status: "uploaded", url: "/api/uploads/logo" });
|
||||
expect(second).toEqual(first);
|
||||
expect(uploadCreateFlowFile).toHaveBeenCalledTimes(1);
|
||||
expect(clearPendingCommunityAvatarFile).toHaveBeenCalledTimes(1);
|
||||
|
||||
await expect(flushPendingCommunityAvatar()).resolves.toEqual({
|
||||
status: "uploaded",
|
||||
url: "/api/uploads/logo",
|
||||
});
|
||||
expect(uploadCreateFlowFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("drops an invalid file and leaves a transient failure staged", async () => {
|
||||
readPendingCommunityAvatarFile.mockResolvedValue(pending);
|
||||
uploadCreateFlowFile.mockRejectedValueOnce(
|
||||
new CreateFlowUploadValidationError("tooLarge"),
|
||||
);
|
||||
|
||||
await expect(flushPendingCommunityAvatar()).resolves.toEqual({
|
||||
status: "invalid",
|
||||
});
|
||||
expect(clearPendingCommunityAvatarFile).toHaveBeenCalledTimes(1);
|
||||
|
||||
uploadCreateFlowFile.mockRejectedValueOnce(new Error("network"));
|
||||
await expect(flushPendingCommunityAvatar()).resolves.toEqual({
|
||||
status: "failed",
|
||||
});
|
||||
expect(clearPendingCommunityAvatarFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("copies a successful upload onto publish state and blocks on failure", async () => {
|
||||
readPendingCommunityAvatarFile.mockResolvedValue(pending);
|
||||
uploadCreateFlowFile.mockResolvedValue({ url: "/api/uploads/logo" });
|
||||
|
||||
await expect(
|
||||
stateWithPendingCommunityAvatar({ title: "Garden" }),
|
||||
).resolves.toEqual({
|
||||
ok: true,
|
||||
state: { title: "Garden", communityAvatarUrl: "/api/uploads/logo" },
|
||||
});
|
||||
|
||||
forgetFlushedCommunityAvatarUrl();
|
||||
uploadCreateFlowFile.mockRejectedValueOnce(new Error("network"));
|
||||
await expect(
|
||||
stateWithPendingCommunityAvatar({
|
||||
title: "Garden",
|
||||
communityAvatarFileName: "logo.png",
|
||||
}),
|
||||
).resolves.toEqual({ ok: false, reason: "upload_failed" });
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import type { CreateFlowState } from "../../../app/(app)/create/types";
|
||||
import { useCreateFlowFinalize } from "../../../app/(app)/create/hooks/useCreateFlowFinalize";
|
||||
import { publishRule, updatePublishedRule } from "../../../lib/create/api";
|
||||
import { buildPublishPayload } from "../../../lib/create/buildPublishPayload";
|
||||
import { stateWithPendingCommunityAvatar } from "../../../lib/create/flushPendingCommunityAvatar";
|
||||
import messages from "../../../messages/en/index";
|
||||
import { writeLastPublishedRule } from "../../../lib/create/lastPublishedRule";
|
||||
import { CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY } from "../../../lib/create/pendingKeepRuleLogin";
|
||||
import {
|
||||
@@ -24,6 +27,13 @@ vi.mock("../../../lib/create/api", () => ({
|
||||
updatePublishedRule: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../lib/create/flushPendingCommunityAvatar", () => ({
|
||||
stateWithPendingCommunityAvatar: vi.fn(async (state: CreateFlowState) => ({
|
||||
ok: true as const,
|
||||
state,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../../../lib/create/lastPublishedRule", () => ({
|
||||
writeLastPublishedRule: vi.fn(),
|
||||
}));
|
||||
@@ -41,6 +51,10 @@ describe("useCreateFlowFinalize", () => {
|
||||
vi.mocked(publishRule).mockReset();
|
||||
vi.mocked(updatePublishedRule).mockReset();
|
||||
vi.mocked(writeLastPublishedRule).mockReset();
|
||||
vi.mocked(stateWithPendingCommunityAvatar).mockReset();
|
||||
vi.mocked(stateWithPendingCommunityAvatar).mockImplementation(
|
||||
async (state) => ({ ok: true as const, state }),
|
||||
);
|
||||
router.push.mockReset();
|
||||
updateState.mockReset();
|
||||
openLogin.mockReset();
|
||||
@@ -148,6 +162,82 @@ describe("useCreateFlowFinalize", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("publishes a guest logo that was only staged in the browser", async () => {
|
||||
vi.mocked(publishRule).mockResolvedValue({
|
||||
ok: true,
|
||||
id: "guest-rule-id",
|
||||
title: "Published title",
|
||||
});
|
||||
vi.mocked(stateWithPendingCommunityAvatar).mockResolvedValue({
|
||||
ok: true,
|
||||
state: {
|
||||
communityAvatarFileName: "logo.png",
|
||||
communityAvatarUrl: "/api/uploads/guest-logo",
|
||||
},
|
||||
});
|
||||
vi.mocked(buildPublishPayload).mockImplementationOnce((draft) => ({
|
||||
ok: true as const,
|
||||
title: "Published title",
|
||||
summary: "Published summary",
|
||||
document: {
|
||||
communityAvatarUrl: draft.communityAvatarUrl,
|
||||
},
|
||||
}));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCreateFlowFinalize({
|
||||
state: { communityAvatarFileName: "logo.png" },
|
||||
router,
|
||||
openLogin,
|
||||
updateState,
|
||||
loginReturnPath: "/create/final-review",
|
||||
sessionUser: null,
|
||||
onGuestPublished,
|
||||
}),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.finalize();
|
||||
});
|
||||
|
||||
expect(publishRule).toHaveBeenCalledWith({
|
||||
title: "Published title",
|
||||
summary: "Published summary",
|
||||
document: { communityAvatarUrl: "/api/uploads/guest-logo" },
|
||||
});
|
||||
expect(updateState).toHaveBeenCalledWith({
|
||||
communityAvatarUrl: "/api/uploads/guest-logo",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not publish when the staged logo fails to upload", async () => {
|
||||
vi.mocked(stateWithPendingCommunityAvatar).mockResolvedValue({
|
||||
ok: false,
|
||||
reason: "upload_failed",
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCreateFlowFinalize({
|
||||
state: { communityAvatarFileName: "logo.png" },
|
||||
router,
|
||||
openLogin,
|
||||
updateState,
|
||||
loginReturnPath: "/create/final-review",
|
||||
sessionUser: null,
|
||||
}),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.finalize();
|
||||
});
|
||||
|
||||
expect(publishRule).not.toHaveBeenCalled();
|
||||
expect(result.current.publishBannerMessage).toBe(
|
||||
messages.create.upload.errors.generic,
|
||||
);
|
||||
expect(result.current.isPublishing).toBe(false);
|
||||
});
|
||||
|
||||
it("hints to claim later when a guest publishes without stakeholder emails", async () => {
|
||||
vi.mocked(publishRule).mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
@@ -80,6 +80,39 @@ describe("POST /api/uploads", () => {
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("returns 401 when a guest uploads an attachment", async () => {
|
||||
getSessionUserMock.mockResolvedValueOnce(null);
|
||||
const res = await POST(
|
||||
multipartRequest({
|
||||
purpose: "customMethodAttachment",
|
||||
fileName: "notes.pdf",
|
||||
fileContent: "hello",
|
||||
contentType: "application/pdf",
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("lets a guest submit a community logo and then validates it", async () => {
|
||||
getSessionUserMock.mockResolvedValueOnce(null);
|
||||
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 500 when UPLOAD_ROOT is unset", async () => {
|
||||
getUploadRootFromEnvMock.mockReturnValueOnce(null);
|
||||
const res = await POST(
|
||||
|
||||
Reference in new issue
Block a user