diff --git a/frontend/src/core/uploads/api.ts b/frontend/src/core/uploads/api.ts index e70279061..a12688685 100644 --- a/frontend/src/core/uploads/api.ts +++ b/frontend/src/core/uploads/api.ts @@ -116,8 +116,9 @@ export async function deleteUploadedFile( threadId: string, filename: string, ): Promise<{ success: boolean; message: string }> { + const encodedFilename = encodeURIComponent(filename); const response = await fetch( - `${getBackendBaseURL()}/api/threads/${threadId}/uploads/${filename}`, + `${getBackendBaseURL()}/api/threads/${threadId}/uploads/${encodedFilename}`, { method: "DELETE", }, diff --git a/frontend/tests/unit/core/uploads/api.test.ts b/frontend/tests/unit/core/uploads/api.test.ts new file mode 100644 index 000000000..3cdb83a4e --- /dev/null +++ b/frontend/tests/unit/core/uploads/api.test.ts @@ -0,0 +1,49 @@ +import { beforeEach, describe, expect, rs, test } from "@rstest/core"; + +rs.mock("@/core/api/fetcher", () => ({ + fetch: rs.fn(), +})); + +rs.mock("@/core/config", () => ({ + getBackendBaseURL: () => "/backend", +})); + +import { fetch as fetcher } from "@/core/api/fetcher"; +import { deleteUploadedFile } from "@/core/uploads/api"; + +const mockedFetch = rs.mocked(fetcher); + +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + statusText: status >= 400 ? "Error" : "OK", + headers: { "Content-Type": "application/json" }, + }); +} + +beforeEach(() => { + mockedFetch.mockReset(); +}); + +describe("uploads api", () => { + test("encodes uploaded filenames in delete request paths", async () => { + mockedFetch.mockResolvedValueOnce( + jsonResponse(200, { + success: true, + message: "Deleted report#1?.txt", + }), + ); + + await expect( + deleteUploadedFile("thread-1", "report#1?.txt"), + ).resolves.toEqual({ + success: true, + message: "Deleted report#1?.txt", + }); + + expect(mockedFetch).toHaveBeenCalledWith( + "/backend/api/threads/thread-1/uploads/report%231%3F.txt", + { method: "DELETE" }, + ); + }); +});