This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 0eb8427af9 test(frontend): add spec for BlobErrorHttpInterceptor
(#5461)
0eb8427af9 is described below
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);
+ });
+});