- Add frontend unit tests with Vitest for components, hooks, pages, and utilities - Add backend tests for webhooks, notifications, middleware, and edge cases - Add ForgotPassword, NotFound, and ResetPassword pages - Add migration for orphaned staff resources conversion - Add coverage directory to gitignore (generated reports) - Various bug fixes and improvements from previous work 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
336 lines
9.6 KiB
TypeScript
336 lines
9.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
// Mock apiClient
|
|
vi.mock('../client', () => ({
|
|
default: {
|
|
get: vi.fn(),
|
|
post: vi.fn(),
|
|
patch: vi.fn(),
|
|
delete: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
import {
|
|
getProfile,
|
|
updateProfile,
|
|
uploadAvatar,
|
|
deleteAvatar,
|
|
sendVerificationEmail,
|
|
verifyEmail,
|
|
requestEmailChange,
|
|
confirmEmailChange,
|
|
changePassword,
|
|
setupTOTP,
|
|
verifyTOTP,
|
|
disableTOTP,
|
|
getRecoveryCodes,
|
|
regenerateRecoveryCodes,
|
|
getSessions,
|
|
revokeSession,
|
|
revokeOtherSessions,
|
|
getLoginHistory,
|
|
sendPhoneVerification,
|
|
verifyPhoneCode,
|
|
getUserEmails,
|
|
addUserEmail,
|
|
deleteUserEmail,
|
|
sendUserEmailVerification,
|
|
verifyUserEmail,
|
|
setPrimaryEmail,
|
|
} from '../profile';
|
|
import apiClient from '../client';
|
|
|
|
describe('profile API', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('getProfile', () => {
|
|
it('fetches user profile', async () => {
|
|
const mockProfile = {
|
|
id: 1,
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
email_verified: true,
|
|
};
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockProfile });
|
|
|
|
const result = await getProfile();
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/auth/profile/');
|
|
expect(result).toEqual(mockProfile);
|
|
});
|
|
});
|
|
|
|
describe('updateProfile', () => {
|
|
it('updates profile with provided data', async () => {
|
|
const mockUpdated = { id: 1, name: 'Updated Name' };
|
|
vi.mocked(apiClient.patch).mockResolvedValue({ data: mockUpdated });
|
|
|
|
const result = await updateProfile({ name: 'Updated Name' });
|
|
|
|
expect(apiClient.patch).toHaveBeenCalledWith('/auth/profile/', { name: 'Updated Name' });
|
|
expect(result).toEqual(mockUpdated);
|
|
});
|
|
});
|
|
|
|
describe('uploadAvatar', () => {
|
|
it('uploads avatar file', async () => {
|
|
const mockResponse = { avatar_url: 'https://example.com/avatar.jpg' };
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
|
|
|
|
const file = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
|
const result = await uploadAvatar(file);
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith(
|
|
'/auth/profile/avatar/',
|
|
expect.any(FormData),
|
|
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
|
);
|
|
expect(result.avatar_url).toBe('https://example.com/avatar.jpg');
|
|
});
|
|
});
|
|
|
|
describe('deleteAvatar', () => {
|
|
it('deletes user avatar', async () => {
|
|
vi.mocked(apiClient.delete).mockResolvedValue({});
|
|
|
|
await deleteAvatar();
|
|
|
|
expect(apiClient.delete).toHaveBeenCalledWith('/auth/profile/avatar/');
|
|
});
|
|
});
|
|
|
|
describe('email verification', () => {
|
|
it('sends verification email', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({});
|
|
|
|
await sendVerificationEmail();
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/email/verify/send/');
|
|
});
|
|
|
|
it('verifies email with token', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({});
|
|
|
|
await verifyEmail('verification-token');
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/email/verify/confirm/', {
|
|
token: 'verification-token',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('email change', () => {
|
|
it('requests email change', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({});
|
|
|
|
await requestEmailChange('new@example.com');
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/email/change/', {
|
|
new_email: 'new@example.com',
|
|
});
|
|
});
|
|
|
|
it('confirms email change', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({});
|
|
|
|
await confirmEmailChange('change-token');
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/email/change/confirm/', {
|
|
token: 'change-token',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('changePassword', () => {
|
|
it('changes password with current and new', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({});
|
|
|
|
await changePassword('oldPassword', 'newPassword');
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/password/change/', {
|
|
current_password: 'oldPassword',
|
|
new_password: 'newPassword',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('2FA / TOTP', () => {
|
|
it('sets up TOTP', async () => {
|
|
const mockSetup = {
|
|
secret: 'ABCD1234',
|
|
qr_code: 'base64...',
|
|
provisioning_uri: 'otpauth://...',
|
|
};
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockSetup });
|
|
|
|
const result = await setupTOTP();
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/mfa/totp/setup/');
|
|
expect(result.secret).toBe('ABCD1234');
|
|
});
|
|
|
|
it('verifies TOTP code', async () => {
|
|
const mockResponse = { success: true, backup_codes: ['code1', 'code2'] };
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
|
|
|
|
const result = await verifyTOTP('123456');
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/mfa/totp/verify/', { code: '123456' });
|
|
expect(result.success).toBe(true);
|
|
expect(result.recovery_codes).toEqual(['code1', 'code2']);
|
|
});
|
|
|
|
it('disables TOTP', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({});
|
|
|
|
await disableTOTP('123456');
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/mfa/disable/', { mfa_code: '123456' });
|
|
});
|
|
|
|
it('gets recovery codes status', async () => {
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: {} });
|
|
|
|
const result = await getRecoveryCodes();
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/auth/mfa/backup-codes/status/');
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('regenerates recovery codes', async () => {
|
|
const mockCodes = ['code1', 'code2', 'code3'];
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: { backup_codes: mockCodes } });
|
|
|
|
const result = await regenerateRecoveryCodes();
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/mfa/backup-codes/');
|
|
expect(result).toEqual(mockCodes);
|
|
});
|
|
});
|
|
|
|
describe('sessions', () => {
|
|
it('gets sessions', async () => {
|
|
const mockSessions = [
|
|
{ id: '1', device_info: 'Chrome', ip_address: '1.1.1.1', is_current: true },
|
|
];
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockSessions });
|
|
|
|
const result = await getSessions();
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/auth/sessions/');
|
|
expect(result).toEqual(mockSessions);
|
|
});
|
|
|
|
it('revokes session', async () => {
|
|
vi.mocked(apiClient.delete).mockResolvedValue({});
|
|
|
|
await revokeSession('session-123');
|
|
|
|
expect(apiClient.delete).toHaveBeenCalledWith('/auth/sessions/session-123/');
|
|
});
|
|
|
|
it('revokes other sessions', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({});
|
|
|
|
await revokeOtherSessions();
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/sessions/revoke-others/');
|
|
});
|
|
|
|
it('gets login history', async () => {
|
|
const mockHistory = [
|
|
{ id: '1', timestamp: '2024-01-01', success: true },
|
|
];
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockHistory });
|
|
|
|
const result = await getLoginHistory();
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/auth/login-history/');
|
|
expect(result).toEqual(mockHistory);
|
|
});
|
|
});
|
|
|
|
describe('phone verification', () => {
|
|
it('sends phone verification', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({});
|
|
|
|
await sendPhoneVerification('555-1234');
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/phone/verify/send/', {
|
|
phone: '555-1234',
|
|
});
|
|
});
|
|
|
|
it('verifies phone code', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({});
|
|
|
|
await verifyPhoneCode('123456');
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/phone/verify/confirm/', {
|
|
code: '123456',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('multiple emails', () => {
|
|
it('gets user emails', async () => {
|
|
const mockEmails = [
|
|
{ id: 1, email: 'primary@example.com', is_primary: true, verified: true },
|
|
{ id: 2, email: 'secondary@example.com', is_primary: false, verified: false },
|
|
];
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockEmails });
|
|
|
|
const result = await getUserEmails();
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/auth/emails/');
|
|
expect(result).toEqual(mockEmails);
|
|
});
|
|
|
|
it('adds user email', async () => {
|
|
const mockEmail = { id: 3, email: 'new@example.com', is_primary: false, verified: false };
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockEmail });
|
|
|
|
const result = await addUserEmail('new@example.com');
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/emails/', { email: 'new@example.com' });
|
|
expect(result).toEqual(mockEmail);
|
|
});
|
|
|
|
it('deletes user email', async () => {
|
|
vi.mocked(apiClient.delete).mockResolvedValue({});
|
|
|
|
await deleteUserEmail(2);
|
|
|
|
expect(apiClient.delete).toHaveBeenCalledWith('/auth/emails/2/');
|
|
});
|
|
|
|
it('sends user email verification', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({});
|
|
|
|
await sendUserEmailVerification(2);
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/emails/2/send-verification/');
|
|
});
|
|
|
|
it('verifies user email', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({});
|
|
|
|
await verifyUserEmail(2, 'verify-token');
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/emails/2/verify/', {
|
|
token: 'verify-token',
|
|
});
|
|
});
|
|
|
|
it('sets primary email', async () => {
|
|
vi.mocked(apiClient.post).mockResolvedValue({});
|
|
|
|
await setPrimaryEmail(2);
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/auth/emails/2/set-primary/');
|
|
});
|
|
});
|
|
});
|