[FE-TEST-001] fe-test: Add unit tests for API services
- Created comprehensive unit tests for marketplaceService (11 tests) - Created comprehensive unit tests for profileService (12 tests) - Created comprehensive unit tests for avatarService (9 tests) - Created comprehensive unit tests for 2fa-service (8 tests) - All 40 tests pass successfully - Tests cover success cases, error handling, edge cases, and validation scenarios Files modified: - apps/web/src/services/marketplaceService.test.ts (new) - apps/web/src/features/profile/services/profileService.test.ts (new) - apps/web/src/features/profile/services/avatarService.test.ts (new) - apps/web/src/services/2fa-service.test.ts (new) - VEZA_COMPLETE_MVP_TODOLIST.json
This commit is contained in:
parent
dfbbc7dfa8
commit
24cf8f0b9d
6 changed files with 929 additions and 7 deletions
|
|
@ -9628,7 +9628,7 @@
|
|||
"description": "Test all API service functions",
|
||||
"owner": "frontend",
|
||||
"estimated_hours": 8,
|
||||
"status": "todo",
|
||||
"status": "completed",
|
||||
"files_involved": [],
|
||||
"implementation_steps": [
|
||||
{
|
||||
|
|
@ -9649,7 +9649,18 @@
|
|||
"Unit tests",
|
||||
"Integration tests"
|
||||
],
|
||||
"notes": ""
|
||||
"notes": "",
|
||||
"completion": {
|
||||
"completed_at": "2025-12-25T14:55:51.412242Z",
|
||||
"implementation_notes": "Created comprehensive unit tests for API services. Added test suites for marketplaceService (11 tests), profileService (12 tests), avatarService (9 tests), and 2fa-service (8 tests). All tests pass successfully. Tests cover success cases, error handling, edge cases, and validation scenarios. Services tested include product fetching/creation, order management, profile operations, avatar upload/delete, and two-factor authentication.",
|
||||
"files_modified": [
|
||||
"apps/web/src/services/marketplaceService.test.ts",
|
||||
"apps/web/src/features/profile/services/profileService.test.ts",
|
||||
"apps/web/src/features/profile/services/avatarService.test.ts",
|
||||
"apps/web/src/services/2fa-service.test.ts"
|
||||
],
|
||||
"validation": "All 40 tests pass successfully"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "FE-TEST-002",
|
||||
|
|
@ -11955,10 +11966,10 @@
|
|||
"in_progress": 0,
|
||||
"todo": 121,
|
||||
"blocked": 0,
|
||||
"last_updated": "2025-12-25T14:53:11.254128Z",
|
||||
"completion_percentage": 88.76,
|
||||
"last_updated": "2025-12-25T14:55:51.412282Z",
|
||||
"completion_percentage": 89.14,
|
||||
"total_tasks": 267,
|
||||
"completed_tasks": 237,
|
||||
"remaining_tasks": 30
|
||||
"completed_tasks": 238,
|
||||
"remaining_tasks": 29
|
||||
}
|
||||
}
|
||||
184
apps/web/src/features/profile/services/avatarService.test.ts
Normal file
184
apps/web/src/features/profile/services/avatarService.test.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
uploadAvatar,
|
||||
deleteAvatar,
|
||||
AvatarUploadError,
|
||||
} from './avatarService';
|
||||
import { apiClient } from '@/services/api/client';
|
||||
|
||||
// Mock apiClient
|
||||
vi.mock('@/services/api/client', () => ({
|
||||
apiClient: {
|
||||
post: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedApiClient = apiClient as {
|
||||
post: ReturnType<typeof vi.fn>;
|
||||
delete: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
describe('avatarService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('uploadAvatar', () => {
|
||||
it('should upload avatar successfully', async () => {
|
||||
const mockFile = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
||||
const mockResponse = {
|
||||
avatar_url: 'https://example.com/avatar.jpg',
|
||||
};
|
||||
|
||||
mockedApiClient.post.mockResolvedValue({
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
const result = await uploadAvatar('user-1', mockFile);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockedApiClient.post).toHaveBeenCalledWith(
|
||||
'/users/user-1/avatar',
|
||||
expect.any(FormData),
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onProgress callback during upload', async () => {
|
||||
const mockFile = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
||||
const mockResponse = {
|
||||
avatar_url: 'https://example.com/avatar.jpg',
|
||||
};
|
||||
const onProgress = vi.fn();
|
||||
|
||||
mockedApiClient.post.mockImplementation((url, data, config) => {
|
||||
// Simulate progress
|
||||
if (config?.onUploadProgress) {
|
||||
config.onUploadProgress({
|
||||
loaded: 50,
|
||||
total: 100,
|
||||
} as any);
|
||||
}
|
||||
return Promise.resolve({ data: mockResponse });
|
||||
});
|
||||
|
||||
await uploadAvatar('user-1', mockFile, onProgress);
|
||||
|
||||
expect(onProgress).toHaveBeenCalledWith(50);
|
||||
});
|
||||
|
||||
it('should throw AvatarUploadError with VALIDATION code on 400 error', async () => {
|
||||
const mockFile = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
||||
const mockError = new AxiosError('Validation failed');
|
||||
mockError.response = {
|
||||
status: 400,
|
||||
data: { error: 'Invalid file format' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.post.mockRejectedValue(mockError);
|
||||
|
||||
await expect(uploadAvatar('user-1', mockFile)).rejects.toThrow(
|
||||
AvatarUploadError,
|
||||
);
|
||||
await expect(uploadAvatar('user-1', mockFile)).rejects.toMatchObject({
|
||||
code: 'VALIDATION',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw AvatarUploadError with VALIDATION code on 413 error', async () => {
|
||||
const mockFile = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
||||
const mockError = new AxiosError('File too large');
|
||||
mockError.response = {
|
||||
status: 413,
|
||||
data: {},
|
||||
} as any;
|
||||
|
||||
mockedApiClient.post.mockRejectedValue(mockError);
|
||||
|
||||
await expect(uploadAvatar('user-1', mockFile)).rejects.toThrow(
|
||||
AvatarUploadError,
|
||||
);
|
||||
await expect(uploadAvatar('user-1', mockFile)).rejects.toMatchObject({
|
||||
code: 'VALIDATION',
|
||||
message: 'Fichier trop volumineux (max 5MB)',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw AvatarUploadError with SERVER code on 500+ error', async () => {
|
||||
const mockFile = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
||||
const mockError = new AxiosError('Server error');
|
||||
mockError.response = {
|
||||
status: 500,
|
||||
data: { error: 'Internal server error' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.post.mockRejectedValue(mockError);
|
||||
|
||||
await expect(uploadAvatar('user-1', mockFile)).rejects.toThrow(
|
||||
AvatarUploadError,
|
||||
);
|
||||
await expect(uploadAvatar('user-1', mockFile)).rejects.toMatchObject({
|
||||
code: 'SERVER',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw AvatarUploadError with NETWORK code on network error', async () => {
|
||||
const mockFile = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
||||
const mockError = new AxiosError('Network error');
|
||||
mockError.request = {};
|
||||
|
||||
mockedApiClient.post.mockRejectedValue(mockError);
|
||||
|
||||
await expect(uploadAvatar('user-1', mockFile)).rejects.toThrow(
|
||||
AvatarUploadError,
|
||||
);
|
||||
await expect(uploadAvatar('user-1', mockFile)).rejects.toMatchObject({
|
||||
code: 'NETWORK',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw AvatarUploadError with UNKNOWN code on unknown error', async () => {
|
||||
const mockFile = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
||||
|
||||
mockedApiClient.post.mockRejectedValue(new Error('Unknown error'));
|
||||
|
||||
await expect(uploadAvatar('user-1', mockFile)).rejects.toThrow(
|
||||
AvatarUploadError,
|
||||
);
|
||||
await expect(uploadAvatar('user-1', mockFile)).rejects.toMatchObject({
|
||||
code: 'UNKNOWN',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAvatar', () => {
|
||||
it('should delete avatar successfully', async () => {
|
||||
mockedApiClient.delete.mockResolvedValue({ data: {} });
|
||||
|
||||
await deleteAvatar('user-1');
|
||||
|
||||
expect(mockedApiClient.delete).toHaveBeenCalledWith(
|
||||
'/users/user-1/avatar',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error on delete failure', async () => {
|
||||
const mockError = new AxiosError('Delete failed');
|
||||
mockError.response = {
|
||||
status: 404,
|
||||
data: { error: 'Avatar not found' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.delete.mockRejectedValue(mockError);
|
||||
|
||||
await expect(deleteAvatar('user-1')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
303
apps/web/src/features/profile/services/profileService.test.ts
Normal file
303
apps/web/src/features/profile/services/profileService.test.ts
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
getProfile,
|
||||
getProfileByUsername,
|
||||
updateProfile,
|
||||
calculateProfileCompletion,
|
||||
followUser,
|
||||
unfollowUser,
|
||||
getFollowers,
|
||||
getFollowing,
|
||||
type UserProfile,
|
||||
type UpdateProfileRequest,
|
||||
} from './profileService';
|
||||
import { apiClient } from '@/services/api/client';
|
||||
|
||||
// Mock apiClient
|
||||
vi.mock('@/services/api/client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
put: vi.fn(),
|
||||
post: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedApiClient = apiClient as {
|
||||
get: ReturnType<typeof vi.fn>;
|
||||
put: ReturnType<typeof vi.fn>;
|
||||
post: ReturnType<typeof vi.fn>;
|
||||
delete: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
describe('profileService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getProfile', () => {
|
||||
it('should fetch user profile by ID', async () => {
|
||||
const mockProfile: UserProfile = {
|
||||
id: 'user-1',
|
||||
username: 'testuser',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
avatar_url: 'https://example.com/avatar.jpg',
|
||||
bio: 'Test bio',
|
||||
location: 'Test Location',
|
||||
birthdate: '1990-01-01',
|
||||
gender: 'male',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
followers_count: 10,
|
||||
following_count: 5,
|
||||
};
|
||||
|
||||
mockedApiClient.get.mockResolvedValue({
|
||||
data: { profile: mockProfile },
|
||||
});
|
||||
|
||||
const result = await getProfile('user-1');
|
||||
|
||||
expect(result).toEqual(mockProfile);
|
||||
expect(mockedApiClient.get).toHaveBeenCalledWith('/users/user-1');
|
||||
});
|
||||
|
||||
it('should throw error on fetch failure', async () => {
|
||||
const mockError = new AxiosError('Fetch failed');
|
||||
mockError.response = {
|
||||
status: 404,
|
||||
data: { error: 'User not found' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.get.mockRejectedValue(mockError);
|
||||
|
||||
await expect(getProfile('invalid-user')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProfileByUsername', () => {
|
||||
it('should fetch user profile by username', async () => {
|
||||
const mockProfile: UserProfile = {
|
||||
id: 'user-1',
|
||||
username: 'testuser',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
avatar_url: null,
|
||||
bio: null,
|
||||
location: null,
|
||||
birthdate: null,
|
||||
gender: null,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
mockedApiClient.get.mockResolvedValue({
|
||||
data: { profile: mockProfile },
|
||||
});
|
||||
|
||||
const result = await getProfileByUsername('testuser');
|
||||
|
||||
expect(result).toEqual(mockProfile);
|
||||
expect(mockedApiClient.get).toHaveBeenCalledWith(
|
||||
'/users/by-username/testuser',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle response without profile wrapper', async () => {
|
||||
const mockProfile: UserProfile = {
|
||||
id: 'user-1',
|
||||
username: 'testuser',
|
||||
first_name: 'Test',
|
||||
last_name: 'User',
|
||||
avatar_url: null,
|
||||
bio: null,
|
||||
location: null,
|
||||
birthdate: null,
|
||||
gender: null,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
mockedApiClient.get.mockResolvedValue({
|
||||
data: mockProfile,
|
||||
});
|
||||
|
||||
const result = await getProfileByUsername('testuser');
|
||||
|
||||
expect(result).toEqual(mockProfile);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateProfile', () => {
|
||||
it('should update user profile successfully', async () => {
|
||||
const updateData: UpdateProfileRequest = {
|
||||
first_name: 'Updated',
|
||||
last_name: 'Name',
|
||||
bio: 'Updated bio',
|
||||
};
|
||||
|
||||
const mockUpdatedProfile: UserProfile = {
|
||||
id: 'user-1',
|
||||
username: 'testuser',
|
||||
first_name: 'Updated',
|
||||
last_name: 'Name',
|
||||
avatar_url: null,
|
||||
bio: 'Updated bio',
|
||||
location: null,
|
||||
birthdate: null,
|
||||
gender: null,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
mockedApiClient.put.mockResolvedValue({
|
||||
data: { profile: mockUpdatedProfile },
|
||||
});
|
||||
|
||||
const result = await updateProfile('user-1', updateData);
|
||||
|
||||
expect(result).toEqual(mockUpdatedProfile);
|
||||
expect(mockedApiClient.put).toHaveBeenCalledWith(
|
||||
'/users/user-1',
|
||||
updateData,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error on update failure', async () => {
|
||||
const mockError = new AxiosError('Update failed');
|
||||
mockError.response = {
|
||||
status: 400,
|
||||
data: { error: 'Invalid data' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.put.mockRejectedValue(mockError);
|
||||
|
||||
await expect(
|
||||
updateProfile('user-1', { username: 'invalid' }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateProfileCompletion', () => {
|
||||
it('should calculate profile completion', async () => {
|
||||
const mockCompletion = {
|
||||
percentage: 75,
|
||||
missing: ['bio', 'location'],
|
||||
};
|
||||
|
||||
mockedApiClient.get.mockResolvedValue({
|
||||
data: mockCompletion,
|
||||
});
|
||||
|
||||
const result = await calculateProfileCompletion('user-1');
|
||||
|
||||
expect(result).toEqual(mockCompletion);
|
||||
expect(mockedApiClient.get).toHaveBeenCalledWith(
|
||||
'/users/user-1/completion',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('followUser', () => {
|
||||
it('should follow a user successfully', async () => {
|
||||
const mockResponse = {
|
||||
message: 'User followed successfully',
|
||||
is_following: true,
|
||||
};
|
||||
|
||||
mockedApiClient.post.mockResolvedValue({
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
const result = await followUser('user-2');
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockedApiClient.post).toHaveBeenCalledWith('/users/user-2/follow');
|
||||
});
|
||||
|
||||
it('should throw error on follow failure', async () => {
|
||||
const mockError = new AxiosError('Follow failed');
|
||||
mockError.response = {
|
||||
status: 400,
|
||||
data: { error: 'Cannot follow yourself' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.post.mockRejectedValue(mockError);
|
||||
|
||||
await expect(followUser('user-1')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unfollowUser', () => {
|
||||
it('should unfollow a user successfully', async () => {
|
||||
const mockResponse = {
|
||||
message: 'User unfollowed successfully',
|
||||
is_following: false,
|
||||
};
|
||||
|
||||
mockedApiClient.delete.mockResolvedValue({
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
const result = await unfollowUser('user-2');
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockedApiClient.delete).toHaveBeenCalledWith(
|
||||
'/users/user-2/follow',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFollowers', () => {
|
||||
it('should fetch user followers', async () => {
|
||||
const mockResponse = {
|
||||
followers: [
|
||||
{
|
||||
id: 'follower-1',
|
||||
username: 'follower1',
|
||||
avatar_url: 'https://example.com/avatar.jpg',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
};
|
||||
|
||||
mockedApiClient.get.mockResolvedValue({
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
const result = await getFollowers('user-1', 1, 20);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockedApiClient.get).toHaveBeenCalledWith('/users/user-1/followers', {
|
||||
params: { page: 1, limit: 20 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFollowing', () => {
|
||||
it('should fetch user following', async () => {
|
||||
const mockResponse = {
|
||||
following: [
|
||||
{
|
||||
id: 'following-1',
|
||||
username: 'following1',
|
||||
avatar_url: 'https://example.com/avatar.jpg',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
};
|
||||
|
||||
mockedApiClient.get.mockResolvedValue({
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
const result = await getFollowing('user-1', 1, 20);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockedApiClient.get).toHaveBeenCalledWith('/users/user-1/following', {
|
||||
params: { page: 1, limit: 20 },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
150
apps/web/src/services/2fa-service.test.ts
Normal file
150
apps/web/src/services/2fa-service.test.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AxiosError } from 'axios';
|
||||
import { twoFactorService } from './2fa-service';
|
||||
import { apiClient } from './api/client';
|
||||
import { requireFeature } from '@/config/features';
|
||||
|
||||
// Mock apiClient
|
||||
vi.mock('./api/client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock feature config
|
||||
vi.mock('@/config/features', () => ({
|
||||
requireFeature: vi.fn(),
|
||||
FEATURES: {
|
||||
TWO_FACTOR_AUTH: true,
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedApiClient = apiClient as {
|
||||
get: ReturnType<typeof vi.fn>;
|
||||
post: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
describe('twoFactorService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getStatus', () => {
|
||||
it('should get 2FA status successfully', async () => {
|
||||
const mockStatus = { enabled: true };
|
||||
|
||||
mockedApiClient.get.mockResolvedValue({
|
||||
data: mockStatus,
|
||||
});
|
||||
|
||||
const result = await twoFactorService.getStatus();
|
||||
|
||||
expect(result).toEqual(mockStatus);
|
||||
expect(requireFeature).toHaveBeenCalledWith('TWO_FACTOR_AUTH');
|
||||
expect(mockedApiClient.get).toHaveBeenCalledWith('/auth/2fa/status');
|
||||
});
|
||||
|
||||
it('should throw error on status fetch failure', async () => {
|
||||
const mockError = new AxiosError('Status fetch failed');
|
||||
mockError.response = {
|
||||
status: 500,
|
||||
data: { error: 'Internal server error' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.get.mockRejectedValue(mockError);
|
||||
|
||||
await expect(twoFactorService.getStatus()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setup', () => {
|
||||
it('should setup 2FA successfully', async () => {
|
||||
const mockSetupResponse = {
|
||||
secret: 'JBSWY3DPEHPK3PXP',
|
||||
qr_code_url: 'https://example.com/qr.png',
|
||||
recovery_codes: ['code1', 'code2', 'code3'],
|
||||
};
|
||||
|
||||
mockedApiClient.post.mockResolvedValue({
|
||||
data: mockSetupResponse,
|
||||
});
|
||||
|
||||
const result = await twoFactorService.setup();
|
||||
|
||||
expect(result).toEqual(mockSetupResponse);
|
||||
expect(requireFeature).toHaveBeenCalledWith('TWO_FACTOR_AUTH');
|
||||
expect(mockedApiClient.post).toHaveBeenCalledWith('/auth/2fa/setup');
|
||||
});
|
||||
|
||||
it('should throw error on setup failure', async () => {
|
||||
const mockError = new AxiosError('Setup failed');
|
||||
mockError.response = {
|
||||
status: 400,
|
||||
data: { error: '2FA already enabled' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.post.mockRejectedValue(mockError);
|
||||
|
||||
await expect(twoFactorService.setup()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('verify', () => {
|
||||
it('should verify 2FA code successfully', async () => {
|
||||
mockedApiClient.post.mockResolvedValue({
|
||||
data: { message: '2FA enabled successfully' },
|
||||
});
|
||||
|
||||
await twoFactorService.verify('JBSWY3DPEHPK3PXP', '123456');
|
||||
|
||||
expect(requireFeature).toHaveBeenCalledWith('TWO_FACTOR_AUTH');
|
||||
expect(mockedApiClient.post).toHaveBeenCalledWith('/auth/2fa/verify', {
|
||||
secret: 'JBSWY3DPEHPK3PXP',
|
||||
code: '123456',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error on verification failure', async () => {
|
||||
const mockError = new AxiosError('Verification failed');
|
||||
mockError.response = {
|
||||
status: 400,
|
||||
data: { error: 'Invalid code' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.post.mockRejectedValue(mockError);
|
||||
|
||||
await expect(
|
||||
twoFactorService.verify('JBSWY3DPEHPK3PXP', 'invalid'),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('disable', () => {
|
||||
it('should disable 2FA successfully', async () => {
|
||||
mockedApiClient.post.mockResolvedValue({
|
||||
data: { message: '2FA disabled successfully' },
|
||||
});
|
||||
|
||||
await twoFactorService.disable('password123');
|
||||
|
||||
expect(requireFeature).toHaveBeenCalledWith('TWO_FACTOR_AUTH');
|
||||
expect(mockedApiClient.post).toHaveBeenCalledWith('/auth/2fa/disable', {
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error on disable failure', async () => {
|
||||
const mockError = new AxiosError('Disable failed');
|
||||
mockError.response = {
|
||||
status: 401,
|
||||
data: { error: 'Invalid password' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.post.mockRejectedValue(mockError);
|
||||
|
||||
await expect(twoFactorService.disable('wrongpassword')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
274
apps/web/src/services/marketplaceService.test.ts
Normal file
274
apps/web/src/services/marketplaceService.test.ts
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AxiosError } from 'axios';
|
||||
import { marketplaceService } from './marketplaceService';
|
||||
import { apiClient } from './api/client';
|
||||
import type { Product, Order, ProductStatus } from '../types/marketplace';
|
||||
|
||||
// Mock apiClient
|
||||
vi.mock('./api/client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedApiClient = apiClient as {
|
||||
get: ReturnType<typeof vi.fn>;
|
||||
post: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
describe('marketplaceService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('fetchProducts', () => {
|
||||
it('should fetch products without filters', async () => {
|
||||
const mockProducts: Product[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Test Product',
|
||||
description: 'Test Description',
|
||||
price: 10.99,
|
||||
status: 'active' as ProductStatus,
|
||||
seller_id: 'seller-1',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
mockedApiClient.get.mockResolvedValue({
|
||||
data: {
|
||||
products: mockProducts,
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total_pages: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await marketplaceService.fetchProducts();
|
||||
|
||||
expect(result.products).toEqual(mockProducts);
|
||||
expect(result.total).toBe(1);
|
||||
expect(mockedApiClient.get).toHaveBeenCalledWith('/marketplace/products');
|
||||
});
|
||||
|
||||
it('should fetch products with filters', async () => {
|
||||
const mockProducts: Product[] = [];
|
||||
|
||||
mockedApiClient.get.mockResolvedValue({
|
||||
data: {
|
||||
products: mockProducts,
|
||||
total: 0,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total_pages: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await marketplaceService.fetchProducts(
|
||||
{
|
||||
status: 'active' as ProductStatus,
|
||||
seller_id: 'seller-1',
|
||||
min_price: 10,
|
||||
max_price: 100,
|
||||
search: 'test',
|
||||
},
|
||||
{ page: 1, limit: 20 },
|
||||
);
|
||||
|
||||
expect(mockedApiClient.get).toHaveBeenCalledWith(
|
||||
'/marketplace/products?status=active&seller_id=seller-1&min_price=10&max_price=100&search=test&page=1&limit=20',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle array response format (backward compatibility)', async () => {
|
||||
const mockProducts: Product[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Test Product',
|
||||
description: 'Test Description',
|
||||
price: 10.99,
|
||||
status: 'active' as ProductStatus,
|
||||
seller_id: 'seller-1',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
mockedApiClient.get.mockResolvedValue({
|
||||
data: mockProducts,
|
||||
});
|
||||
|
||||
const result = await marketplaceService.fetchProducts();
|
||||
|
||||
expect(result.products).toEqual(mockProducts);
|
||||
expect(result.total).toBe(1);
|
||||
expect(result.page).toBe(1);
|
||||
});
|
||||
|
||||
it('should throw error on fetch failure', async () => {
|
||||
const mockError = new AxiosError('Fetch failed');
|
||||
mockError.response = {
|
||||
status: 500,
|
||||
data: { error: 'Internal server error' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.get.mockRejectedValue(mockError);
|
||||
|
||||
await expect(marketplaceService.fetchProducts()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProduct', () => {
|
||||
it('should create a product successfully', async () => {
|
||||
const mockProduct: Product = {
|
||||
id: '1',
|
||||
name: 'New Product',
|
||||
description: 'New Description',
|
||||
price: 19.99,
|
||||
status: 'active' as ProductStatus,
|
||||
seller_id: 'seller-1',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
mockedApiClient.post.mockResolvedValue({
|
||||
data: mockProduct,
|
||||
});
|
||||
|
||||
const result = await marketplaceService.createProduct({
|
||||
name: 'New Product',
|
||||
description: 'New Description',
|
||||
price: 19.99,
|
||||
product_type: 'track',
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockProduct);
|
||||
expect(mockedApiClient.post).toHaveBeenCalledWith(
|
||||
'/marketplace/products',
|
||||
{
|
||||
name: 'New Product',
|
||||
description: 'New Description',
|
||||
price: 19.99,
|
||||
product_type: 'track',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error on creation failure', async () => {
|
||||
const mockError = new AxiosError('Creation failed');
|
||||
mockError.response = {
|
||||
status: 403,
|
||||
data: { error: 'Permission denied' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.post.mockRejectedValue(mockError);
|
||||
|
||||
await expect(
|
||||
marketplaceService.createProduct({
|
||||
name: 'New Product',
|
||||
description: 'New Description',
|
||||
price: 19.99,
|
||||
product_type: 'track',
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createOrder', () => {
|
||||
it('should create an order successfully', async () => {
|
||||
const mockOrder: Order = {
|
||||
id: 'order-1',
|
||||
user_id: 'user-1',
|
||||
items: [{ product_id: 'product-1', quantity: 1 }],
|
||||
total: 19.99,
|
||||
status: 'pending',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
mockedApiClient.post.mockResolvedValue({
|
||||
data: mockOrder,
|
||||
});
|
||||
|
||||
const result = await marketplaceService.createOrder([
|
||||
{ product_id: 'product-1' },
|
||||
]);
|
||||
|
||||
expect(result).toEqual(mockOrder);
|
||||
expect(mockedApiClient.post).toHaveBeenCalledWith(
|
||||
'/marketplace/orders',
|
||||
{ items: [{ product_id: 'product-1' }] },
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error on order creation failure', async () => {
|
||||
const mockError = new AxiosError('Order creation failed');
|
||||
mockError.response = {
|
||||
status: 400,
|
||||
data: { error: 'Invalid product' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.post.mockRejectedValue(mockError);
|
||||
|
||||
await expect(
|
||||
marketplaceService.createOrder([{ product_id: 'invalid-product' }]),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('purchaseProduct', () => {
|
||||
it('should purchase a single product successfully', async () => {
|
||||
const mockOrder: Order = {
|
||||
id: 'order-1',
|
||||
user_id: 'user-1',
|
||||
items: [{ product_id: 'product-1', quantity: 1 }],
|
||||
total: 19.99,
|
||||
status: 'pending',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
mockedApiClient.post.mockResolvedValue({
|
||||
data: mockOrder,
|
||||
});
|
||||
|
||||
const result = await marketplaceService.purchaseProduct('product-1');
|
||||
|
||||
expect(result).toEqual(mockOrder);
|
||||
expect(mockedApiClient.post).toHaveBeenCalledWith(
|
||||
'/marketplace/orders',
|
||||
{ items: [{ product_id: 'product-1' }] },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDownloadLink', () => {
|
||||
it('should get download link successfully', async () => {
|
||||
const mockResponse = { url: 'https://example.com/download/product-1' };
|
||||
|
||||
mockedApiClient.get.mockResolvedValue({
|
||||
data: mockResponse,
|
||||
});
|
||||
|
||||
const result = await marketplaceService.getDownloadLink('product-1');
|
||||
|
||||
expect(result).toBe('https://example.com/download/product-1');
|
||||
expect(mockedApiClient.get).toHaveBeenCalledWith(
|
||||
'/marketplace/download/product-1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error on download link failure', async () => {
|
||||
const mockError = new AxiosError('Download link failed');
|
||||
mockError.response = {
|
||||
status: 403,
|
||||
data: { error: 'No license found' },
|
||||
} as any;
|
||||
|
||||
mockedApiClient.get.mockRejectedValue(mockError);
|
||||
|
||||
await expect(
|
||||
marketplaceService.getDownloadLink('product-1'),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ type APIRouter struct {
|
|||
config *config.Config
|
||||
engine *gin.Engine
|
||||
logger *zap.Logger
|
||||
versionManager *VersionManager // BE-SVC-019: API versioning manager
|
||||
versionManager *VersionManager // BE-SVC-019: API versioning manager
|
||||
monitoringService *services.MonitoringAlertingService // INT-021: API monitoring and alerting
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue