diff --git a/VEZA_COMPLETE_MVP_TODOLIST.json b/VEZA_COMPLETE_MVP_TODOLIST.json index e9ec4ddef..9aa581320 100644 --- a/VEZA_COMPLETE_MVP_TODOLIST.json +++ b/VEZA_COMPLETE_MVP_TODOLIST.json @@ -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 } } \ No newline at end of file diff --git a/apps/web/src/features/profile/services/avatarService.test.ts b/apps/web/src/features/profile/services/avatarService.test.ts new file mode 100644 index 000000000..9d312f70b --- /dev/null +++ b/apps/web/src/features/profile/services/avatarService.test.ts @@ -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; + delete: ReturnType; +}; + +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(); + }); + }); +}); + diff --git a/apps/web/src/features/profile/services/profileService.test.ts b/apps/web/src/features/profile/services/profileService.test.ts new file mode 100644 index 000000000..c7192e7c7 --- /dev/null +++ b/apps/web/src/features/profile/services/profileService.test.ts @@ -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; + put: ReturnType; + post: ReturnType; + delete: ReturnType; +}; + +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 }, + }); + }); + }); +}); + diff --git a/apps/web/src/services/2fa-service.test.ts b/apps/web/src/services/2fa-service.test.ts new file mode 100644 index 000000000..dd8ec3ec7 --- /dev/null +++ b/apps/web/src/services/2fa-service.test.ts @@ -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; + post: ReturnType; +}; + +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(); + }); + }); +}); + diff --git a/apps/web/src/services/marketplaceService.test.ts b/apps/web/src/services/marketplaceService.test.ts new file mode 100644 index 000000000..6720bb6ad --- /dev/null +++ b/apps/web/src/services/marketplaceService.test.ts @@ -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; + post: ReturnType; +}; + +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(); + }); + }); +}); + diff --git a/veza-backend-api/internal/api/router.go b/veza-backend-api/internal/api/router.go index bfe3be687..7f4bd871b 100644 --- a/veza-backend-api/internal/api/router.go +++ b/veza-backend-api/internal/api/router.go @@ -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 }