diff --git a/app/(admin)/admin/sources/_client.tsx b/app/(admin)/admin/sources/_client.tsx index abec554fd..63aff433a 100644 --- a/app/(admin)/admin/sources/_client.tsx +++ b/app/(admin)/admin/sources/_client.tsx @@ -242,8 +242,11 @@ const AdminSourcesPage = () => { }, async onSuccess(signedUrl) { try { - const response = await uploadFile(signedUrl, file); - const { fileLocation } = response; + const { ok, fileLocation } = await uploadFile(signedUrl, file); + if (!ok) { + toast.error("Failed to upload logo, please try again."); + return; + } setEditingSource({ ...editingSource, logoUrl: fileLocation, diff --git a/app/(app)/jobs/create/_client.tsx b/app/(app)/jobs/create/_client.tsx index 1bcb76c80..55a15ae97 100644 --- a/app/(app)/jobs/create/_client.tsx +++ b/app/(app)/jobs/create/_client.tsx @@ -127,8 +127,8 @@ export default function Content() { ); } - const { fileLocation } = await uploadFile(signedUrl, file); - if (!fileLocation) { + const { ok, fileLocation } = await uploadFile(signedUrl, file); + if (!ok || !fileLocation) { setUploadStatus("error"); return toast.error( "Something went wrong uploading the logo, please retry.", diff --git a/app/(app)/settings/_client.tsx b/app/(app)/settings/_client.tsx index 64532f9b0..79384606e 100644 --- a/app/(app)/settings/_client.tsx +++ b/app/(app)/settings/_client.tsx @@ -201,8 +201,12 @@ const Settings = ({ profile }: { profile: User }) => { return; } - const response = await uploadFile(signedUrl, file); - const { fileLocation } = response; + const { ok, fileLocation } = await uploadFile(signedUrl, file); + if (!ok) { + setProfilePhoto({ status: "error", url: "" }); + return; + } + await updateUserPhotoUrl({ url: fileLocation, }); diff --git a/utils/s3helpers.test.ts b/utils/s3helpers.test.ts new file mode 100644 index 000000000..dc55c3527 --- /dev/null +++ b/utils/s3helpers.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { uploadFile } from "./s3helpers"; + +// Regression guard: spreading a Response (`{ ...response }`) dropped its +// prototype getters, so `result.ok` came back `undefined` and the editor +// reported failure on every upload — even on a 200. +describe("uploadFile", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + function mockPutResponse(status: number, url: string) { + const response = new Response(null, { status }); + // Response.url is read-only via the constructor; pin it for fileLocation. + Object.defineProperty(response, "url", { value: url }); + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(response)); + } + + const file = new Blob(["x"], { type: "image/png" }) as unknown as File; + + it("exposes ok=true on a successful PUT so callers can detect success", async () => { + mockPutResponse( + 200, + "https://bucket.s3.amazonaws.com/uploads/u1/abc.png?sig=x", + ); + + const result = await uploadFile("https://signed-url", file); + + expect(result.ok).toBe(true); + expect(result.fileLocation).toBe( + "https://bucket.s3.amazonaws.com/uploads/u1/abc.png", + ); + }); + + it("exposes ok=false when S3 rejects the PUT", async () => { + mockPutResponse( + 403, + "https://bucket.s3.amazonaws.com/uploads/u1/abc.png?sig=x", + ); + + const result = await uploadFile("https://signed-url", file); + + expect(result.ok).toBe(false); + }); +}); diff --git a/utils/s3helpers.ts b/utils/s3helpers.ts index e70b1717d..9e26a0294 100644 --- a/utils/s3helpers.ts +++ b/utils/s3helpers.ts @@ -27,5 +27,7 @@ export const uploadFile = async (signedUrl: string, file: File) => { }); const fileLocation = response.url.split("?")[0]; - return { ...response, fileLocation }; + // Pull fields off explicitly — spreading a Response drops its prototype + // getters (ok/status), leaving callers to read `undefined` for `ok`. + return { ok: response.ok, status: response.status, fileLocation }; };