🧪 Tester view
Untested behaviours, edge cases, test gap inventory · QA Engineer, SDET, Test Lead
⚡ 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: MEDIUM — onSettled 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.