- Convert CacheTTL enum to const object to fix duplicate value errors - Fix import ordering in test files (eslint-disable for vi.mock pattern) - Fix unused variable warnings (prefix with underscore) - Auto-fix import ordering in subscription page, dashboard layout - 0 lint errors remaining Co-Authored-By: Paperclip <noreply@paperclip.ing>
218 lines
6.8 KiB
TypeScript
218 lines
6.8 KiB
TypeScript
/* eslint-disable import-x/order */
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { ApiError } from '../api-client';
|
|
import { useAuthStore } from '../auth-store';
|
|
|
|
// Mock auth-api module
|
|
vi.mock('../auth-api', () => ({
|
|
authApi: {
|
|
login: vi.fn(),
|
|
register: vi.fn(),
|
|
logout: vi.fn(),
|
|
refresh: vi.fn(),
|
|
exchangeToken: vi.fn(),
|
|
getProfile: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// Import mocked module
|
|
import { authApi } from '../auth-api';
|
|
const mockedAuthApi = vi.mocked(authApi);
|
|
|
|
const mockUser = {
|
|
id: '1',
|
|
email: 'test@example.com',
|
|
phone: '0912345678',
|
|
fullName: 'Nguyen Van A',
|
|
avatarUrl: null,
|
|
role: 'user',
|
|
kycStatus: 'pending',
|
|
isActive: true,
|
|
createdAt: '2024-01-01',
|
|
};
|
|
|
|
describe('useAuthStore', () => {
|
|
beforeEach(() => {
|
|
// Reset store state
|
|
useAuthStore.setState({
|
|
user: null,
|
|
isAuthenticated: false,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('initial state', () => {
|
|
it('starts with null user and unauthenticated', () => {
|
|
const state = useAuthStore.getState();
|
|
expect(state.user).toBeNull();
|
|
expect(state.isAuthenticated).toBe(false);
|
|
expect(state.isLoading).toBe(false);
|
|
expect(state.error).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('login', () => {
|
|
it('sets isAuthenticated and fetches profile on success', async () => {
|
|
mockedAuthApi.login.mockResolvedValue({ message: 'ok' });
|
|
mockedAuthApi.getProfile.mockResolvedValue(mockUser);
|
|
|
|
await useAuthStore.getState().login({ phone: '0912345678', password: 'pass123' });
|
|
|
|
const state = useAuthStore.getState();
|
|
expect(state.isAuthenticated).toBe(true);
|
|
expect(state.user).toEqual(mockUser);
|
|
expect(state.isLoading).toBe(false);
|
|
expect(state.error).toBeNull();
|
|
});
|
|
|
|
it('sets error on login failure', async () => {
|
|
mockedAuthApi.login.mockRejectedValue(new ApiError(401, 'Sai mật khẩu'));
|
|
|
|
await expect(
|
|
useAuthStore.getState().login({ phone: '0912345678', password: 'wrong' }),
|
|
).rejects.toThrow();
|
|
|
|
const state = useAuthStore.getState();
|
|
expect(state.isAuthenticated).toBe(false);
|
|
expect(state.error).toBe('Sai mật khẩu');
|
|
expect(state.isLoading).toBe(false);
|
|
});
|
|
|
|
it('uses default error message for non-ApiError', async () => {
|
|
mockedAuthApi.login.mockRejectedValue(new Error('Network error'));
|
|
|
|
await expect(
|
|
useAuthStore.getState().login({ phone: '0912345678', password: 'pass' }),
|
|
).rejects.toThrow();
|
|
|
|
expect(useAuthStore.getState().error).toBe('Đăng nhập thất bại');
|
|
});
|
|
});
|
|
|
|
describe('register', () => {
|
|
it('sets isAuthenticated and fetches profile on success', async () => {
|
|
mockedAuthApi.register.mockResolvedValue({ message: 'ok' });
|
|
mockedAuthApi.getProfile.mockResolvedValue(mockUser);
|
|
|
|
await useAuthStore.getState().register({
|
|
phone: '0912345678',
|
|
password: 'password123',
|
|
fullName: 'Nguyen Van A',
|
|
});
|
|
|
|
const state = useAuthStore.getState();
|
|
expect(state.isAuthenticated).toBe(true);
|
|
expect(state.user).toEqual(mockUser);
|
|
});
|
|
|
|
it('sets error on register failure', async () => {
|
|
mockedAuthApi.register.mockRejectedValue(new ApiError(409, 'Số điện thoại đã tồn tại'));
|
|
|
|
await expect(
|
|
useAuthStore.getState().register({
|
|
phone: '0912345678',
|
|
password: 'pass',
|
|
fullName: 'Test',
|
|
}),
|
|
).rejects.toThrow();
|
|
|
|
expect(useAuthStore.getState().error).toBe('Số điện thoại đã tồn tại');
|
|
});
|
|
});
|
|
|
|
describe('logout', () => {
|
|
it('clears user and auth state', async () => {
|
|
useAuthStore.setState({ user: mockUser, isAuthenticated: true });
|
|
mockedAuthApi.logout.mockResolvedValue({ message: 'ok' });
|
|
|
|
await useAuthStore.getState().logout();
|
|
|
|
const state = useAuthStore.getState();
|
|
expect(state.user).toBeNull();
|
|
expect(state.isAuthenticated).toBe(false);
|
|
});
|
|
|
|
it('clears state even if API logout fails', async () => {
|
|
useAuthStore.setState({ user: mockUser, isAuthenticated: true });
|
|
mockedAuthApi.logout.mockRejectedValue(new Error('Network error'));
|
|
|
|
await useAuthStore.getState().logout();
|
|
|
|
expect(useAuthStore.getState().user).toBeNull();
|
|
expect(useAuthStore.getState().isAuthenticated).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('refreshToken', () => {
|
|
it('returns true and sets authenticated on success', async () => {
|
|
mockedAuthApi.refresh.mockResolvedValue({ message: 'ok' });
|
|
|
|
const result = await useAuthStore.getState().refreshToken();
|
|
|
|
expect(result).toBe(true);
|
|
expect(useAuthStore.getState().isAuthenticated).toBe(true);
|
|
});
|
|
|
|
it('returns false and clears state on failure', async () => {
|
|
useAuthStore.setState({ user: mockUser, isAuthenticated: true });
|
|
mockedAuthApi.refresh.mockRejectedValue(new Error('expired'));
|
|
|
|
const result = await useAuthStore.getState().refreshToken();
|
|
|
|
expect(result).toBe(false);
|
|
expect(useAuthStore.getState().user).toBeNull();
|
|
expect(useAuthStore.getState().isAuthenticated).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('fetchProfile', () => {
|
|
it('fetches and sets user profile', async () => {
|
|
mockedAuthApi.getProfile.mockResolvedValue(mockUser);
|
|
|
|
await useAuthStore.getState().fetchProfile();
|
|
|
|
expect(useAuthStore.getState().user).toEqual(mockUser);
|
|
expect(useAuthStore.getState().isAuthenticated).toBe(true);
|
|
});
|
|
|
|
it('attempts refresh on 401 and retries profile', async () => {
|
|
mockedAuthApi.getProfile
|
|
.mockRejectedValueOnce(new ApiError(401, 'Unauthorized'))
|
|
.mockResolvedValueOnce(mockUser);
|
|
mockedAuthApi.refresh.mockResolvedValue({ message: 'ok' });
|
|
|
|
await useAuthStore.getState().fetchProfile();
|
|
|
|
expect(mockedAuthApi.refresh).toHaveBeenCalled();
|
|
expect(useAuthStore.getState().user).toEqual(mockUser);
|
|
});
|
|
});
|
|
|
|
describe('handleOAuthCallback', () => {
|
|
it('exchanges token and fetches profile', async () => {
|
|
mockedAuthApi.exchangeToken.mockResolvedValue({ message: 'ok' });
|
|
mockedAuthApi.getProfile.mockResolvedValue(mockUser);
|
|
|
|
await useAuthStore.getState().handleOAuthCallback('access', 'refresh', 3600);
|
|
|
|
expect(mockedAuthApi.exchangeToken).toHaveBeenCalledWith('access', 'refresh', 3600);
|
|
expect(useAuthStore.getState().isAuthenticated).toBe(true);
|
|
expect(useAuthStore.getState().user).toEqual(mockUser);
|
|
});
|
|
});
|
|
|
|
describe('clearError', () => {
|
|
it('clears the error state', () => {
|
|
useAuthStore.setState({ error: 'Some error' });
|
|
useAuthStore.getState().clearError();
|
|
expect(useAuthStore.getState().error).toBeNull();
|
|
});
|
|
});
|
|
});
|