This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-5461-cc94414e6059944d304c7c85e9cebe03d6a2453d in repository https://gitbox.apache.org/repos/asf/texera.git
commit 0eb8427af94e8c28ad8f4cf673cfc5404e708c8e Author: Meng Wang <[email protected]> AuthorDate: Tue Jun 23 19:19:46 2026 -0700 test(frontend): add spec for BlobErrorHttpInterceptor (#5461) ### What changes were proposed in this PR? Adds a unit spec for `BlobErrorHttpInterceptor`, which previously had none. Covers every branch of `intercept()`: - success → passed through unchanged - re-thrown unchanged for: a non-`HttpErrorResponse` error, an `HttpErrorResponse` whose `error` is not a `Blob`, and a `Blob` error whose type is not `application/json` - an `application/json` `Blob` error → parsed into a structured `HttpErrorResponse` with the original `status` / `statusText` / `url` preserved - malformed JSON in the blob, or a `FileReader` failure → falls back to the original error The interceptor is driven directly with a stub `HttpHandler` rather than through `HttpClient`/`HTTP_INTERCEPTORS`. Two branches — a non-`HttpErrorResponse` error and a `FileReader` failure — cannot be produced through `HttpClient` (it always wraps errors as `HttpErrorResponse`, and a readable `Blob` never triggers `FileReader.onerror`), so direct invocation is the only way to cover them. Follows `frontend/TESTING.md` (Vitest). ### Any related issues, documentation, discussions? Closes #5455. ### How was this PR tested? `yarn test --include='**/blob-error-http-interceptor.service.spec.ts'` → 7 passed. `prettier --check` clean. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (claude-opus-4-7) --------- Signed-off-by: Meng Wang <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> --- .../blob-error-http-interceptor.service.spec.ts | 143 +++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/frontend/src/app/common/service/blob-error-http-interceptor.service.spec.ts b/frontend/src/app/common/service/blob-error-http-interceptor.service.spec.ts new file mode 100644 index 0000000000..1a06826393 --- /dev/null +++ b/frontend/src/app/common/service/blob-error-http-interceptor.service.spec.ts @@ -0,0 +1,143 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + HttpErrorResponse, + HttpEvent, + HttpHandler, + HttpHeaders, + HttpRequest, + HttpResponse, +} from "@angular/common/http"; +import { Observable, firstValueFrom, of, throwError } from "rxjs"; + +import { BlobErrorHttpInterceptor } from "./blob-error-http-interceptor.service"; + +/** + * The interceptor is a pure function of (req, next), so the specs drive it + * directly with a stub `HttpHandler` rather than through HttpClient. Two of + * the branches under test — a non-`HttpErrorResponse` error and a + * `FileReader` failure — cannot be produced through `HttpClient` at all + * (it always wraps errors as `HttpErrorResponse`, and a readable Blob never + * triggers `FileReader.onerror`), so direct invocation is the only way to + * cover them. + */ +describe("BlobErrorHttpInterceptor", () => { + let interceptor: BlobErrorHttpInterceptor; + const req = new HttpRequest("GET", "/test"); + + const handlerReturning = (obs: Observable<HttpEvent<any>>): HttpHandler => ({ + handle: (_req: HttpRequest<any>) => obs, + }); + + // Run the interceptor and resolve to the emitted value or, on error, the error. + const run = (next: HttpHandler): Promise<any> => firstValueFrom(interceptor.intercept(req, next)).catch(e => e); + + beforeEach(() => { + interceptor = new BlobErrorHttpInterceptor(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("passes a successful response through unchanged", async () => { + const response = new HttpResponse({ body: "ok", status: 200 }); + expect(await run(handlerReturning(of(response)))).toBe(response); + }); + + it("re-throws an error that is not an HttpErrorResponse unchanged", async () => { + const err = new Error("not-http"); + expect(await run(handlerReturning(throwError(() => err)))).toBe(err); + }); + + it("re-throws an HttpErrorResponse whose error is not a Blob unchanged", async () => { + const err = new HttpErrorResponse({ error: { message: "plain" }, status: 500 }); + expect(await run(handlerReturning(throwError(() => err)))).toBe(err); + }); + + it("re-throws an HttpErrorResponse with a non-json Blob unchanged", async () => { + const err = new HttpErrorResponse({ + error: new Blob(["whatever"], { type: "text/plain" }), + status: 500, + }); + expect(await run(handlerReturning(throwError(() => err)))).toBe(err); + }); + + it("parses an application/json Blob error into a new HttpErrorResponse, preserving status/headers/url", async () => { + const err = new HttpErrorResponse({ + error: new Blob([JSON.stringify({ message: "Boom" })], { type: "application/json" }), + status: 502, + statusText: "Bad Gateway", + url: "http://example.com/api", + headers: new HttpHeaders({ "x-request-id": "trace-123" }), + }); + + const rejected = await run(handlerReturning(throwError(() => err))); + + expect(rejected).toBeInstanceOf(HttpErrorResponse); + expect(rejected).not.toBe(err); // a new instance was constructed, not the original + expect(rejected.error).toEqual({ message: "Boom" }); + expect(rejected.status).toBe(502); + expect(rejected.statusText).toBe("Bad Gateway"); + expect(rejected.url).toBe("http://example.com/api"); + expect(rejected.headers.get("x-request-id")).toBe("trace-123"); + }); + + it("builds a new error with a null url when the original error has no url", async () => { + const err = new HttpErrorResponse({ + error: new Blob([JSON.stringify({ message: "Boom" })], { type: "application/json" }), + status: 500, + // url omitted → HttpErrorResponse defaults it to null, exercising the + // `err.url !== null ? err.url : undefined` false branch. + }); + + const rejected = await run(handlerReturning(throwError(() => err))); + + expect(rejected).toBeInstanceOf(HttpErrorResponse); + expect(rejected).not.toBe(err); // a new instance was constructed, not the original + expect(rejected.error).toEqual({ message: "Boom" }); + expect(rejected.url).toBeNull(); + }); + + it("re-throws the original error when the Blob contains malformed JSON", async () => { + const err = new HttpErrorResponse({ + error: new Blob(["not json {"], { type: "application/json" }), + status: 500, + }); + expect(await run(handlerReturning(throwError(() => err)))).toBe(err); + }); + + it("re-throws the original error when the FileReader fails", async () => { + class FailingFileReader { + onload: ((e: Event) => void) | null = null; + onerror: ((e: Event) => void) | null = null; + readAsText(): void { + this.onerror?.(new Event("error")); + } + } + vi.stubGlobal("FileReader", FailingFileReader); + + const err = new HttpErrorResponse({ + error: new Blob([JSON.stringify({ message: "Boom" })], { type: "application/json" }), + status: 500, + }); + expect(await run(handlerReturning(throwError(() => err)))).toBe(err); + }); +});
