feat: add useAuth hook with Supabase Auth session management
Implements useAuth with getSession, onAuthStateChange, login, and logout. All 6 TDD tests pass, using vi.hoisted to resolve Vitest mock hoisting.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
const {
|
||||
mockUnsubscribe,
|
||||
mockGetSession,
|
||||
mockOnAuthStateChange,
|
||||
mockSignInWithPassword,
|
||||
mockSignOut,
|
||||
} = vi.hoisted(() => ({
|
||||
mockUnsubscribe: vi.fn(),
|
||||
mockGetSession: vi.fn(),
|
||||
mockOnAuthStateChange: vi.fn(() => ({
|
||||
data: { subscription: { unsubscribe: vi.fn() } },
|
||||
})),
|
||||
mockSignInWithPassword: vi.fn(),
|
||||
mockSignOut: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../lib/supabase', () => ({
|
||||
supabase: {
|
||||
auth: {
|
||||
getSession: mockGetSession,
|
||||
onAuthStateChange: mockOnAuthStateChange,
|
||||
signInWithPassword: mockSignInWithPassword,
|
||||
signOut: mockSignOut,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { useAuth } from '../useAuth';
|
||||
|
||||
describe('useAuth', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockOnAuthStateChange.mockReturnValue({
|
||||
data: { subscription: { unsubscribe: mockUnsubscribe } },
|
||||
});
|
||||
});
|
||||
|
||||
it('starts with loading=true and session=null', () => {
|
||||
mockGetSession.mockReturnValue(new Promise(() => {}));
|
||||
const { result } = renderHook(() => useAuth());
|
||||
expect(result.current.loading).toBe(true);
|
||||
expect(result.current.session).toBe(null);
|
||||
});
|
||||
|
||||
it('sets loading=false and session after getSession resolves', async () => {
|
||||
const fakeSession = { user: { email: 'admin@test.com' } };
|
||||
mockGetSession.mockResolvedValue({ data: { session: fakeSession } });
|
||||
const { result } = renderHook(() => useAuth());
|
||||
await act(async () => {});
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.session).toEqual(fakeSession);
|
||||
});
|
||||
|
||||
it('login calls signInWithPassword with email and password', async () => {
|
||||
mockGetSession.mockResolvedValue({ data: { session: null } });
|
||||
mockSignInWithPassword.mockResolvedValue({ error: null });
|
||||
const { result } = renderHook(() => useAuth());
|
||||
await act(async () => {
|
||||
await result.current.login('admin@test.com', 'secret');
|
||||
});
|
||||
expect(mockSignInWithPassword).toHaveBeenCalledWith({
|
||||
email: 'admin@test.com',
|
||||
password: 'secret',
|
||||
});
|
||||
});
|
||||
|
||||
it('login returns error when credentials are wrong', async () => {
|
||||
mockGetSession.mockResolvedValue({ data: { session: null } });
|
||||
const fakeError = new Error('Invalid credentials');
|
||||
mockSignInWithPassword.mockResolvedValue({ error: fakeError });
|
||||
const { result } = renderHook(() => useAuth());
|
||||
let loginResult;
|
||||
await act(async () => {
|
||||
loginResult = await result.current.login('bad@test.com', 'wrong');
|
||||
});
|
||||
expect(loginResult.error).toBe(fakeError);
|
||||
});
|
||||
|
||||
it('logout calls signOut', async () => {
|
||||
mockGetSession.mockResolvedValue({ data: { session: null } });
|
||||
mockSignOut.mockResolvedValue({});
|
||||
const { result } = renderHook(() => useAuth());
|
||||
await act(async () => {
|
||||
await result.current.logout();
|
||||
});
|
||||
expect(mockSignOut).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('unsubscribes on unmount', async () => {
|
||||
mockGetSession.mockResolvedValue({ data: { session: null } });
|
||||
const { unmount } = renderHook(() => useAuth());
|
||||
await act(async () => {});
|
||||
unmount();
|
||||
expect(mockUnsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
|
||||
export function useAuth() {
|
||||
const [session, setSession] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setSession(session);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const login = async (email, password) => {
|
||||
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
||||
return { error };
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
await supabase.auth.signOut();
|
||||
};
|
||||
|
||||
return { session, loading, login, logout };
|
||||
}
|
||||
Reference in New Issue
Block a user