





















@@ -358,3 +358,129 @@ describe("nextcloud-talk send cfg threading", () => {
358358).rejects.toThrow("Nextcloud Talk reaction failed: 403 forbidden");
359359});
360360});
361+362+describe("nextcloud-talk send bounded response reads", () => {
363+const fetchMock = vi.fn<typeof fetch>();
364+const account = {
365+accountId: "default",
366+baseUrl: "https://nextcloud.example.com",
367+secret: "secret-value",
368+};
369+370+// Builds a streaming body with NO content-length so only the streaming byte
371+// cap can stop it. `chunks` chunks of `chunkBytes` each => total may exceed cap.
372+function streamingResponse(params: {
373+status: number;
374+chunkBytes: number;
375+chunks: number;
376+contentType: string;
377+fill?: number;
378+}): Response {
379+let remaining = params.chunks;
380+const stream = new ReadableStream<Uint8Array>({
381+pull(controller) {
382+if (remaining <= 0) {
383+controller.close();
384+return;
385+}
386+remaining -= 1;
387+controller.enqueue(new Uint8Array(params.chunkBytes).fill(params.fill ?? 0x7b));
388+},
389+});
390+return new Response(stream, {
391+status: params.status,
392+headers: { "content-type": params.contentType },
393+});
394+}
395+396+beforeEach(() => {
397+vi.stubGlobal("fetch", fetchMock);
398+hoisted.mockFetchGuard.mockImplementation(async (p: { url: string; init?: RequestInit }) => {
399+const response = await globalThis.fetch(p.url, p.init);
400+return { response, release: async () => {}, finalUrl: p.url };
401+});
402+hoisted.resolveNextcloudTalkAccount.mockReset();
403+hoisted.resolveNextcloudTalkAccount.mockReturnValue(account);
404+hoisted.record.mockReset();
405+hoisted.generateNextcloudTalkSignature.mockClear();
406+});
407+408+afterEach(() => {
409+fetchMock.mockReset();
410+hoisted.mockFetchGuard.mockReset();
411+vi.unstubAllGlobals();
412+});
413+414+it("keeps the unknown receipt when a success body exceeds the JSON byte cap", async () => {
415+// 17 MiB streamed as 200-OK JSON with no content-length: over the 16 MiB cap.
416+fetchMock.mockResolvedValueOnce(
417+streamingResponse({
418+status: 200,
419+chunkBytes: 1024 * 1024,
420+chunks: 17,
421+contentType: "application/json",
422+}),
423+);
424+425+const result = await sendMessageNextcloudTalk("room:abc", "hello", {
426+cfg: { source: "provided" },
427+});
428+429+// Over-limit success body must not throw and must fall back to the unknown receipt.
430+expect(result.messageId).toBe("unknown");
431+expect(result.timestamp).toBeUndefined();
432+});
433+434+it("bounds an oversized error body into a short send-failure snippet", async () => {
435+fetchMock.mockResolvedValueOnce(
436+streamingResponse({
437+status: 400,
438+chunkBytes: 1024 * 1024,
439+chunks: 17,
440+contentType: "text/plain",
441+}),
442+);
443+444+await expect(
445+sendMessageNextcloudTalk("room:abc", "hello", { cfg: { source: "provided" } }),
446+).rejects.toThrow(/Nextcloud Talk: bad request/);
447+});
448+449+it("bounds an oversized reaction error body into a short snippet", async () => {
450+fetchMock.mockResolvedValueOnce(
451+streamingResponse({
452+status: 500,
453+chunkBytes: 1024 * 1024,
454+chunks: 17,
455+contentType: "text/plain",
456+}),
457+);
458+459+let caught: unknown;
460+try {
461+await sendReactionNextcloudTalk("room:abc", "m-1", "👍", { cfg: { source: "provided" } });
462+} catch (error) {
463+caught = error;
464+}
465+466+expect(caught).toBeInstanceOf(Error);
467+// The collapsed snippet caps the message far below the streamed 17 MiB body.
468+expect((caught as Error).message.length).toBeLessThan(4_000);
469+});
470+471+it("still parses a normal small success body", async () => {
472+fetchMock.mockResolvedValueOnce(
473+new Response(JSON.stringify({ ocs: { data: { id: 99, timestamp: 1_700_000_000 } } }), {
474+status: 200,
475+headers: { "content-type": "application/json" },
476+}),
477+);
478+479+const result = await sendMessageNextcloudTalk("room:abc", "hello", {
480+cfg: { source: "provided" },
481+});
482+483+expect(result.messageId).toBe("99");
484+expect(result.timestamp).toBe(1_700_000_000);
485+});
486+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。