Skip to content

Commit 2954a60

Browse files
committed
Add tests for ApiError and user-facing error mapping
1 parent 5ac0d36 commit 2954a60

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, it } from "vitest";
2+
import { ApiError } from "../errors";
3+
4+
describe("ApiError", () => {
5+
it("includes stale session guidance for unauthorized responses", async () => {
6+
const response = new Response(
7+
JSON.stringify({ detail: "Session token is no longer valid." }),
8+
{
9+
status: 401,
10+
statusText: "Unauthorized",
11+
headers: { "Content-Type": "application/json" },
12+
}
13+
);
14+
15+
const error = await ApiError.createFromResponse(response, {
16+
sessionWasUsed: true,
17+
});
18+
19+
expect(error.message).toContain(
20+
"Your session has expired. Please sign in again."
21+
);
22+
expect(error.message).toContain("Session token is no longer valid.");
23+
expect(error.message).toContain("(401 Unauthorized)");
24+
});
25+
26+
it("surfaces validation issue details when available", async () => {
27+
const response = new Response(
28+
JSON.stringify({ errors: { field: ["Missing value", "Invalid state"] } }),
29+
{
30+
status: 422,
31+
statusText: "Unprocessable Entity",
32+
headers: { "Content-Type": "application/json" },
33+
}
34+
);
35+
36+
const error = await ApiError.createFromResponse(response);
37+
38+
expect(error.message).toContain(
39+
"The server could not process the request due to validation errors."
40+
);
41+
expect(error.message).toContain("Missing value");
42+
expect(error.message).toContain("(422 Unprocessable Entity)");
43+
});
44+
});
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, expect, it } from "vitest";
2+
import { z } from "zod";
3+
import {
4+
ApiError,
5+
RequestBodyParseError,
6+
createResourceNotFoundError,
7+
mapApiErrorToUserFacingError,
8+
UserFacingError,
9+
} from "../index";
10+
11+
describe("mapApiErrorToUserFacingError", () => {
12+
it("categorises stale session responses as auth errors", async () => {
13+
const response = new Response(
14+
JSON.stringify({ detail: "Session token is no longer valid." }),
15+
{
16+
status: 401,
17+
statusText: "Unauthorized",
18+
headers: { "Content-Type": "application/json" },
19+
}
20+
);
21+
22+
const apiError = await ApiError.createFromResponse(response, {
23+
sessionWasUsed: true,
24+
});
25+
26+
const userFacing = mapApiErrorToUserFacingError(apiError);
27+
28+
expect(userFacing).toBeInstanceOf(UserFacingError);
29+
expect(userFacing.category).toBe("auth");
30+
expect(userFacing.status).toBe(401);
31+
expect(userFacing.headline).toContain("session has expired");
32+
expect(userFacing.description).toContain(
33+
"Session token is no longer valid."
34+
);
35+
expect(userFacing.context?.sessionWasUsed).toBe(true);
36+
});
37+
38+
it("maps request validation issues to validation category", () => {
39+
const schema = z.object({ field: z.string().min(1) });
40+
const result = schema.safeParse({ field: "" });
41+
42+
expect(result.success).toBe(false);
43+
44+
const zodError = result.success ? undefined : result.error;
45+
const validationError = new RequestBodyParseError(zodError!);
46+
47+
const userFacing = mapApiErrorToUserFacingError(validationError);
48+
49+
expect(userFacing.category).toBe("validation");
50+
expect(userFacing.headline).toContain("Invalid request data");
51+
});
52+
53+
it("falls back to network category for fetch issues", () => {
54+
const networkError = new TypeError("Failed to fetch");
55+
56+
const userFacing = mapApiErrorToUserFacingError(networkError, {
57+
fallbackHeadline: "Network issue detected.",
58+
fallbackDescription: "Check your connection and retry.",
59+
});
60+
61+
expect(userFacing.category).toBe("network");
62+
expect(userFacing.headline).toBe("Network issue detected.");
63+
expect(userFacing.description).toBe("Check your connection and retry.");
64+
});
65+
66+
it("creates a consistent not found user-facing error", async () => {
67+
const response = new Response(JSON.stringify({ detail: "Not found." }), {
68+
status: 404,
69+
statusText: "Not Found",
70+
headers: { "Content-Type": "application/json" },
71+
});
72+
73+
const apiError = await ApiError.createFromResponse(response);
74+
75+
const userFacing = createResourceNotFoundError({
76+
resourceName: "team",
77+
identifier: "alpha",
78+
originalError: apiError,
79+
});
80+
81+
expect(userFacing).toBeInstanceOf(UserFacingError);
82+
expect(userFacing.headline).toBe("Team not found.");
83+
expect(userFacing.description).toBe('We could not find the team "alpha".');
84+
expect(userFacing.status).toBe(404);
85+
expect(userFacing.category).toBe("not_found");
86+
expect(userFacing.context?.resource).toBe("team");
87+
expect(userFacing.context?.identifier).toBe("alpha");
88+
});
89+
90+
it("falls back to generic copy when identifier absent", async () => {
91+
const response = new Response("Not found.", {
92+
status: 404,
93+
statusText: "Not Found",
94+
});
95+
96+
const apiError = await ApiError.createFromResponse(response);
97+
98+
const userFacing = createResourceNotFoundError({
99+
resourceName: "team",
100+
originalError: apiError,
101+
});
102+
103+
expect(userFacing.description).toBe(
104+
"We could not find the requested team."
105+
);
106+
expect(userFacing.context?.identifier).toBeUndefined();
107+
});
108+
});

0 commit comments

Comments
 (0)