**Authentication Refactor:** Remove authStore and its associated tests, transitioning to the new authentication model. Add dynamic loading for PasswordResetConfirmForm to optimize performance. Include a theme initialization script in layout.tsx to prevent FOUC.
This commit is contained in:
@@ -25,7 +25,7 @@ let mockAuthState: {
|
||||
refreshToken: null,
|
||||
};
|
||||
|
||||
jest.mock('@/stores/authStore', () => ({
|
||||
jest.mock('@/lib/stores/authStore', () => ({
|
||||
useAuthStore: (selector?: (state: any) => any) => {
|
||||
if (selector) {
|
||||
return selector(mockAuthState);
|
||||
|
||||
426
frontend/tests/lib/stores/authStore.test.ts
Normal file
426
frontend/tests/lib/stores/authStore.test.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
/**
|
||||
* Tests for auth store
|
||||
*/
|
||||
|
||||
import { useAuthStore, type User } from '@/lib/stores/authStore';
|
||||
import * as storage from '@/lib/auth/storage';
|
||||
|
||||
// Mock storage module
|
||||
jest.mock('@/lib/auth/storage');
|
||||
|
||||
/**
|
||||
* Helper to create mock user object with all required fields
|
||||
*/
|
||||
function createMockUser(overrides: Partial<User> = {}): User {
|
||||
return {
|
||||
id: 'user-123',
|
||||
email: 'test@example.com',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
phone_number: null,
|
||||
is_active: true,
|
||||
is_superuser: false,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Auth Store', () => {
|
||||
beforeEach(() => {
|
||||
// Reset store state
|
||||
useAuthStore.setState({
|
||||
user: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
tokenExpiresAt: null,
|
||||
});
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Reset storage mocks to default successful implementations
|
||||
(storage.saveTokens as jest.Mock).mockResolvedValue(undefined);
|
||||
(storage.getTokens as jest.Mock).mockResolvedValue(null);
|
||||
(storage.clearTokens as jest.Mock).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
describe('User validation', () => {
|
||||
it('should reject empty string user ID', async () => {
|
||||
const invalidUser = createMockUser({ id: '' });
|
||||
|
||||
await expect(
|
||||
useAuthStore.getState().setAuth(
|
||||
invalidUser,
|
||||
'valid.access.token',
|
||||
'valid.refresh.token'
|
||||
)
|
||||
).rejects.toThrow('Invalid user object');
|
||||
});
|
||||
|
||||
it('should reject whitespace-only user ID', async () => {
|
||||
const invalidUser = createMockUser({ id: ' ' });
|
||||
|
||||
await expect(
|
||||
useAuthStore.getState().setAuth(
|
||||
invalidUser,
|
||||
'valid.access.token',
|
||||
'valid.refresh.token'
|
||||
)
|
||||
).rejects.toThrow('Invalid user object');
|
||||
});
|
||||
|
||||
it('should reject empty string email', async () => {
|
||||
const invalidUser = createMockUser({ email: '' });
|
||||
|
||||
await expect(
|
||||
useAuthStore.getState().setAuth(
|
||||
invalidUser,
|
||||
'valid.access.token',
|
||||
'valid.refresh.token'
|
||||
)
|
||||
).rejects.toThrow('Invalid user object');
|
||||
});
|
||||
|
||||
it('should reject non-string user ID', async () => {
|
||||
const invalidUser = createMockUser({ id: 123 as any });
|
||||
|
||||
await expect(
|
||||
useAuthStore.getState().setAuth(
|
||||
invalidUser as any, // Testing runtime validation with invalid type
|
||||
'valid.access.token',
|
||||
'valid.refresh.token'
|
||||
)
|
||||
).rejects.toThrow('Invalid user object');
|
||||
});
|
||||
|
||||
it('should accept valid user', async () => {
|
||||
const validUser = createMockUser();
|
||||
|
||||
(storage.saveTokens as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
await expect(
|
||||
useAuthStore.getState().setAuth(
|
||||
validUser,
|
||||
'header.payload.signature',
|
||||
'header.payload.signature'
|
||||
)
|
||||
).resolves.not.toThrow();
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
expect(state.user).toEqual(validUser);
|
||||
expect(state.isAuthenticated).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Token validation', () => {
|
||||
it('should reject invalid JWT format (not 3 parts)', async () => {
|
||||
const validUser = createMockUser();
|
||||
|
||||
await expect(
|
||||
useAuthStore.getState().setAuth(
|
||||
validUser,
|
||||
'invalid.token', // Only 2 parts
|
||||
'header.payload.signature'
|
||||
)
|
||||
).rejects.toThrow('Invalid token format');
|
||||
});
|
||||
|
||||
it('should reject JWT with empty parts', async () => {
|
||||
const validUser = createMockUser();
|
||||
|
||||
await expect(
|
||||
useAuthStore.getState().setAuth(
|
||||
validUser,
|
||||
'header..signature', // Empty payload
|
||||
'header.payload.signature'
|
||||
)
|
||||
).rejects.toThrow('Invalid token format');
|
||||
});
|
||||
|
||||
it('should accept valid JWT format', async () => {
|
||||
const validUser = createMockUser();
|
||||
|
||||
(storage.saveTokens as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
await expect(
|
||||
useAuthStore.getState().setAuth(
|
||||
validUser,
|
||||
'header.payload.signature',
|
||||
'header.payload.signature'
|
||||
)
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Token expiry calculation', () => {
|
||||
it('should reject negative expiresIn', async () => {
|
||||
const validUser = createMockUser();
|
||||
|
||||
(storage.saveTokens as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// Should not throw, but should use default
|
||||
await useAuthStore.getState().setAuth(
|
||||
validUser,
|
||||
'header.payload.signature',
|
||||
'header.payload.signature',
|
||||
-1 // Negative!
|
||||
);
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
const expectedExpiry = Date.now() + 900 * 1000; // Should use default 900s
|
||||
|
||||
// Allow 1 second tolerance
|
||||
expect(state.tokenExpiresAt).toBeGreaterThan(expectedExpiry - 1000);
|
||||
expect(state.tokenExpiresAt).toBeLessThan(expectedExpiry + 1000);
|
||||
});
|
||||
|
||||
it('should reject zero expiresIn', async () => {
|
||||
const validUser = createMockUser();
|
||||
|
||||
(storage.saveTokens as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
await useAuthStore.getState().setAuth(
|
||||
validUser,
|
||||
'header.payload.signature',
|
||||
'header.payload.signature',
|
||||
0 // Zero!
|
||||
);
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
const expectedExpiry = Date.now() + 900 * 1000;
|
||||
|
||||
expect(state.tokenExpiresAt).toBeGreaterThan(expectedExpiry - 1000);
|
||||
expect(state.tokenExpiresAt).toBeLessThan(expectedExpiry + 1000);
|
||||
});
|
||||
|
||||
it('should reject excessively large expiresIn', async () => {
|
||||
const validUser = createMockUser();
|
||||
|
||||
(storage.saveTokens as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
await useAuthStore.getState().setAuth(
|
||||
validUser,
|
||||
'header.payload.signature',
|
||||
'header.payload.signature',
|
||||
99999999 // Way too large!
|
||||
);
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
const expectedExpiry = Date.now() + 900 * 1000; // Should use default
|
||||
|
||||
expect(state.tokenExpiresAt).toBeGreaterThan(expectedExpiry - 1000);
|
||||
expect(state.tokenExpiresAt).toBeLessThan(expectedExpiry + 1000);
|
||||
});
|
||||
|
||||
it('should accept valid expiresIn', async () => {
|
||||
const validUser = createMockUser();
|
||||
|
||||
(storage.saveTokens as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
await useAuthStore.getState().setAuth(
|
||||
validUser,
|
||||
'header.payload.signature',
|
||||
'header.payload.signature',
|
||||
3600 // 1 hour
|
||||
);
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
const expectedExpiry = Date.now() + 3600 * 1000;
|
||||
|
||||
expect(state.tokenExpiresAt).toBeGreaterThan(expectedExpiry - 1000);
|
||||
expect(state.tokenExpiresAt).toBeLessThan(expectedExpiry + 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTokenExpired', () => {
|
||||
it('should return true when no expiry set', () => {
|
||||
expect(useAuthStore.getState().isTokenExpired()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when token is expired', () => {
|
||||
useAuthStore.setState({
|
||||
tokenExpiresAt: Date.now() - 1000, // 1 second ago
|
||||
});
|
||||
|
||||
expect(useAuthStore.getState().isTokenExpired()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when token is still valid', () => {
|
||||
useAuthStore.setState({
|
||||
tokenExpiresAt: Date.now() + 10000, // 10 seconds from now
|
||||
});
|
||||
|
||||
expect(useAuthStore.getState().isTokenExpired()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearAuth', () => {
|
||||
it('should clear all auth state', async () => {
|
||||
(storage.saveTokens as jest.Mock).mockResolvedValue(undefined);
|
||||
(storage.clearTokens as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
// First set auth
|
||||
const validUser = createMockUser();
|
||||
|
||||
await useAuthStore.getState().setAuth(
|
||||
validUser,
|
||||
'header.payload.signature',
|
||||
'header.payload.signature'
|
||||
);
|
||||
|
||||
expect(useAuthStore.getState().isAuthenticated).toBe(true);
|
||||
|
||||
// Then clear
|
||||
await useAuthStore.getState().clearAuth();
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
expect(state.user).toBeNull();
|
||||
expect(state.accessToken).toBeNull();
|
||||
expect(state.refreshToken).toBeNull();
|
||||
expect(state.isAuthenticated).toBe(false);
|
||||
expect(state.tokenExpiresAt).toBeNull();
|
||||
|
||||
expect(storage.clearTokens).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not throw if clearTokens fails', async () => {
|
||||
(storage.clearTokens as jest.Mock).mockRejectedValue(new Error('Storage error'));
|
||||
|
||||
await expect(useAuthStore.getState().clearAuth()).resolves.not.toThrow();
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
expect(state.isAuthenticated).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setTokens', () => {
|
||||
it('should update tokens while preserving user state', async () => {
|
||||
// First set initial auth with user
|
||||
await useAuthStore.getState().setAuth(
|
||||
createMockUser({ id: 'user-1' }),
|
||||
'old.access.token',
|
||||
'old.refresh.token'
|
||||
);
|
||||
|
||||
const oldUser = useAuthStore.getState().user;
|
||||
|
||||
// Now update just the tokens
|
||||
await useAuthStore.getState().setTokens(
|
||||
'new.access.token',
|
||||
'new.refresh.token',
|
||||
900
|
||||
);
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
expect(state.accessToken).toBe('new.access.token');
|
||||
expect(state.refreshToken).toBe('new.refresh.token');
|
||||
expect(state.user).toBe(oldUser); // User should remain unchanged
|
||||
expect(state.tokenExpiresAt).toBeGreaterThan(Date.now());
|
||||
});
|
||||
|
||||
it('should reject invalid access token in setTokens', async () => {
|
||||
await expect(
|
||||
useAuthStore.getState().setTokens('invalid', 'valid.refresh.token', 900)
|
||||
).rejects.toThrow('Invalid token format');
|
||||
});
|
||||
|
||||
it('should reject invalid refresh token in setTokens', async () => {
|
||||
await expect(
|
||||
useAuthStore.getState().setTokens('valid.access.token', 'invalid', 900)
|
||||
).rejects.toThrow('Invalid token format');
|
||||
});
|
||||
|
||||
it('should throw if storage fails in setTokens', async () => {
|
||||
(storage.saveTokens as jest.Mock).mockRejectedValue(new Error('Storage error'));
|
||||
|
||||
await expect(
|
||||
useAuthStore.getState().setTokens('valid.access.token', 'valid.refresh.token', 900)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUser', () => {
|
||||
it('should update user while preserving auth state', async () => {
|
||||
// First set initial auth
|
||||
await useAuthStore.getState().setAuth(
|
||||
createMockUser({ id: 'user-1' }),
|
||||
'valid.access.token',
|
||||
'valid.refresh.token'
|
||||
);
|
||||
|
||||
const oldToken = useAuthStore.getState().accessToken;
|
||||
|
||||
// Update just the user
|
||||
const newUser = createMockUser({ id: 'user-1', email: 'updated@example.com', is_superuser: true });
|
||||
useAuthStore.getState().setUser(newUser);
|
||||
|
||||
const state = useAuthStore.getState();
|
||||
expect(state.user).toEqual(newUser);
|
||||
expect(state.accessToken).toBe(oldToken); // Tokens unchanged
|
||||
});
|
||||
|
||||
it('should reject null user', () => {
|
||||
expect(() => {
|
||||
useAuthStore.getState().setUser(null as any);
|
||||
}).toThrow('Invalid user object');
|
||||
});
|
||||
|
||||
it('should reject user with empty id', () => {
|
||||
expect(() => {
|
||||
useAuthStore.getState().setUser(createMockUser({ id: '' }));
|
||||
}).toThrow('Invalid user object');
|
||||
});
|
||||
|
||||
it('should reject user with whitespace-only id', () => {
|
||||
expect(() => {
|
||||
useAuthStore.getState().setUser(createMockUser({ id: ' ' }));
|
||||
}).toThrow('Invalid user object');
|
||||
});
|
||||
|
||||
it('should reject user with non-string email', () => {
|
||||
expect(() => {
|
||||
useAuthStore.getState().setUser(createMockUser({ id: 'user-1', email: 123 as any }));
|
||||
}).toThrow('Invalid user object');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadAuthFromStorage (deprecated)', () => {
|
||||
it('should log deprecation warning', async () => {
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
|
||||
await useAuthStore.getState().loadAuthFromStorage();
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'loadAuthFromStorage() is deprecated and no longer necessary'
|
||||
);
|
||||
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeAuth (deprecated)', () => {
|
||||
it('should log deprecation warning', async () => {
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
|
||||
const { initializeAuth } = await import('@/lib/stores/authStore');
|
||||
await initializeAuth();
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'initializeAuth() is deprecated and no longer necessary'
|
||||
);
|
||||
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not throw', async () => {
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
|
||||
const { initializeAuth } = await import('@/lib/stores/authStore');
|
||||
await expect(initializeAuth()).resolves.not.toThrow();
|
||||
|
||||
consoleWarnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user