🧪 Tester view

Untested behaviours, edge cases, test gap inventory  ·  QA Engineer, SDET, Test Lead

Generated 4 days ago
⚡ Agent trace  ·  13 steps  ·  $0.0329  ·  click to expand
search_claims(query='error handling exception try catch', category='behavioral')search_claims(query='edge case boundary limit empty null', category='behavioral')search_claims(query='test coverage unit test', category='any')search_claims(query='behavioral', category='behavioral')search_claims(query='security', category='security')search_claims(query='structural', category='structural')search_claims(query='function method class', max_results=30)search_claims(query='return throws', max_results=30)search_claims(query='validate check assert', max_results=30)search_claims(query='redirect error', max_results=30)search_claims(query='state transition effect', max_results=30)search_claims(query='input form submit', max_results=30)search_claims(max_results=50, query='api fetch request')search_claims(max_results=50, query='component render')search_claims(max_results=50, query='hook context provider')read_source_file(file_path='frontend/src/main.tsx', start_line=1)read_source_file(file_path='frontend/src/routes/login.tsx', start_line=1)read_source_file(file_path='frontend/src/routes/signup.tsx', start_line=1)read_source_file(file_path='frontend/src/routes/login.tsx', start_line=61)read_source_file(file_path='frontend/src/routes/signup.tsx', start_line=61)get_file_claims(file_path='frontend/src/hooks/useAuth.ts')read_source_file(file_path='frontend/src/hooks/useAuth.ts', start_line=1)search_claims(max_results=30, query='mutation query invalidate')search_claims(max_results=30, query='delete remove account user')read_source_file(file_path='frontend/src/hooks/useAuth.ts', start_line=61)get_file_claims(file_path='frontend/src/components/UserSettings/DeleteConfirmation.tsx')get_file_claims(file_path='frontend/src/components/Admin/DeleteUser.tsx')get_file_claims(file_path='frontend/src/components/UserSettings/DeleteAccount.tsx')read_source_file(file_path='frontend/src/components/UserSettings/DeleteConfirmation.tsx', start_line=1)read_source_file(file_path='frontend/src/components/Admin/DeleteUser.tsx', start_line=1)get_file_claims(file_path='frontend/src/utils')read_source_file(file_path='frontend/src/utils.ts', start_line=1)read_source_file(file_path='frontend/src/components/UserSettings/DeleteConfirmation.tsx', start_line=61)read_source_file(file_path='frontend/src/components/Admin/DeleteUser.tsx', start_line=61)get_file_claims(file_path='frontend/src/components/Items/AddItem.tsx')get_file_claims(file_path='frontend/src/components/Items/EditItem.tsx')get_file_claims(file_path='frontend/src/components/Admin/AddUser.tsx')get_file_claims(file_path='frontend/src/components/Admin/EditUser.tsx')

Test Intelligence Report

High-Risk Untested Behaviours

1. handleApiError silently redirects on 401/403 — no user feedback

File: frontend/src/main.tsx:21-26 Risk: HIGH — When any API call returns a 401 or 403, the error handler removes the token and redirects to /login via window.location.href. This is a hard redirect that bypasses the React Router, losing all in-memory state. The user receives no toast, no error message explaining why they were logged out. If a mutation fails mid-flight (e.g., saving a form), the user loses their work without explanation.

2. handleError in utils.ts silently falls back to "Something went wrong."

File: frontend/src/utils.ts:12-13 Risk: HIGH — The extractErrorMessage function has a fallback chain: - If err.body.detail is falsy → returns "Something went wrong." - This generic message is shown to the user via toast, providing no actionable information.

3. DeleteConfirmation uses useForm only for handleSubmit — no validation

File: frontend/src/components/UserSettings/DeleteConfirmation.tsx:24 Risk: MEDIUM — The component imports useForm from react-hook-form but only uses handleSubmit. There is no form validation, no confirmation text input (e.g., typing "DELETE" to confirm). A single click on "Delete" triggers the destructive mutation. This is a UX risk for accidental account deletion.

4. DeleteUser also uses useForm only for handleSubmit — no validation

File: frontend/src/components/Admin/DeleteUser.tsx:31 Risk: MEDIUM — Same pattern as above. Admin can delete a user (and all associated items) with a single click after opening the dialog. No confirmation text required.

5. onSettled invalidates queries even on error — potential stale data

Files: - frontend/src/components/Admin/DeleteUser.tsx:45-46 — invalidates all queries - frontend/src/components/UserSettings/DeleteConfirmation.tsx:34-36 — invalidates ['currentUser'] - frontend/src/components/Items/AddItem.tsx:61-63 — invalidates ['items'] - frontend/src/components/Admin/AddUser.tsx:70-72 — invalidates ['users'] - frontend/src/hooks/useAuth.ts:36-38 — invalidates ['users'] on signup

Risk: MEDIUMonSettled fires on both success and error. If a mutation fails, the query cache is still invalidated, causing a refetch that may show stale or inconsistent data. This is especially risky for DeleteUser which invalidates all queries.


Edge Case Inventory

Form Validation Edge Cases

File Line(s) Edge Case Risk
frontend/src/routes/login.tsx 25-31 username validated as z.email() — no support for non-email usernames LOW
frontend/src/routes/login.tsx 28-30 Password: min(1) then min(8) — redundant check LOW
frontend/src/routes/signup.tsx 23-38 full_name only requires min(1) — accepts whitespace-only names MEDIUM
frontend/src/routes/signup.tsx 35-37 Password confirmation mismatch error is on confirm_password field only LOW
frontend/src/components/Items/AddItem.tsx 30-33 title requires min(1) — accepts whitespace-only titles MEDIUM
frontend/src/components/Admin/AddUser.tsx 17-33 full_name is optional — admin can create user with no name LOW
frontend/src/components/Admin/EditUser.tsx 24-39 password is optional with min(8) — empty password is stripped before submission MEDIUM

State & Timing Edge Cases

File Line(s) Edge Case Risk
frontend/src/hooks/useAuth.ts 23-27 useQuery for currentUser is enabled only when isLoggedIn() — if token exists but is expired, the query returns 401, triggering handleApiError redirect HIGH
frontend/src/hooks/useAuth.ts 41-46 login stores token in localStorage but does not invalidate currentUser query — stale user data may persist until next render cycle MEDIUM
frontend/src/hooks/useAuth.ts 56-59 logout removes token and navigates, but does not invalidate any query caches — stale data remains in memory MEDIUM
frontend/src/routes/login.tsx 37-43 beforeLoad guard runs before component mount — if isLoggedIn() returns true, redirect throws. But isLoggedIn only checks localStorage, not token validity HIGH
frontend/src/routes/signup.tsx 44-49 Same guard pattern as login — redirects authenticated users away from signup MEDIUM

Data Boundary Edge Cases

File Line(s) Edge Case Risk
frontend/src/utils.ts 9-11 err.body.detail is accessed with (err.body as any)?.detail — if detail is an array but empty, falls through to "Something went wrong." MEDIUM
frontend/src/utils.ts 24-31 getInitials splits by space, takes first 2 parts — single-word names get 1 initial, names with >2 words get truncated LOW
frontend/src/components/Admin/DeleteUser.tsx 33-34 deleteUser passes id as userId — no validation that id is a valid UUID/string MEDIUM

Error Handling Gaps

1. Global API error handler does not distinguish error types

File: frontend/src/main.tsx:21-26 Gap: The handleApiError only checks for 401 or 403. All other HTTP errors (400, 404, 409, 422, 500) are not handled globally. They rely on individual mutation/query onError handlers. If a query has no onError handler, the error is silently swallowed by React Query.

2. extractErrorMessage has incomplete type safety

File: frontend/src/utils.ts:4-14 Gap: The function uses (err.body as any)?.detail — this bypasses TypeScript checking. If the API returns an error body with a different structure (e.g., { message: "..." } instead of { detail: "..." }), the function returns "Something went wrong." with no indication of the actual error.

3. No loading state for currentUser query

File: frontend/src/hooks/useAuth.ts:23-27 Gap: The useAuth hook only returns user from the query. It does not expose isLoading or isError states. Components using useAuth cannot distinguish between "user is not logged in" and "user data is still loading" or "user fetch failed."

4. onSettled invalidates queries on error — no error isolation

Files: Multiple (see High-Risk section #5) Gap: When a mutation fails, onSettled still runs and invalidates query caches. This means a failed delete will still trigger a refetch of the user list, potentially showing data that was about to be deleted. The pattern should use onSuccess for invalidation, not onSettled.

5. No network error handling for offline scenarios

Gap: None of the components handle navigator.onLine or network timeout errors specifically. If the API is unreachable, the user gets either a generic "Something went wrong." toast or a hard redirect to /login (if the error is an AxiosError that happens to have status 0).


Suggested Test Scenarios

Scenario 1: Expired token during form submission

Given the user is logged in with an expired access_token in localStorage
And the user is filling out the "Add Item" form
When the user clicks "Save"
Then the mutation fails with 401
And the user is redirected to /login via window.location.href
And the form data is lost
And no toast message explains what happened

Source: frontend/src/main.tsx:21-26, frontend/src/components/Items/AddItem.tsx:51-63

Scenario 2: API returns error with non-standard body

Given the API returns an error with body { "message": "Rate limit exceeded" }
When any mutation or query fails
Then extractErrorMessage is called
And err.body.detail is undefined
And the user sees toast: "Something went wrong."
And the actual error "Rate limit exceeded" is hidden

Source: frontend/src/utils.ts:9-13

Scenario 3: Admin deletes user while mutation is pending

Given the admin opens the Delete User dialog for user "Alice"
When the admin clicks "Delete"
And the mutation is in flight (isPending = true)
Then the Cancel button is disabled
And the Delete button shows loading state
And the admin cannot close the dialog or click Delete again

Source: frontend/src/components/Admin/DeleteUser.tsx:76-87

Scenario 4: Failed delete still refreshes all data

Given the admin attempts to delete user "Bob"
And the API returns a 500 error
Then onError shows an error toast
And onSettled invalidates ALL query caches
And all data tables refetch unnecessarily
And the user list shows data that may be inconsistent

Source: frontend/src/components/Admin/DeleteUser.tsx:44-47

Scenario 5: User navigates to /login while already authenticated

Given the user has a valid access_token in localStorage
When the user navigates to /login
Then the beforeLoad guard fires
And isLoggedIn() returns true
And the user is redirected to /
And the login form never renders

Source: frontend/src/routes/login.tsx:37-43, frontend/src/hooks/useAuth.ts:14-16

Scenario 6: Signup with whitespace-only full_name

Given the user is on the signup page
When the user enters "   " as full_name
And submits the form
Then Zod validation passes (min(1) accepts whitespace)
And the API receives full_name = "   "
And the user is created with a whitespace-only name

Source: frontend/src/routes/signup.tsx:26

Scenario 7: Logout does not clear query cache

Given the user is logged in with cached user data
When the user clicks logout
Then access_token is removed from localStorage
And the user is navigated to /login
But the query cache still contains ['currentUser'] data
And if another user logs in, they briefly see stale data

Source: frontend/src/hooks/useAuth.ts:56-59

Scenario 8: Login success does not invalidate currentUser

Given a user logs in successfully
Then the token is stored in localStorage
And the user is navigated to /
But the ['currentUser'] query is NOT invalidated
And the user data may be stale until the next query refetch

Source: frontend/src/hooks/useAuth.ts:48-54


Complex Code Paths

1. extractErrorMessage — branching on error shape

File: frontend/src/utils.ts:4-14 Complexity: 3 conditional branches - instanceof AxiosError → returns err.message - Array.isArray(errDetail) && errDetail.length > 0 → returns errDetail[0].msg - Fallback → returns errDetail || "Something went wrong." Risk: The (err.body as any) cast bypasses type safety. The function assumes a specific error shape from the generated client. If the API changes its error format, this function silently degrades.

2. useAuth hook — state machine with 3 async operations

File: frontend/src/hooks/useAuth.ts:18-67 Complexity: 3 concurrent async flows (query + 2 mutations) with shared state - currentUser query (enabled conditionally) - signUpMutation (with success → navigate, error → toast, settled → invalidate) - loginMutation (with success → navigate, error → toast) - logout (imperative: remove token + navigate) Risk: Race conditions between login success and currentUser query. The login function stores the token but does not invalidate ['currentUser']. If the query was previously disabled (not logged in), it may not automatically refetch.

3. handleApiError — global error interceptor with side effects

File: frontend/src/main.tsx:21-26 Complexity: Single condition with destructive side effects - Removes access_token from localStorage - Performs window.location.href = "/login" (hard redirect, not React Router) Risk: This fires for every 401/403 across all queries and mutations. A single failed request causes a full page reload and state loss. There is no debouncing or deduplication — if two requests fail simultaneously, the redirect happens twice (harmless but wasteful).

4. EditUser — conditional data stripping before mutation

File: frontend/src/components/Admin/EditUser.tsx:77-83 Complexity: 2 conditional operations on form data - Removes confirm_password from submission data - Deletes password key if it is empty string Risk: If the password field is empty, the API receives no password field at all. The backend must handle this as "do not change password." If the backend treats missing password differently from an explicit null, this could cause unexpected behavior.

5. DeleteUser — dialog state + mutation + dropdown interaction

File: frontend/src/components/Admin/DeleteUser.tsx:27-93 Complexity: 3 interacting state machines - isOpen state (dialog open/closed) - mutation state (idle/pending/success/error) - Dropdown menu onSelect with e.preventDefault() Risk: The onSelect handler calls e.preventDefault() to prevent the dropdown from closing, then sets isOpen(true). If the dropdown behavior changes (e.g., Radix update), the dialog may not open correctly. The onSuccess callback closes the dialog and calls onSuccess prop — if onSuccess throws, the dialog stays open.