import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock localStorage BEFORE anything imports auth store // vi.hoisted runs before vi.mock and imports const mockStorage: Record = {}; const mockLocalStorage = { getItem: vi.fn((key: string) => mockStorage[key] || null), setItem: vi.fn((key: string, value: string) => { mockStorage[key] = value; }), removeItem: vi.fn((key: string) => { delete mockStorage[key]; }), clear: vi.fn(() => { Object.keys(mockStorage).forEach(k => delete mockStorage[k]); }), get length() { return Object.keys(mockStorage).length; }, key: vi.fn((i: number) => Object.keys(mockStorage)[i] || null), }; vi.stubGlobal('localStorage', mockLocalStorage); // Mock the api module before importing the store vi.mock('../../src/lib/api', () => ({ authApi: { login: vi.fn(), register: vi.fn(), refreshToken: vi.fn(), getMe: vi.fn(), }, })); import { useAuthStore } from '../../src/store/auth'; import { authApi } from '../../src/lib/api'; const mockedAuthApi = vi.mocked(authApi); describe('Auth Store', () => { beforeEach(() => { Object.keys(mockStorage).forEach(k => delete mockStorage[k]); vi.clearAllMocks(); // Reset store to initial state useAuthStore.setState({ user: null, accessToken: null, refreshToken: null, isAuthenticated: false, isLoading: true, quota: null, }); }); describe('Initial state', () => { it('should have correct default values', () => { const state = useAuthStore.getState(); expect(state.user).toBeNull(); expect(state.isAuthenticated).toBe(false); expect(state.isLoading).toBe(true); expect(state.quota).toBeNull(); }); }); describe('login', () => { it('should login successfully and store tokens', async () => { mockedAuthApi.login.mockResolvedValue({ data: { user: { id: 1, username: 'testuser', email: 'test@test.com', is_staff: false }, tokens: { access: 'access-token-123', refresh: 'refresh-token-456' }, }, } as any); mockedAuthApi.getMe.mockResolvedValue({ data: { id: 1, username: 'testuser', email: 'test@test.com', is_staff: false, quota: { daily_limit: 50, daily_used: 5, monthly_limit: 500, monthly_used: 100 }, }, } as any); await useAuthStore.getState().login('testuser', 'password123'); const state = useAuthStore.getState(); expect(state.user?.username).toBe('testuser'); expect(state.isAuthenticated).toBe(true); expect(state.accessToken).toBe('access-token-123'); expect(state.refreshToken).toBe('refresh-token-456'); expect(mockLocalStorage.setItem).toHaveBeenCalledWith('access_token', 'access-token-123'); expect(mockLocalStorage.setItem).toHaveBeenCalledWith('refresh_token', 'refresh-token-456'); }); it('should call authApi.login with correct args', async () => { mockedAuthApi.login.mockResolvedValue({ data: { user: { id: 1, username: 'u', email: 'e', is_staff: false }, tokens: { access: 'a', refresh: 'r' }, }, } as any); mockedAuthApi.getMe.mockResolvedValue({ data: { id: 1, username: 'u', email: 'e', is_staff: false, quota: { daily_limit: 50, daily_used: 0, monthly_limit: 500, monthly_used: 0 } }, } as any); await useAuthStore.getState().login('myuser', 'mypass'); expect(mockedAuthApi.login).toHaveBeenCalledWith('myuser', 'mypass'); }); it('should throw on login failure', async () => { mockedAuthApi.login.mockRejectedValue(new Error('Invalid credentials')); await expect(useAuthStore.getState().login('bad', 'cred')).rejects.toThrow('Invalid credentials'); expect(useAuthStore.getState().isAuthenticated).toBe(false); }); it('should fetch user info (quota) after login', async () => { mockedAuthApi.login.mockResolvedValue({ data: { user: { id: 1, username: 'u', email: 'e', is_staff: false }, tokens: { access: 'a', refresh: 'r' }, }, } as any); mockedAuthApi.getMe.mockResolvedValue({ data: { id: 1, username: 'u', email: 'e', is_staff: false, quota: { daily_limit: 50, daily_used: 10, monthly_limit: 500, monthly_used: 200 }, }, } as any); await useAuthStore.getState().login('u', 'p'); expect(mockedAuthApi.getMe).toHaveBeenCalled(); expect(useAuthStore.getState().quota?.daily_used).toBe(10); }); }); describe('register', () => { it('should register and auto-login', async () => { mockedAuthApi.register.mockResolvedValue({ data: { user: { id: 2, username: 'newuser', email: 'new@test.com', is_staff: false }, tokens: { access: 'new-access', refresh: 'new-refresh' }, }, } as any); mockedAuthApi.getMe.mockResolvedValue({ data: { id: 2, username: 'newuser', email: 'new@test.com', is_staff: false, quota: { daily_limit: 50, daily_used: 0, monthly_limit: 500, monthly_used: 0 }, }, } as any); await useAuthStore.getState().register('newuser', 'new@test.com', 'pass123'); expect(mockedAuthApi.register).toHaveBeenCalledWith('newuser', 'new@test.com', 'pass123'); expect(useAuthStore.getState().isAuthenticated).toBe(true); expect(useAuthStore.getState().user?.username).toBe('newuser'); expect(mockLocalStorage.setItem).toHaveBeenCalledWith('access_token', 'new-access'); }); it('should throw on register failure', async () => { mockedAuthApi.register.mockRejectedValue(new Error('Username taken')); await expect(useAuthStore.getState().register('taken', 'e@e.com', 'p')).rejects.toThrow(); }); }); describe('logout', () => { it('should clear all auth state and localStorage', () => { useAuthStore.setState({ user: { id: 1, username: 'u', email: 'e', is_staff: false }, accessToken: 'tok', refreshToken: 'ref', isAuthenticated: true, quota: { daily_limit: 50, daily_used: 5, monthly_limit: 500, monthly_used: 100 }, }); useAuthStore.getState().logout(); const state = useAuthStore.getState(); expect(state.user).toBeNull(); expect(state.accessToken).toBeNull(); expect(state.refreshToken).toBeNull(); expect(state.isAuthenticated).toBe(false); expect(state.quota).toBeNull(); expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('access_token'); expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('refresh_token'); }); }); describe('refreshAccessToken', () => { it('should refresh access token using refresh token', async () => { useAuthStore.setState({ refreshToken: 'valid-refresh' }); mockedAuthApi.refreshToken.mockResolvedValue({ data: { access: 'new-access-token' } } as any); await useAuthStore.getState().refreshAccessToken(); expect(mockedAuthApi.refreshToken).toHaveBeenCalledWith('valid-refresh'); expect(useAuthStore.getState().accessToken).toBe('new-access-token'); expect(mockLocalStorage.setItem).toHaveBeenCalledWith('access_token', 'new-access-token'); }); it('should throw when no refresh token exists', async () => { useAuthStore.setState({ refreshToken: null }); await expect(useAuthStore.getState().refreshAccessToken()).rejects.toThrow('No refresh token'); }); }); describe('fetchUserInfo', () => { it('should fetch and set user info and quota', async () => { mockedAuthApi.getMe.mockResolvedValue({ data: { id: 1, username: 'test', email: 'test@test.com', is_staff: true, quota: { daily_limit: 100, daily_used: 25, monthly_limit: 1000, monthly_used: 300 }, }, } as any); await useAuthStore.getState().fetchUserInfo(); const state = useAuthStore.getState(); expect(state.user?.username).toBe('test'); expect(state.user?.is_staff).toBe(true); expect(state.isAuthenticated).toBe(true); expect(state.quota?.daily_limit).toBe(100); expect(state.quota?.daily_used).toBe(25); }); it('should logout on API failure (invalid token)', async () => { useAuthStore.setState({ user: { id: 1, username: 'u', email: 'e', is_staff: false }, isAuthenticated: true, }); mockedAuthApi.getMe.mockRejectedValue(new Error('401')); await useAuthStore.getState().fetchUserInfo(); expect(useAuthStore.getState().isAuthenticated).toBe(false); expect(useAuthStore.getState().user).toBeNull(); }); }); describe('initialize', () => { it('should set isLoading to false after initialization with no token', async () => { mockLocalStorage.getItem.mockReturnValue(null); await useAuthStore.getState().initialize(); expect(useAuthStore.getState().isLoading).toBe(false); expect(mockedAuthApi.getMe).not.toHaveBeenCalled(); }); it('should fetch user info when token exists in localStorage', async () => { mockLocalStorage.getItem.mockReturnValue('stored-access-token'); mockedAuthApi.getMe.mockResolvedValue({ data: { id: 1, username: 'stored', email: 's@t.com', is_staff: false, quota: { daily_limit: 50, daily_used: 0, monthly_limit: 500, monthly_used: 0 }, }, } as any); await useAuthStore.getState().initialize(); expect(useAuthStore.getState().isLoading).toBe(false); expect(useAuthStore.getState().user?.username).toBe('stored'); }); it('should logout if token is invalid during initialization', async () => { mockLocalStorage.getItem.mockReturnValue('expired-token'); mockedAuthApi.getMe.mockRejectedValue(new Error('401')); await useAuthStore.getState().initialize(); expect(useAuthStore.getState().isLoading).toBe(false); expect(useAuthStore.getState().isAuthenticated).toBe(false); }); }); });