[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:
senke 2025-12-25 15:55:53 +01:00
parent dfbbc7dfa8
commit 24cf8f0b9d
6 changed files with 929 additions and 7 deletions

View file

@ -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
}
}

View 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();
});
});
});

View 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 },
});
});
});
});

View 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();
});
});
});

View 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();
});
});
});

View file

@ -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
}