Skip to content
This repository was archived by the owner on Jun 15, 2026. It is now read-only.
Merged
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
8 changes: 2 additions & 6 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import './styles/custom.scss';
* @returns {React.ReactNode} The rendered component.
*/
function App(): React.ReactNode {
const { signed, checkCurrentAuthUser } = useContext(AuthContext);
const { signed, loading } = useContext(AuthContext);
const [theme, setTheme] = useState(() => {
return localStorage.getItem('theme') ?? 'light';
});
Expand Down Expand Up @@ -114,10 +114,6 @@ function App(): React.ReactNode {

const browserRouter = getBrowserRouter();

useEffect(() => {
checkCurrentAuthUser(window.location.pathname);
}, []);

useEffect(() => {
document.body.setAttribute('data-bs-theme', theme);
localStorage.setItem('theme', theme);
Expand All @@ -138,7 +134,7 @@ function App(): React.ReactNode {
</button>

{/* Your routes or content here */}
<RouterProvider router={browserRouter} />
{!loading && <RouterProvider router={browserRouter} />}
</>
);
};
Expand Down
1 change: 1 addition & 0 deletions client/src/__test__/__mocks__/authContextMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { vi } from "vitest";

const authContextMock = {
signed: true,
loading: false,
user: undefined,
checkCurrentAuthUser: vi.fn(),
signIn: vi.fn(),
Expand Down
48 changes: 46 additions & 2 deletions client/src/__test__/context/AuthProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ vi.mock('../../api-service/api');
const ConsumerComponent: React.FC = () => {
const {
signed,
loading,
user,
signIn,
register,
Expand All @@ -26,6 +27,7 @@ const ConsumerComponent: React.FC = () => {
return (
<div>
<div data-testid="signed">{signed ? 'true' : 'false'}</div>
<div data-testid="loading">{loading ? 'true' : 'false'}</div>
<div data-testid="user">{user ? user.name : 'none'}</div>
<button
data-testid="signIn"
Expand Down Expand Up @@ -89,15 +91,57 @@ describe('AuthProvider', () => {
cleanup();
});

it('should render the default context values', () => {
it('should render the default context values', async () => {
const { getByTestId } = render(
<AuthProvider>
<ConsumerComponent />
</AuthProvider>
);

await waitFor(() => {
expect(getByTestId('signed').textContent).toBe('false');
expect(getByTestId('user').textContent).toBe('none');
});
});

it('should set loading to false after initial auth check with no token', async () => {
// No token in localStorage, so fetchCurrentSession returns immediately.
const { getByTestId } = render(
<AuthProvider>
<ConsumerComponent />
</AuthProvider>
);

await waitFor(() =>
expect(getByTestId('loading').textContent).toBe('false')
);
expect(getByTestId('signed').textContent).toBe('false');
expect(getByTestId('user').textContent).toBe('none');
});

it('should set loading to false and signed to true after successful initial auth check', async () => {
const fakeResponse = {
token: 'refresh-token',
userId: '789',
name: 'Refreshed User',
email: 'refreshed@example.com',
admin: false,
createdAt: new Date().toISOString(),
gravatarImageUrl: 'http://dummyimage.com'
};

vi.spyOn(api, 'getJSON').mockResolvedValue(fakeResponse);
localStorage.setItem(API_TOKEN, 'dummy');

const { getByTestId } = render(
<AuthProvider>
<ConsumerComponent />
</AuthProvider>
);

await waitFor(() =>
expect(getByTestId('loading').textContent).toBe('false')
);
expect(getByTestId('signed').textContent).toBe('true');
});

it('should sign in a user successfully', async () => {
Expand Down
1 change: 1 addition & 0 deletions client/src/context/AuthContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { UserRegistration } from '../types/UserRegistration';

export interface AuthContextData {
signed: boolean;
loading: boolean;
user: UserResponse | undefined;
checkCurrentAuthUser: (pathname: string) => Promise<void>;
signIn: (email: string, password: string) => Promise<string>;
Expand Down
10 changes: 9 additions & 1 deletion client/src/context/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface Props {

const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Props) => {
const [signed, setSigned] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
const [user, setUser] = useState<UserResponse | undefined>();
const [isAdmin, setIsAdmin] = useState<boolean>(false);
const [intervalInstance, setIntervalInstance] = useState<NodeJS.Timeout | null>(null);
Expand Down Expand Up @@ -136,6 +137,12 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro
const minute = second * 60;
const REFRESH_TIMER = minute * 2;

useEffect(() => {
checkCurrentAuthUser(window.location.pathname)
.catch(e => console.error(e))
.finally(() => setLoading(false));
}, []);

useEffect(() => {
if (signed && intervalInstance == null) {
const instance = setInterval(() => {
Expand Down Expand Up @@ -163,14 +170,15 @@ const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Pro

const contextValue: AuthContextData = useMemo(() => ({
signed,
loading,
user,
checkCurrentAuthUser,
signIn,
signOut,
register,
isAdmin,
updateUser
}), [signed, user, checkCurrentAuthUser, signIn, signOut, register, isAdmin, updateUser]);
}), [signed, loading, user, checkCurrentAuthUser, signIn, signOut, register, isAdmin, updateUser]);

return (
<AuthContext.Provider value={contextValue}>
Expand Down
Loading