Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export interface DocInfo {
}

export default function DashboardPage() {
const { user, loading, initialized } = useAuth();
const { user, initialized } = useAuth();
const router = useRouter();

const [documents, setDocuments] = useState<DocInfo[]>([]);
Expand Down
1 change: 0 additions & 1 deletion frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { AuthProvider } from "@/lib/auth";
import { TooltipProvider } from "@/components/ui/tooltip";
import I18nProvider from "@/components/providers/I18nProvider";
import { ThemeProvider } from "@/components/layout/ThemeProvider";
import { Toaster } from "sonner";

const inter = Inter({
variable: "--font-sans",
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/app/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { Brain } from "lucide-react";
import Link from "next/link";
import GoogleSignInButton from "@/components/auth/GoogleSignInButton";
import { PasswordField } from "@/components/auth/PasswordField";
import { isPasswordValid } from "@/lib/password-validation";
import HuggingFaceSignInButton from "@/components/auth/HuggingFaceSignInButton";

export default function RegisterPage() {
Expand All @@ -27,8 +26,7 @@ export default function RegisterPage() {
const [verificationUrl, setVerificationUrl] = useState("");
const [loading, setLoading] = useState(false);

const passwordValid = isPasswordValid(password);
const canSubmit = username.trim().length >= 3 && email.trim().length > 0 && passwordValid && !loading;

// Redirect if already logged in
useEffect(() => {
if (initialized && user) {
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/chat/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@
const connectTimeout = setTimeout(() => {
try {
ws.close();
} catch (e) {
} catch {
// ignore
}
reject(new Error("WebSocket connection timeout"));
Expand Down Expand Up @@ -275,12 +275,12 @@
ws.close();
resolve();
}
} catch (err) {
} catch {
// ignore malformed messages
}
};

ws.onerror = (ev) => {
ws.onerror = () => {
clearTimeout(connectTimeout);
reject(new Error("WebSocket error"));
};
Expand All @@ -291,7 +291,7 @@
});

await wsDone;
} catch (err) {
} catch {
// Fallback to existing SSE stream if WebSocket fails
try {
const stream = api.streamPost("/api/v1/chat/ask/stream", {
Expand Down Expand Up @@ -593,7 +593,7 @@
return () => {
window.removeEventListener("keydown", handleGlobalKeyDown);
};
}, [input, streaming, showHelpModal, showExportMenu, messages]); // Dependencies updated to capture fresh state data

Check warning on line 596 in frontend/src/components/chat/ChatPanel.tsx

View workflow job for this annotation

GitHub Actions / βš›οΈ Frontend β€” TypeScript & Build

React Hook useEffect has missing dependencies: 'handleClear', 'handleSend', 'setInput', 'setIsTyping', 'setStreaming', and 'toggleRecording'. Either include them or remove the dependency array

return (
<div className="h-full flex flex-col relative">
Expand Down
7 changes: 0 additions & 7 deletions frontend/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
} from "@/components/ui/dropdown-menu";
import {
Brain,
Expand All @@ -29,7 +23,6 @@ import {
LogOut,
Menu,
X,
Palette,
Briefcase,
ChevronDown,
Sun,
Expand Down
154 changes: 154 additions & 0 deletions frontend/src/lib/__tests__/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { api, API_BASE, CONNECTION_ERROR_MESSAGE } from '../api';

describe('ApiClient', () => {
let fetchMock: ReturnType<typeof vi.fn>;
let localStorageStore: Record<string, string> = {};

beforeEach(() => {
fetchMock = vi.fn();
global.fetch = fetchMock as unknown as typeof fetch;

localStorageStore = {};
const mockLocalStorage = {
getItem: vi.fn((key: string) => localStorageStore[key] || null),
setItem: vi.fn((key: string, value: string) => {
localStorageStore[key] = value.toString();
}),
removeItem: vi.fn((key: string) => {
delete localStorageStore[key];
}),
clear: vi.fn(() => {
localStorageStore = {};
}),
};
Object.defineProperty(global, 'window', {
value: { localStorage: mockLocalStorage, dispatchEvent: vi.fn(), CustomEvent: class {} },
writable: true,
});
Object.defineProperty(global, 'localStorage', {
value: mockLocalStorage,
writable: true,
});
});

afterEach(() => {
vi.restoreAllMocks();
});

describe('Headers & Auth', () => {
it('should include Authorization header if token exists in localStorage', async () => {
localStorageStore['token'] = 'dummy_auth_token_string';
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ ok: true })));

await api.get('/test');

expect(fetchMock).toHaveBeenCalledWith(
`${API_BASE}/test`,
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
'Content-Type': 'application/json',
'Authorization': 'Bearer dummy_auth_token_string',
})
})
);
});

it('should NOT include Authorization header if token is missing', async () => {
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ ok: true })));

await api.get('/test');

const calls = fetchMock.mock.calls;
const headers = calls[0][1].headers;
expect(headers).not.toHaveProperty('Authorization');
});
});

describe('Parameter Handling', () => {
it('should stringify JSON bodies correctly in POST requests', async () => {
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ success: true })));
const body = { document_id: '123', query: 'hello' };

await api.post('/message', body);

expect(fetchMock).toHaveBeenCalledWith(
`${API_BASE}/message`,
expect.objectContaining({
method: 'POST',
body: JSON.stringify(body)
})
);
});

it('should handle FormData correctly in postForm without overriding Content-Type', async () => {
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ success: true })));
const formData = new FormData();
formData.append('file', new Blob(['test'], { type: 'text/plain' }), 'test.txt');

await api.postForm('/upload', formData);

const callArgs = fetchMock.mock.calls[0];
const reqInit = callArgs[1];

expect(reqInit.method).toBe('POST');
expect(reqInit.body).toBe(formData);
expect(reqInit.headers).not.toHaveProperty('Content-Type');
});
});

describe('Error Handling', () => {
it('should throw connection error message if fetch throws TypeError', async () => {
fetchMock.mockRejectedValueOnce(new TypeError('Failed to fetch'));

await expect(api.get('/test')).rejects.toThrow(CONNECTION_ERROR_MESSAGE);
});

it('should throw parsed error message if response is not ok', async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ detail: 'Invalid document ID' }), {
status: 400,
statusText: 'Bad Request'
})
);

await expect(api.get('/test')).rejects.toThrow('Invalid document ID');
});

it('should fallback to statusText if response has no JSON body', async () => {
fetchMock.mockResolvedValueOnce(
new Response(null, {
status: 500,
statusText: 'Internal Server Error'
})
);

await expect(api.get('/test')).rejects.toThrow('Internal Server Error');
});
});

describe('Token Refresh', () => {
it('should auto-refresh token on 401 response', async () => {
localStorageStore['token'] = 'old_dummy_token';
localStorageStore['refresh_token'] = 'dummy_refresh_string';

// 1st request -> 401
fetchMock.mockResolvedValueOnce(new Response(null, { status: 401 }));

// Refresh request -> 200
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({
access_token: 'new_dummy_token'
})));

// Retry original request -> 200
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ data: 'success' })));

const res = await api.get('/protected');

expect(res).toEqual({ data: 'success' });
expect(localStorageStore['token']).toBe('new_dummy_token');
expect(fetchMock).toHaveBeenCalledTimes(3);
});
});
});
Loading