213 lines
7.8 KiB
TypeScript
213 lines
7.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
// import userEvent from '@testing-library/user-event';
|
|
import { AuthProvider, useAuth } from './AuthContext';
|
|
import { tableNameProfiles, tableNameUsers } from '../lib/const';
|
|
|
|
const {
|
|
getSessionMock,
|
|
signOutMock,
|
|
fromMock,
|
|
usersSingleMock,
|
|
profilesLimitMock,
|
|
profilesInsertMock,
|
|
unsubscribeMock,
|
|
tradingAuthState
|
|
} = vi.hoisted(() => ({
|
|
getSessionMock: vi.fn(),
|
|
signOutMock: vi.fn(),
|
|
fromMock: vi.fn(),
|
|
usersSingleMock: vi.fn(),
|
|
profilesLimitMock: vi.fn(),
|
|
profilesInsertMock: vi.fn(),
|
|
unsubscribeMock: vi.fn(),
|
|
tradingAuthState: {
|
|
user: { id: 'user-1', email: 'sarah@example.com', role: 'admin', name: 'Sarah Algo' } as any,
|
|
isLoading: false,
|
|
logout: vi.fn()
|
|
}
|
|
}));
|
|
|
|
vi.mock('../lib/tradingAuth', () => ({
|
|
TradingAuthProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
useTradingAuth: () => tradingAuthState
|
|
}));
|
|
|
|
vi.mock('../lib/supabaseClient', () => ({
|
|
supabase: {
|
|
auth: {
|
|
getSession: getSessionMock,
|
|
signOut: signOutMock
|
|
},
|
|
from: fromMock
|
|
}
|
|
}));
|
|
|
|
const Probe = () => {
|
|
const auth = useAuth();
|
|
return (
|
|
<div>
|
|
<div data-testid="loading">{String(auth.loading)}</div>
|
|
<div data-testid="user">{auth.user?.id || 'none'}</div>
|
|
<div data-testid="role">{auth.profile?.role || 'none'}</div>
|
|
<button onClick={() => void auth.refreshProfile()}>Refresh</button>
|
|
<button onClick={() => void auth.signOut()}>Sign Out</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
describe('AuthContext DOM behavior', () => {
|
|
beforeEach(() => {
|
|
getSessionMock.mockReset();
|
|
signOutMock.mockReset();
|
|
fromMock.mockReset();
|
|
usersSingleMock.mockReset();
|
|
profilesLimitMock.mockReset();
|
|
profilesInsertMock.mockReset();
|
|
unsubscribeMock.mockReset();
|
|
tradingAuthState.user = { id: 'user-1', email: 'sarah@example.com', role: 'admin', name: 'Sarah Algo' };
|
|
tradingAuthState.isLoading = false;
|
|
tradingAuthState.logout.mockReset();
|
|
|
|
signOutMock.mockResolvedValue({ error: null });
|
|
usersSingleMock.mockResolvedValue({
|
|
data: {
|
|
user_id: 'user-1',
|
|
first_name: 'Sarah',
|
|
last_name: 'Algo',
|
|
email: 'sarah@example.com',
|
|
role: 'admin',
|
|
trade_enable: true
|
|
},
|
|
error: null
|
|
});
|
|
profilesLimitMock.mockResolvedValue({ data: [], error: null });
|
|
profilesInsertMock.mockResolvedValue({ error: null });
|
|
|
|
fromMock.mockImplementation((table: string) => {
|
|
if (table === tableNameUsers) {
|
|
const usersBuilder: any = {
|
|
select: vi.fn(() => usersBuilder),
|
|
eq: vi.fn(() => usersBuilder),
|
|
single: usersSingleMock
|
|
};
|
|
return usersBuilder;
|
|
}
|
|
if (table === tableNameProfiles) {
|
|
const profilesBuilder: any = {
|
|
select: vi.fn(() => profilesBuilder),
|
|
eq: vi.fn(() => profilesBuilder),
|
|
limit: profilesLimitMock,
|
|
insert: profilesInsertMock
|
|
};
|
|
return profilesBuilder;
|
|
}
|
|
return {
|
|
select: vi.fn(),
|
|
eq: vi.fn(),
|
|
single: vi.fn(),
|
|
limit: vi.fn(),
|
|
insert: vi.fn()
|
|
};
|
|
});
|
|
});
|
|
|
|
it('loads session/profile, ensures default profile, and cleans up subscription', async () => {
|
|
getSessionMock.mockResolvedValue({
|
|
data: { session: { user: { id: 'user-1' } } }
|
|
});
|
|
const dispatchSpy = vi.spyOn(window, 'dispatchEvent');
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<Probe />
|
|
</AuthProvider>
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loading')).toHaveTextContent('false');
|
|
expect(screen.getByTestId('user')).toHaveTextContent('user-1');
|
|
expect(screen.getByTestId('role')).toHaveTextContent('admin');
|
|
});
|
|
|
|
expect(profilesInsertMock).toHaveBeenCalledTimes(1);
|
|
expect(profilesInsertMock.mock.calls[0][0]).toEqual([
|
|
expect.objectContaining({ user_id: 'user-1', name: 'My First Strategy' })
|
|
]);
|
|
expect(dispatchSpy).toHaveBeenCalled();
|
|
expect((dispatchSpy.mock.calls[0][0] as Event).type).toBe('profiles-updated');
|
|
}, 20000);
|
|
|
|
it('handles no initial session gracefully', async () => {
|
|
tradingAuthState.user = null;
|
|
getSessionMock.mockResolvedValue({ data: { session: null } });
|
|
render(<AuthProvider><Probe /></AuthProvider>);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('loading')).toHaveTextContent('false');
|
|
expect(screen.getByTestId('user')).toHaveTextContent('none');
|
|
});
|
|
expect(usersSingleMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('handles auth state changes with no session', async () => {
|
|
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
|
const { rerender } = render(<AuthProvider><Probe /></AuthProvider>);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('user')).toHaveTextContent('u1');
|
|
});
|
|
|
|
tradingAuthState.user = null;
|
|
rerender(<AuthProvider><Probe /></AuthProvider>);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('user')).toHaveTextContent('none');
|
|
expect(screen.getByTestId('role')).toHaveTextContent('none');
|
|
});
|
|
});
|
|
|
|
it('logs error when profile fetch fails', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
tradingAuthState.user = { id: 'u1', email: 'u1@example.com', role: 'member', name: 'U One' };
|
|
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
|
usersSingleMock.mockResolvedValue({ data: null, error: { message: 'Profile Not Found' } });
|
|
|
|
render(<AuthProvider><Probe /></AuthProvider>);
|
|
|
|
await waitFor(() => {
|
|
expect(consoleSpy).toHaveBeenCalledWith('Error fetching user profile:', expect.objectContaining({ message: 'Profile Not Found' }));
|
|
});
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('handles unexpected errors in fetchProfile', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
tradingAuthState.user = { id: 'u1', email: 'u1@example.com', role: 'member', name: 'U One' };
|
|
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
|
usersSingleMock.mockImplementation(() => { throw new Error('Crashed'); });
|
|
|
|
render(<AuthProvider><Probe /></AuthProvider>);
|
|
|
|
await waitFor(() => {
|
|
expect(consoleSpy).toHaveBeenCalledWith('Unexpected error fetching profile:', expect.any(Error));
|
|
});
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('handles unexpected errors in ensureDefaultProfile', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
tradingAuthState.user = { id: 'u1', email: 'u1@example.com', role: 'member', name: 'U One' };
|
|
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
|
profilesLimitMock.mockImplementation(() => { throw new Error('Limit Error'); });
|
|
|
|
render(<AuthProvider><Probe /></AuthProvider>);
|
|
|
|
await waitFor(() => {
|
|
expect(consoleSpy).toHaveBeenCalledWith('[Auth] Error ensuring default profile:', expect.any(Error));
|
|
});
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|