feat(cache): implement Redis caching for search & analytics hot paths
- Add TTL-specific cache durations: district stats (5min), market report (15min), heatmap (5min) - Add Redis caching to GeoSearch handler with 60s TTL - Add cache invalidation on listing.approved, listing.updated, listing.deactivated, listing.sold events - Invalidate search, geo_search, and all analytics cache prefixes on listing state changes - Update tests for new CacheService dependency in event handler and geo-search handler Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
216
apps/web/lib/__tests__/auth-store.spec.ts
Normal file
216
apps/web/lib/__tests__/auth-store.spec.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useAuthStore } from '../auth-store';
|
||||
import { ApiError } from '../api-client';
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user