-
Notifications
You must be signed in to change notification settings - Fork 4
Add tests for ApiError and user-facing error mapping #1635
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 11-19-_ts-api-react-actions_enhance_error_handling_in_api_actions_and_hooks_with_user-facing_error_mapping
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { ApiError } from "../errors"; | ||
|
|
||
| describe("ApiError", () => { | ||
| it("includes stale session guidance for unauthorized responses", async () => { | ||
| const response = new Response( | ||
| JSON.stringify({ detail: "Session token is no longer valid." }), | ||
| { | ||
| status: 401, | ||
| statusText: "Unauthorized", | ||
| headers: { "Content-Type": "application/json" }, | ||
| } | ||
| ); | ||
|
|
||
| const error = await ApiError.createFromResponse(response, { | ||
| sessionWasUsed: true, | ||
| }); | ||
|
|
||
| expect(error.message).toContain( | ||
| "Your session has expired. Please sign in again." | ||
| ); | ||
| expect(error.message).toContain("Session token is no longer valid."); | ||
| expect(error.message).toContain("(401 Unauthorized)"); | ||
| }); | ||
|
|
||
| it("surfaces validation issue details when available", async () => { | ||
| const response = new Response( | ||
| JSON.stringify({ errors: { field: ["Missing value", "Invalid state"] } }), | ||
| { | ||
| status: 422, | ||
| statusText: "Unprocessable Entity", | ||
| headers: { "Content-Type": "application/json" }, | ||
| } | ||
| ); | ||
|
|
||
| const error = await ApiError.createFromResponse(response); | ||
|
|
||
| expect(error.message).toContain( | ||
| "The server could not process the request due to validation errors." | ||
| ); | ||
| expect(error.message).toContain("Missing value"); | ||
| expect(error.message).toContain("(422 Unprocessable Entity)"); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { z } from "zod"; | ||
| import { | ||
Check failureCode scanning / ESLint Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error test
Insert ⏎
|
||
| ApiError, | ||
| RequestBodyParseError, | ||
Check failureCode scanning / ESLint Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error test
Insert Error,⏎··UserFacing
|
||
| createResourceNotFoundError, | ||
| mapApiErrorToUserFacingError, | ||
| UserFacingError, | ||
|
Comment on lines
+7
to
+8
Check failureCode scanning / ESLint Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error test
Delete UserFacingError,⏎··
|
||
| } from "../index"; | ||
|
|
||
| describe("mapApiErrorToUserFacingError", () => { | ||
| it("categorises stale session responses as auth errors", async () => { | ||
| const response = new Response( | ||
| JSON.stringify({ detail: "Session token is no longer valid." }), | ||
| { | ||
| status: 401, | ||
| statusText: "Unauthorized", | ||
| headers: { "Content-Type": "application/json" }, | ||
| } | ||
| ); | ||
|
|
||
| const apiError = await ApiError.createFromResponse(response, { | ||
| sessionWasUsed: true, | ||
| }); | ||
|
|
||
| const userFacing = mapApiErrorToUserFacingError(apiError); | ||
|
|
||
| expect(userFacing).toBeInstanceOf(UserFacingError); | ||
| expect(userFacing.category).toBe("auth"); | ||
| expect(userFacing.status).toBe(401); | ||
| expect(userFacing.headline).toContain("session has expired"); | ||
| expect(userFacing.description).toContain( | ||
| "Session token is no longer valid." | ||
| ); | ||
| expect(userFacing.context?.sessionWasUsed).toBe(true); | ||
| }); | ||
|
|
||
| it("maps request validation issues to validation category", () => { | ||
| const schema = z.object({ field: z.string().min(1) }); | ||
| const result = schema.safeParse({ field: "" }); | ||
|
|
||
| expect(result.success).toBe(false); | ||
|
|
||
| const zodError = result.success ? undefined : result.error; | ||
| const validationError = new RequestBodyParseError(zodError!); | ||
|
|
||
| const userFacing = mapApiErrorToUserFacingError(validationError); | ||
|
|
||
| expect(userFacing.category).toBe("validation"); | ||
| expect(userFacing.headline).toContain("Invalid request data"); | ||
| }); | ||
|
|
||
| it("falls back to network category for fetch issues", () => { | ||
| const networkError = new TypeError("Failed to fetch"); | ||
|
|
||
| const userFacing = mapApiErrorToUserFacingError(networkError, { | ||
| fallbackHeadline: "Network issue detected.", | ||
| fallbackDescription: "Check your connection and retry.", | ||
| }); | ||
|
|
||
| expect(userFacing.category).toBe("network"); | ||
| expect(userFacing.headline).toBe("Network issue detected."); | ||
| expect(userFacing.description).toBe("Check your connection and retry."); | ||
| }); | ||
|
|
||
| it("creates a consistent not found user-facing error", async () => { | ||
| const response = new Response(JSON.stringify({ detail: "Not found." }), { | ||
| status: 404, | ||
| statusText: "Not Found", | ||
| headers: { "Content-Type": "application/json" }, | ||
| }); | ||
|
|
||
| const apiError = await ApiError.createFromResponse(response); | ||
|
|
||
| const userFacing = createResourceNotFoundError({ | ||
| resourceName: "team", | ||
| identifier: "alpha", | ||
| originalError: apiError, | ||
| }); | ||
|
|
||
| expect(userFacing).toBeInstanceOf(UserFacingError); | ||
| expect(userFacing.headline).toBe("Team not found."); | ||
| expect(userFacing.description).toBe('We could not find the team "alpha".'); | ||
| expect(userFacing.status).toBe(404); | ||
| expect(userFacing.category).toBe("not_found"); | ||
| expect(userFacing.context?.resource).toBe("team"); | ||
| expect(userFacing.context?.identifier).toBe("alpha"); | ||
| }); | ||
|
|
||
| it("falls back to generic copy when identifier absent", async () => { | ||
| const response = new Response("Not found.", { | ||
| status: 404, | ||
| statusText: "Not Found", | ||
| }); | ||
|
|
||
| const apiError = await ApiError.createFromResponse(response); | ||
|
|
||
| const userFacing = createResourceNotFoundError({ | ||
| resourceName: "team", | ||
| originalError: apiError, | ||
| }); | ||
|
|
||
| expect(userFacing.description).toBe( | ||
| "We could not find the requested team." | ||
| ); | ||
| expect(userFacing.context?.identifier).toBeUndefined(); | ||
| }); | ||
| }); | ||
Check failure
Code scanning / ESLint
Ensure code is properly formatted, use insertion, deletion, or replacement to obtain desired formatting. Error test