100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { tokenStorage } from '@/api/tokenStorage';
|
|
import { authApi } from '@/features/auth/api/authApi';
|
|
import { useAuthStore } from '@/features/auth/store/authStore';
|
|
|
|
vi.mock('@/features/auth/api/authApi', () => ({
|
|
authApi: {
|
|
login: vi.fn(),
|
|
logout: vi.fn(),
|
|
refresh: vi.fn(),
|
|
getMe: vi.fn(),
|
|
listUsers: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
const mockedAuthApi = vi.mocked(authApi);
|
|
|
|
describe('authStore', () => {
|
|
beforeEach(() => {
|
|
tokenStorage.clear();
|
|
useAuthStore.setState({ user: null, isInitializing: true });
|
|
vi.clearAllMocks();
|
|
mockedAuthApi.login.mockResolvedValue({
|
|
accessToken: 'access-123',
|
|
refreshToken: 'refresh-123',
|
|
user: { id: 'u1', email: 'learner@aplp.io', name: 'Learner' },
|
|
});
|
|
mockedAuthApi.refresh.mockResolvedValue({
|
|
accessToken: 'access-refreshed',
|
|
refreshToken: 'refresh-refreshed',
|
|
user: { id: 'u1', email: 'learner@aplp.io', name: 'Learner' },
|
|
});
|
|
});
|
|
|
|
it('login stores token and user', async () => {
|
|
await useAuthStore.getState().login('learner', 'secret');
|
|
|
|
expect(mockedAuthApi.login).toHaveBeenCalledWith({
|
|
username: 'learner',
|
|
password: 'secret',
|
|
});
|
|
expect(tokenStorage.get()).toBe('access-123');
|
|
expect(useAuthStore.getState().user?.email).toBe('learner@aplp.io');
|
|
expect(useAuthStore.getState().isInitializing).toBe(false);
|
|
});
|
|
|
|
it('login failure leaves user signed out', async () => {
|
|
mockedAuthApi.login.mockRejectedValue(new Error('invalid credentials'));
|
|
|
|
await expect(useAuthStore.getState().login('learner', 'wrong')).rejects.toThrow(
|
|
'invalid credentials',
|
|
);
|
|
expect(tokenStorage.get()).toBeNull();
|
|
expect(useAuthStore.getState().user).toBeNull();
|
|
});
|
|
|
|
it('logout clears token and user even when API call fails', async () => {
|
|
await useAuthStore.getState().login('learner', 'secret');
|
|
mockedAuthApi.logout.mockRejectedValue(new Error('network'));
|
|
|
|
await useAuthStore.getState().logout();
|
|
|
|
expect(tokenStorage.get()).toBeNull();
|
|
expect(useAuthStore.getState().user).toBeNull();
|
|
});
|
|
|
|
it('init restores session from refresh token', async () => {
|
|
tokenStorage.set('old-access', 'refresh-123');
|
|
|
|
await useAuthStore.getState().init();
|
|
|
|
expect(mockedAuthApi.refresh).toHaveBeenCalledOnce();
|
|
expect(mockedAuthApi.refresh).toHaveBeenCalledWith('refresh-123');
|
|
expect(tokenStorage.get()).toBe('access-refreshed');
|
|
expect(useAuthStore.getState().user?.id).toBe('u1');
|
|
expect(useAuthStore.getState().isInitializing).toBe(false);
|
|
});
|
|
|
|
it('init skips refresh when no refresh token is stored', async () => {
|
|
await useAuthStore.getState().init();
|
|
|
|
expect(mockedAuthApi.refresh).not.toHaveBeenCalled();
|
|
expect(tokenStorage.get()).toBeNull();
|
|
expect(useAuthStore.getState().user).toBeNull();
|
|
expect(useAuthStore.getState().isInitializing).toBe(false);
|
|
});
|
|
|
|
it('init handles expired refresh token gracefully', async () => {
|
|
tokenStorage.set('old-access', 'refresh-expired');
|
|
mockedAuthApi.refresh.mockRejectedValue(new Error('401'));
|
|
|
|
await useAuthStore.getState().init();
|
|
|
|
expect(tokenStorage.get()).toBeNull();
|
|
expect(useAuthStore.getState().user).toBeNull();
|
|
expect(useAuthStore.getState().isInitializing).toBe(false);
|
|
});
|
|
});
|