refactoring-backend #3
@@ -1,31 +1,3 @@
|
|||||||
|
|
||||||
### TASK-STATE-01 — Auth Errors Silently Swallowed in `useRegexTester` and `useLogPreview`
|
|
||||||
|
|
||||||
**Where found**
|
|
||||||
`frontend/src/hooks/useRegexTester.ts` and `frontend/src/hooks/useLogPreview.ts`. Each hook's catch block handles errors only as `err instanceof Error`, which catches `ApiError` (a subclass of `Error`), but does not call `handleFetchError`. The `handleFetchError` utility calls `isAuthError` and returns early without setting UI error text, allowing the global `SESSION_EXPIRED_EVENT` listener in `AuthProvider` to handle the redirect. By bypassing `handleFetchError`, auth errors (401/403) are displayed as UI error text (`err.message`) instead of triggering the session expiry flow.
|
|
||||||
|
|
||||||
**Goal**
|
|
||||||
Replace the ad-hoc catch blocks in both hooks with `handleFetchError`:
|
|
||||||
```ts
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (signal.aborted) return;
|
|
||||||
handleFetchError(err, setError, "Regex test failed");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
`handleFetchError` already imports `isAuthError` and returns early for auth errors, allowing the global handler to take over.
|
|
||||||
|
|
||||||
**Possible traps and issues**
|
|
||||||
- Confirm that `SESSION_EXPIRED_EVENT` is being listened to in `AuthProvider` and that it triggers navigation to `/login`. It is — this was verified in the architecture review.
|
|
||||||
- After the fix, a 401 during a regex test will show no error text (the auth handler navigates away), which is correct behaviour.
|
|
||||||
|
|
||||||
**Docs changes needed**
|
|
||||||
Add a convention to `Docs/Web-Development.md`: "All hook catch blocks must use `handleFetchError` rather than directly calling `setError`, so auth errors are routed to the session-expiry flow."
|
|
||||||
|
|
||||||
**Why this is needed**
|
|
||||||
When a session expires while the user is in the Regex Tester, they see a confusing "Unauthorized" error message in the tester UI instead of being cleanly redirected to the login page.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### TASK-STATE-02 — `withRefresh` in `useJailList` Creates New Function References Every Render
|
### TASK-STATE-02 — `withRefresh` in `useJailList` Creates New Function References Every Render
|
||||||
|
|
||||||
**Where found**
|
**Where found**
|
||||||
|
|||||||
54
frontend/src/hooks/__tests__/useJailList.test.ts
Normal file
54
frontend/src/hooks/__tests__/useJailList.test.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { renderHook } from "@testing-library/react";
|
||||||
|
import { useJails } from "../useJailList";
|
||||||
|
|
||||||
|
// Mock the API calls
|
||||||
|
vi.mock("../../api/jails", () => ({
|
||||||
|
fetchJails: vi.fn().mockResolvedValue({
|
||||||
|
jails: [],
|
||||||
|
total: 0,
|
||||||
|
}),
|
||||||
|
startJail: vi.fn(),
|
||||||
|
stopJail: vi.fn(),
|
||||||
|
setJailIdle: vi.fn(),
|
||||||
|
reloadJail: vi.fn(),
|
||||||
|
reloadAllJails: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../utils/fetchError", () => ({
|
||||||
|
handleFetchError: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("useJails", () => {
|
||||||
|
it("returns stable function references between renders", () => {
|
||||||
|
const { result, rerender } = renderHook(() => useJails());
|
||||||
|
|
||||||
|
const firstRender = {
|
||||||
|
startJail: result.current.startJail,
|
||||||
|
stopJail: result.current.stopJail,
|
||||||
|
setIdle: result.current.setIdle,
|
||||||
|
reloadJail: result.current.reloadJail,
|
||||||
|
reloadAll: result.current.reloadAll,
|
||||||
|
refresh: result.current.refresh,
|
||||||
|
};
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
const secondRender = {
|
||||||
|
startJail: result.current.startJail,
|
||||||
|
stopJail: result.current.stopJail,
|
||||||
|
setIdle: result.current.setIdle,
|
||||||
|
reloadJail: result.current.reloadJail,
|
||||||
|
reloadAll: result.current.reloadAll,
|
||||||
|
refresh: result.current.refresh,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function references should be the same between renders
|
||||||
|
expect(firstRender.startJail).toBe(secondRender.startJail);
|
||||||
|
expect(firstRender.stopJail).toBe(secondRender.stopJail);
|
||||||
|
expect(firstRender.setIdle).toBe(secondRender.setIdle);
|
||||||
|
expect(firstRender.reloadJail).toBe(secondRender.reloadJail);
|
||||||
|
expect(firstRender.reloadAll).toBe(secondRender.reloadAll);
|
||||||
|
expect(firstRender.refresh).toBe(secondRender.refresh);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -70,12 +70,45 @@ export function useJails(): UseJailsResult {
|
|||||||
};
|
};
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
const withRefresh =
|
const startJailMemo = useCallback(
|
||||||
(fn: (name: string) => Promise<unknown>) =>
|
|
||||||
async (name: string): Promise<void> => {
|
async (name: string): Promise<void> => {
|
||||||
await fn(name);
|
await startJail(name);
|
||||||
load();
|
load();
|
||||||
};
|
},
|
||||||
|
[load],
|
||||||
|
);
|
||||||
|
|
||||||
|
const stopJailMemo = useCallback(
|
||||||
|
async (name: string): Promise<void> => {
|
||||||
|
await stopJail(name);
|
||||||
|
load();
|
||||||
|
},
|
||||||
|
[load],
|
||||||
|
);
|
||||||
|
|
||||||
|
const reloadJailMemo = useCallback(
|
||||||
|
async (name: string): Promise<void> => {
|
||||||
|
await reloadJail(name);
|
||||||
|
load();
|
||||||
|
},
|
||||||
|
[load],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setIdleMemo = useCallback(
|
||||||
|
(name: string, on: boolean): Promise<void> =>
|
||||||
|
setJailIdle(name, on).then(() => {
|
||||||
|
load();
|
||||||
|
}),
|
||||||
|
[load],
|
||||||
|
);
|
||||||
|
|
||||||
|
const reloadAllMemo = useCallback(
|
||||||
|
(): Promise<void> =>
|
||||||
|
reloadAllJails().then(() => {
|
||||||
|
load();
|
||||||
|
}),
|
||||||
|
[load],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
jails,
|
jails,
|
||||||
@@ -83,14 +116,10 @@ export function useJails(): UseJailsResult {
|
|||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
refresh: load,
|
refresh: load,
|
||||||
startJail: withRefresh(startJail),
|
startJail: startJailMemo,
|
||||||
stopJail: withRefresh(stopJail),
|
stopJail: stopJailMemo,
|
||||||
setIdle: (name, on) => setJailIdle(name, on).then(() => {
|
setIdle: setIdleMemo,
|
||||||
load();
|
reloadJail: reloadJailMemo,
|
||||||
}),
|
reloadAll: reloadAllMemo,
|
||||||
reloadJail: withRefresh(reloadJail),
|
|
||||||
reloadAll: () => reloadAllJails().then(() => {
|
|
||||||
load();
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user