Replace crypto tests with comprehensive unit tests for authStore, storage, and configuration modules
- Removed outdated `crypto` tests; added dedicated and structured tests for `authStore`, `storage`, and `app.config`. - Enhanced test coverage for user and token validation, secure persistence, state management, and configuration parsing. - Consolidated encryption and storage error handling with thorough validation to ensure SSR-safety and resilience. - Improved runtime validations for tokens and configuration with stricter type checks and fallback mechanisms.
This commit is contained in:
@@ -134,14 +134,17 @@ export async function getTokens(): Promise<TokenStorage | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const decrypted = await decryptData(encrypted);
|
const decrypted = await decryptData(encrypted);
|
||||||
const tokens = JSON.parse(decrypted) as TokenStorage;
|
const parsed = JSON.parse(decrypted);
|
||||||
|
|
||||||
// Validate structure
|
// Validate structure - must have required fields
|
||||||
if (!tokens || typeof tokens !== 'object') {
|
if (!parsed || typeof parsed !== 'object' ||
|
||||||
|
!('accessToken' in parsed) || !('refreshToken' in parsed) ||
|
||||||
|
(parsed.accessToken !== null && typeof parsed.accessToken !== 'string') ||
|
||||||
|
(parsed.refreshToken !== null && typeof parsed.refreshToken !== 'string')) {
|
||||||
throw new Error('Invalid token structure');
|
throw new Error('Invalid token structure');
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokens;
|
return parsed as TokenStorage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to retrieve tokens:', error);
|
console.error('Failed to retrieve tokens:', error);
|
||||||
// If decryption fails, clear invalid data
|
// If decryption fails, clear invalid data
|
||||||
|
|||||||
151
frontend/tests/config/app.config.test.ts
Normal file
151
frontend/tests/config/app.config.test.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* Tests for application configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('App Configuration', () => {
|
||||||
|
let originalEnv: NodeJS.ProcessEnv;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Save original env
|
||||||
|
originalEnv = { ...process.env };
|
||||||
|
|
||||||
|
// Clear module cache to test with different env vars
|
||||||
|
jest.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original env
|
||||||
|
process.env = originalEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseIntSafe', () => {
|
||||||
|
it('should parse valid integers', () => {
|
||||||
|
process.env.NEXT_PUBLIC_API_TIMEOUT = '5000';
|
||||||
|
|
||||||
|
// Dynamically import to pick up new env
|
||||||
|
const { config } = require('@/config/app.config');
|
||||||
|
|
||||||
|
expect(config.api.timeout).toBe(5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default for NaN', () => {
|
||||||
|
process.env.NEXT_PUBLIC_API_TIMEOUT = 'not_a_number';
|
||||||
|
|
||||||
|
const { config } = require('@/config/app.config');
|
||||||
|
|
||||||
|
expect(config.api.timeout).toBe(30000); // default
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enforce minimum values', () => {
|
||||||
|
process.env.NEXT_PUBLIC_API_TIMEOUT = '500'; // Below min of 1000
|
||||||
|
|
||||||
|
const { config } = require('@/config/app.config');
|
||||||
|
|
||||||
|
expect(config.api.timeout).toBe(1000); // minimum
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enforce maximum values', () => {
|
||||||
|
process.env.NEXT_PUBLIC_API_TIMEOUT = '999999'; // Above max of 120000
|
||||||
|
|
||||||
|
const { config } = require('@/config/app.config');
|
||||||
|
|
||||||
|
expect(config.api.timeout).toBe(120000); // maximum
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle negative numbers', () => {
|
||||||
|
process.env.NEXT_PUBLIC_ACCESS_TOKEN_EXPIRY = '-100';
|
||||||
|
|
||||||
|
const { config } = require('@/config/app.config');
|
||||||
|
|
||||||
|
expect(config.auth.accessTokenExpiry).toBe(60000); // minimum
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('URL validation', () => {
|
||||||
|
it('should accept valid URLs', () => {
|
||||||
|
process.env.NEXT_PUBLIC_API_BASE_URL = 'https://api.example.com';
|
||||||
|
|
||||||
|
const { config } = require('@/config/app.config');
|
||||||
|
|
||||||
|
expect(config.api.baseUrl).toBe('https://api.example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on invalid URLs', () => {
|
||||||
|
process.env.NEXT_PUBLIC_API_BASE_URL = 'not a url';
|
||||||
|
|
||||||
|
// Mock window to undefined to prevent validation
|
||||||
|
const originalWindow = global.window;
|
||||||
|
(global as any).window = undefined;
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
require('@/config/app.config');
|
||||||
|
}).toThrow('Invalid URL');
|
||||||
|
|
||||||
|
(global as any).window = originalWindow;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Config validation', () => {
|
||||||
|
it('should validate access token expiry is positive', () => {
|
||||||
|
process.env.NEXT_PUBLIC_ACCESS_TOKEN_EXPIRY = '-1000';
|
||||||
|
|
||||||
|
const originalWindow = global.window;
|
||||||
|
(global as any).window = { document: {} }; // Simulate browser
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
require('@/config/app.config');
|
||||||
|
}).toThrow('Invalid application configuration');
|
||||||
|
|
||||||
|
(global as any).window = originalWindow;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate refresh token expiry > access token expiry', () => {
|
||||||
|
process.env.NEXT_PUBLIC_ACCESS_TOKEN_EXPIRY = '1000000';
|
||||||
|
process.env.NEXT_PUBLIC_REFRESH_TOKEN_EXPIRY = '500000'; // Less than access!
|
||||||
|
|
||||||
|
const originalWindow = global.window;
|
||||||
|
(global as any).window = { document: {} };
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
require('@/config/app.config');
|
||||||
|
}).toThrow('Invalid application configuration');
|
||||||
|
|
||||||
|
(global as any).window = originalWindow;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Boolean parsing', () => {
|
||||||
|
it('should parse "true" as true', () => {
|
||||||
|
process.env.NEXT_PUBLIC_ENABLE_REGISTRATION = 'true';
|
||||||
|
|
||||||
|
const { config } = require('@/config/app.config');
|
||||||
|
|
||||||
|
expect(config.features.enableRegistration).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse anything else as false', () => {
|
||||||
|
process.env.NEXT_PUBLIC_ENABLE_REGISTRATION = 'yes';
|
||||||
|
|
||||||
|
const { config } = require('@/config/app.config');
|
||||||
|
|
||||||
|
expect(config.features.enableRegistration).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default when undefined', () => {
|
||||||
|
delete process.env.NEXT_PUBLIC_ENABLE_REGISTRATION;
|
||||||
|
|
||||||
|
const { config } = require('@/config/app.config');
|
||||||
|
|
||||||
|
expect(config.features.enableRegistration).toBe(true); // default
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Environment detection', () => {
|
||||||
|
it('should detect test environment', () => {
|
||||||
|
// NODE_ENV is set by Jest to 'test'
|
||||||
|
const { config } = require('@/config/app.config');
|
||||||
|
|
||||||
|
expect(config.env.isTest).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* Tests for crypto utilities
|
* Tests for crypto utilities
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { encryptData, decryptData, clearEncryptionKey } from '../crypto';
|
import { encryptData, decryptData, clearEncryptionKey } from '@/lib/auth/crypto';
|
||||||
|
|
||||||
describe('Crypto Utilities', () => {
|
describe('Crypto Utilities', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
141
frontend/tests/lib/auth/storage.test.ts
Normal file
141
frontend/tests/lib/auth/storage.test.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Tests for secure storage module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { saveTokens, getTokens, clearTokens, isStorageAvailable } from '@/lib/auth/storage';
|
||||||
|
|
||||||
|
// Mock crypto functions for testing
|
||||||
|
jest.mock('@/lib/auth/crypto', () => ({
|
||||||
|
encryptData: jest.fn((data: string) => Promise.resolve(`encrypted_${data}`)),
|
||||||
|
decryptData: jest.fn((data: string) => Promise.resolve(data.replace('encrypted_', ''))),
|
||||||
|
clearEncryptionKey: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Storage Module', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isStorageAvailable', () => {
|
||||||
|
it('should return true when localStorage is available', () => {
|
||||||
|
expect(isStorageAvailable()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle quota exceeded errors gracefully', () => {
|
||||||
|
const originalSetItem = Storage.prototype.setItem;
|
||||||
|
Storage.prototype.setItem = jest.fn(() => {
|
||||||
|
throw new Error('QuotaExceededError');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(isStorageAvailable()).toBe(false);
|
||||||
|
|
||||||
|
Storage.prototype.setItem = originalSetItem;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveTokens and getTokens', () => {
|
||||||
|
it('should save and retrieve tokens', async () => {
|
||||||
|
const tokens = {
|
||||||
|
accessToken: 'test.access.token',
|
||||||
|
refreshToken: 'test.refresh.token',
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveTokens(tokens);
|
||||||
|
const retrieved = await getTokens();
|
||||||
|
|
||||||
|
expect(retrieved).toEqual(tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when no tokens are stored', async () => {
|
||||||
|
const result = await getTokens();
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle corrupted data gracefully', async () => {
|
||||||
|
// Manually set invalid encrypted data
|
||||||
|
localStorage.setItem('auth_tokens', 'invalid_encrypted_data');
|
||||||
|
|
||||||
|
const { decryptData } = require('@/lib/auth/crypto');
|
||||||
|
decryptData.mockRejectedValueOnce(new Error('Decryption failed'));
|
||||||
|
|
||||||
|
const result = await getTokens();
|
||||||
|
expect(result).toBeNull();
|
||||||
|
|
||||||
|
// Should clear corrupted data
|
||||||
|
expect(localStorage.getItem('auth_tokens')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate token structure after decryption', async () => {
|
||||||
|
const { decryptData } = require('@/lib/auth/crypto');
|
||||||
|
|
||||||
|
// Mock decryptData to return invalid structure
|
||||||
|
decryptData.mockResolvedValueOnce('not_an_object');
|
||||||
|
|
||||||
|
localStorage.setItem('auth_tokens', 'encrypted_data');
|
||||||
|
|
||||||
|
const result = await getTokens();
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject tokens with missing fields', async () => {
|
||||||
|
const { decryptData } = require('@/lib/auth/crypto');
|
||||||
|
|
||||||
|
// Mock decryptData to return incomplete tokens
|
||||||
|
decryptData.mockResolvedValueOnce(JSON.stringify({ accessToken: 'only_access' }));
|
||||||
|
|
||||||
|
localStorage.setItem('auth_tokens', 'encrypted_data');
|
||||||
|
|
||||||
|
const result = await getTokens();
|
||||||
|
|
||||||
|
// Should still return the object (validation is minimal)
|
||||||
|
expect(result).toEqual({ accessToken: 'only_access' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearTokens', () => {
|
||||||
|
it('should clear all stored tokens', async () => {
|
||||||
|
const tokens = {
|
||||||
|
accessToken: 'test.access.token',
|
||||||
|
refreshToken: 'test.refresh.token',
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveTokens(tokens);
|
||||||
|
expect(localStorage.getItem('auth_tokens')).not.toBeNull();
|
||||||
|
|
||||||
|
await clearTokens();
|
||||||
|
expect(localStorage.getItem('auth_tokens')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw if no tokens exist', async () => {
|
||||||
|
await expect(clearTokens()).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call clearEncryptionKey', async () => {
|
||||||
|
const { clearEncryptionKey } = require('@/lib/auth/crypto');
|
||||||
|
|
||||||
|
await clearTokens();
|
||||||
|
|
||||||
|
expect(clearEncryptionKey).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error handling', () => {
|
||||||
|
it('should throw clear error when localStorage not available', async () => {
|
||||||
|
const originalSetItem = Storage.prototype.setItem;
|
||||||
|
Storage.prototype.setItem = jest.fn(() => {
|
||||||
|
throw new Error('localStorage disabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokens = {
|
||||||
|
accessToken: 'test.access.token',
|
||||||
|
refreshToken: 'test.refresh.token',
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(saveTokens(tokens)).rejects.toThrow('Token storage failed');
|
||||||
|
|
||||||
|
Storage.prototype.setItem = originalSetItem;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
340
frontend/tests/stores/authStore.test.ts
Normal file
340
frontend/tests/stores/authStore.test.ts
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
/**
|
||||||
|
* Tests for auth store
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useAuthStore } from '@/stores/authStore';
|
||||||
|
import * as storage from '@/lib/auth/storage';
|
||||||
|
|
||||||
|
// Mock storage module
|
||||||
|
jest.mock('@/lib/auth/storage');
|
||||||
|
|
||||||
|
describe('Auth Store', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset store state
|
||||||
|
useAuthStore.setState({
|
||||||
|
user: null,
|
||||||
|
accessToken: null,
|
||||||
|
refreshToken: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isLoading: false,
|
||||||
|
tokenExpiresAt: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User validation', () => {
|
||||||
|
it('should reject empty string user ID', async () => {
|
||||||
|
const invalidUser = {
|
||||||
|
id: '',
|
||||||
|
email: 'test@example.com',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
id: ' ',
|
||||||
|
email: 'test@example.com',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: '',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
id: 123,
|
||||||
|
email: 'test@example.com',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
// @ts-expect-error - Testing invalid input
|
||||||
|
useAuthStore.getState().setAuth(
|
||||||
|
invalidUser,
|
||||||
|
'valid.access.token',
|
||||||
|
'valid.refresh.token'
|
||||||
|
)
|
||||||
|
).rejects.toThrow('Invalid user object');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid user', async () => {
|
||||||
|
const validUser = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
(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 = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
(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 = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
(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 = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
(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 = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
(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 = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
(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 = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user