diff --git a/apps/desktop/src/features/score/scoreStorage.test.ts b/apps/desktop/src/features/score/scoreStorage.test.ts index 0feec199..c039e5bd 100644 --- a/apps/desktop/src/features/score/scoreStorage.test.ts +++ b/apps/desktop/src/features/score/scoreStorage.test.ts @@ -16,6 +16,47 @@ describe("scoreStorage bridge resolution", () => { delete tauriWindow.__TAURI_INVOKE__; }); + it("throws INVALID_RESPONSE_MESSAGE when readScorePdf returns an array with non-number elements (early exit)", async () => { + const mockInvoke = vi.fn().mockResolvedValue([1, 2, "not-a-number", 4]); + const tauriWindow = { + __TAURI_INVOKE__: mockInvoke + } as unknown as TauriWindow; + vi.stubGlobal("window", tauriWindow); + + await expect(readScorePdf("project-1", "score-1")).rejects.toThrow("Invalid score bridge response"); + expect(mockInvoke).toHaveBeenCalledWith("read_score_pdf", { projectId: "project-1", scoreId: "score-1" }); + }); + + it.each([ + ["negative", -1], + ["above the byte range", 256], + ["fractional", 1.5], + ["NaN", Number.NaN], + ["infinite", Number.POSITIVE_INFINITY] + ])("rejects %s numeric values before Uint8Array coercion", async (_label, invalidByte) => { + const mockInvoke = vi.fn().mockResolvedValue([0, invalidByte, 255]); + const tauriWindow = { + __TAURI_INVOKE__: mockInvoke + } as unknown as TauriWindow; + vi.stubGlobal("window", tauriWindow); + + await expect(readScorePdf("project-1", "score-1")).rejects.toThrow("Invalid score bridge response"); + expect(mockInvoke).toHaveBeenCalledWith("read_score_pdf", { projectId: "project-1", scoreId: "score-1" }); + }); + + it("returns Uint8Array when readScorePdf returns a valid number array", async () => { + const mockInvoke = vi.fn().mockResolvedValue([1, 2, 3, 4]); + const tauriWindow = { + __TAURI_INVOKE__: mockInvoke + } as unknown as TauriWindow; + vi.stubGlobal("window", tauriWindow); + + const result = await readScorePdf("project-1", "score-1"); + expect(result).toBeInstanceOf(Uint8Array); + expect(result).toEqual(new Uint8Array([1, 2, 3, 4])); + expect(mockInvoke).toHaveBeenCalledWith("read_score_pdf", { projectId: "project-1", scoreId: "score-1" }); + }); + it("fails closed on every command when there is no window (non-browser runtime)", async () => { // Simulate a runtime without a DOM window (e.g. SSR / bundler prerender): // getInvoke() must take the `typeof window === "undefined"` branch and diff --git a/apps/desktop/src/features/score/scoreStorage.ts b/apps/desktop/src/features/score/scoreStorage.ts index 492f1259..e9bd8990 100644 --- a/apps/desktop/src/features/score/scoreStorage.ts +++ b/apps/desktop/src/features/score/scoreStorage.ts @@ -91,7 +91,18 @@ export async function readScorePdf(projectId: string, scoreId: string): Promise< if (response instanceof ArrayBuffer) { return new Uint8Array(response); } - if (Array.isArray(response) && response.every((byte) => typeof byte === "number")) { + if (Array.isArray(response)) { + for (let index = 0; index < response.length; index += 1) { + const byte = response[index]; + if ( + typeof byte !== "number" || + !Number.isInteger(byte) || + byte < 0 || + byte > 255 + ) { + throw new Error(INVALID_RESPONSE_MESSAGE); + } + } return Uint8Array.from(response as number[]); }