Skip to content

Commit 0eb8427

Browse files
mengw15Copilot
andauthored
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 <mengw15@uci.edu> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent cc94414 commit 0eb8427

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import {
21+
HttpErrorResponse,
22+
HttpEvent,
23+
HttpHandler,
24+
HttpHeaders,
25+
HttpRequest,
26+
HttpResponse,
27+
} from "@angular/common/http";
28+
import { Observable, firstValueFrom, of, throwError } from "rxjs";
29+
30+
import { BlobErrorHttpInterceptor } from "./blob-error-http-interceptor.service";
31+
32+
/**
33+
* The interceptor is a pure function of (req, next), so the specs drive it
34+
* directly with a stub `HttpHandler` rather than through HttpClient. Two of
35+
* the branches under test — a non-`HttpErrorResponse` error and a
36+
* `FileReader` failure — cannot be produced through `HttpClient` at all
37+
* (it always wraps errors as `HttpErrorResponse`, and a readable Blob never
38+
* triggers `FileReader.onerror`), so direct invocation is the only way to
39+
* cover them.
40+
*/
41+
describe("BlobErrorHttpInterceptor", () => {
42+
let interceptor: BlobErrorHttpInterceptor;
43+
const req = new HttpRequest("GET", "/test");
44+
45+
const handlerReturning = (obs: Observable<HttpEvent<any>>): HttpHandler => ({
46+
handle: (_req: HttpRequest<any>) => obs,
47+
});
48+
49+
// Run the interceptor and resolve to the emitted value or, on error, the error.
50+
const run = (next: HttpHandler): Promise<any> => firstValueFrom(interceptor.intercept(req, next)).catch(e => e);
51+
52+
beforeEach(() => {
53+
interceptor = new BlobErrorHttpInterceptor();
54+
});
55+
56+
afterEach(() => {
57+
vi.unstubAllGlobals();
58+
});
59+
60+
it("passes a successful response through unchanged", async () => {
61+
const response = new HttpResponse({ body: "ok", status: 200 });
62+
expect(await run(handlerReturning(of(response)))).toBe(response);
63+
});
64+
65+
it("re-throws an error that is not an HttpErrorResponse unchanged", async () => {
66+
const err = new Error("not-http");
67+
expect(await run(handlerReturning(throwError(() => err)))).toBe(err);
68+
});
69+
70+
it("re-throws an HttpErrorResponse whose error is not a Blob unchanged", async () => {
71+
const err = new HttpErrorResponse({ error: { message: "plain" }, status: 500 });
72+
expect(await run(handlerReturning(throwError(() => err)))).toBe(err);
73+
});
74+
75+
it("re-throws an HttpErrorResponse with a non-json Blob unchanged", async () => {
76+
const err = new HttpErrorResponse({
77+
error: new Blob(["whatever"], { type: "text/plain" }),
78+
status: 500,
79+
});
80+
expect(await run(handlerReturning(throwError(() => err)))).toBe(err);
81+
});
82+
83+
it("parses an application/json Blob error into a new HttpErrorResponse, preserving status/headers/url", async () => {
84+
const err = new HttpErrorResponse({
85+
error: new Blob([JSON.stringify({ message: "Boom" })], { type: "application/json" }),
86+
status: 502,
87+
statusText: "Bad Gateway",
88+
url: "http://example.com/api",
89+
headers: new HttpHeaders({ "x-request-id": "trace-123" }),
90+
});
91+
92+
const rejected = await run(handlerReturning(throwError(() => err)));
93+
94+
expect(rejected).toBeInstanceOf(HttpErrorResponse);
95+
expect(rejected).not.toBe(err); // a new instance was constructed, not the original
96+
expect(rejected.error).toEqual({ message: "Boom" });
97+
expect(rejected.status).toBe(502);
98+
expect(rejected.statusText).toBe("Bad Gateway");
99+
expect(rejected.url).toBe("http://example.com/api");
100+
expect(rejected.headers.get("x-request-id")).toBe("trace-123");
101+
});
102+
103+
it("builds a new error with a null url when the original error has no url", async () => {
104+
const err = new HttpErrorResponse({
105+
error: new Blob([JSON.stringify({ message: "Boom" })], { type: "application/json" }),
106+
status: 500,
107+
// url omitted → HttpErrorResponse defaults it to null, exercising the
108+
// `err.url !== null ? err.url : undefined` false branch.
109+
});
110+
111+
const rejected = await run(handlerReturning(throwError(() => err)));
112+
113+
expect(rejected).toBeInstanceOf(HttpErrorResponse);
114+
expect(rejected).not.toBe(err); // a new instance was constructed, not the original
115+
expect(rejected.error).toEqual({ message: "Boom" });
116+
expect(rejected.url).toBeNull();
117+
});
118+
119+
it("re-throws the original error when the Blob contains malformed JSON", async () => {
120+
const err = new HttpErrorResponse({
121+
error: new Blob(["not json {"], { type: "application/json" }),
122+
status: 500,
123+
});
124+
expect(await run(handlerReturning(throwError(() => err)))).toBe(err);
125+
});
126+
127+
it("re-throws the original error when the FileReader fails", async () => {
128+
class FailingFileReader {
129+
onload: ((e: Event) => void) | null = null;
130+
onerror: ((e: Event) => void) | null = null;
131+
readAsText(): void {
132+
this.onerror?.(new Event("error"));
133+
}
134+
}
135+
vi.stubGlobal("FileReader", FailingFileReader);
136+
137+
const err = new HttpErrorResponse({
138+
error: new Blob([JSON.stringify({ message: "Boom" })], { type: "application/json" }),
139+
status: 500,
140+
});
141+
expect(await run(handlerReturning(throwError(() => err)))).toBe(err);
142+
});
143+
});

0 commit comments

Comments
 (0)