mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-23 22:35:42 +00:00
Bound feedback requests and verify streamed upload limits
This commit is contained in:
@@ -72,15 +72,34 @@ it("cleans partial uploads and does not expose backend errors", async () => {
|
||||
expect(await response.json()).toEqual({ code: "SUBMISSION_FAILED" });
|
||||
expect(backend.remove).toHaveBeenCalledWith([expect.stringMatching(/^user-id\//)]);
|
||||
});
|
||||
it("caps streamed request bodies even without a content-length header", async () => {
|
||||
it("caps a valid multipart stream before consuming the entire oversized upload", async () => {
|
||||
const multipart = request([new File([new Uint8Array(16 * 1024 * 1024)], "large.bin")]);
|
||||
const bytes = new Uint8Array(await multipart.arrayBuffer());
|
||||
let produced = 0;
|
||||
let cancelled = false;
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
if (produced === bytes.length) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
const end = Math.min(produced + 64 * 1024, bytes.length);
|
||||
controller.enqueue(bytes.slice(produced, end));
|
||||
produced = end;
|
||||
},
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
},
|
||||
});
|
||||
const req = new Request("https://example.test", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer session",
|
||||
"Content-Type": "multipart/form-data; boundary=x",
|
||||
},
|
||||
body: new Uint8Array(12 * 1024 * 1024),
|
||||
});
|
||||
headers: multipart.headers,
|
||||
body,
|
||||
duplex: "half",
|
||||
} as RequestInit);
|
||||
expect(req.headers.has("content-length")).toBe(false);
|
||||
expect((await handle(req)).status).toBe(400);
|
||||
expect(backend.reserve).not.toHaveBeenCalled();
|
||||
expect(cancelled).toBe(true);
|
||||
expect(produced).toBeLessThan(bytes.length);
|
||||
});
|
||||
|
||||
@@ -47,3 +47,35 @@ it("rejects unconfirmed submissions", async () => {
|
||||
api.invoke.mockResolvedValue({ data: {}, error: null });
|
||||
await expect(submitFeedback(input)).rejects.toThrow("not confirmed");
|
||||
});
|
||||
|
||||
it("aborts a stalled request after one minute and permits a subsequent submission", async () => {
|
||||
const { FunctionsClient } = await import("@supabase/functions-js");
|
||||
vi.useFakeTimers();
|
||||
let signal: AbortSignal | null | undefined;
|
||||
try {
|
||||
const functions = new FunctionsClient("https://example.test/functions/v1", {
|
||||
customFetch: (_url, options) =>
|
||||
new Promise((_resolve, reject) => {
|
||||
signal = options?.signal;
|
||||
signal?.addEventListener(
|
||||
"abort",
|
||||
() => reject(new DOMException("Aborted", "AbortError")),
|
||||
{ once: true },
|
||||
);
|
||||
}),
|
||||
});
|
||||
api.invoke.mockImplementation(functions.invoke.bind(functions));
|
||||
const pending = submitFeedback(input).catch((error) => error);
|
||||
await vi.advanceTimersByTimeAsync(59_999);
|
||||
expect(signal?.aborted).toBe(false);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(signal?.aborted).toBe(true);
|
||||
expect(feedbackErrorMessage(await pending)).toBe(
|
||||
"Could not send feedback. Please try again.",
|
||||
);
|
||||
api.invoke.mockResolvedValue({ data: { success: true }, error: null });
|
||||
await expect(submitFeedback(input)).resolves.toBeUndefined();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function submitFeedback(input: {
|
||||
body.set("message", input.message.trim());
|
||||
if (input.logs !== null) body.set("logs", input.logs);
|
||||
for (const file of input.files) body.append("files", file);
|
||||
const result = await client.functions.invoke("submit-feedback", { body });
|
||||
const result = await client.functions.invoke("submit-feedback", { body, timeout: 60_000 });
|
||||
if (result.error) {
|
||||
const response = result.error.context;
|
||||
if (response instanceof Response) {
|
||||
|
||||
Reference in New Issue
Block a user