470 lines
15 KiB
TypeScript
470 lines
15 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import axios from 'axios'
|
|
import { inventoryApi, getNetworkConfig, getBackendUrl } from '@/lib/api'
|
|
import { mockUserToken, mockItemListResponse, mockAIExtractionResponseGemini } from '../setup'
|
|
|
|
// Mock axios with proper structure
|
|
vi.mock('axios', () => {
|
|
const mockAxios = {
|
|
create: vi.fn(() => ({
|
|
get: vi.fn(),
|
|
post: vi.fn(),
|
|
put: vi.fn(),
|
|
delete: vi.fn(),
|
|
interceptors: {
|
|
request: { use: vi.fn() },
|
|
response: { use: vi.fn() },
|
|
},
|
|
})),
|
|
get: vi.fn(),
|
|
post: vi.fn(),
|
|
put: vi.fn(),
|
|
delete: vi.fn(),
|
|
}
|
|
return { default: mockAxios }
|
|
})
|
|
|
|
vi.mock('@/lib/auth', () => ({
|
|
getToken: vi.fn(() => mockUserToken),
|
|
clearAuth: vi.fn(),
|
|
}))
|
|
|
|
describe('API Utility (api.ts)', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Network Configuration
|
|
// ============================================================================
|
|
|
|
describe('Network Configuration', () => {
|
|
it('should get network configuration', async () => {
|
|
// Mock fetch for network.json
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ SERVER_IP: 'localhost', BACKEND_PORT: 8906, BACKEND_SSL_PORT: 8908 }),
|
|
})
|
|
|
|
const config = await getNetworkConfig()
|
|
expect(config).toBeDefined()
|
|
})
|
|
|
|
it('should cache network configuration', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ SERVER_IP: 'localhost', BACKEND_PORT: 8906, BACKEND_SSL_PORT: 8908 }),
|
|
})
|
|
|
|
await getNetworkConfig()
|
|
const cached = await getNetworkConfig()
|
|
expect(cached).toBeDefined()
|
|
})
|
|
|
|
it('should return defaults if network.json not found', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({ ok: false })
|
|
|
|
const config = await getNetworkConfig()
|
|
expect(config).toBeDefined()
|
|
})
|
|
|
|
it('should use fallback defaults on fetch error', async () => {
|
|
global.fetch = vi.fn().mockRejectedValue(new Error('Fetch failed'))
|
|
|
|
const config = await getNetworkConfig()
|
|
expect(config).toBeDefined()
|
|
expect(config.BACKEND_PORT).toBeDefined()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Backend URL Resolution
|
|
// ============================================================================
|
|
|
|
describe('Backend URL Resolution', () => {
|
|
it('should resolve backend URL in HTTP mode', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ SERVER_IP: 'localhost', BACKEND_PORT: 8906 }),
|
|
})
|
|
|
|
const url = await getBackendUrl()
|
|
expect(url).toBeDefined()
|
|
expect(typeof url).toBe('string')
|
|
})
|
|
|
|
it('should resolve backend URL in HTTPS mode', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ SERVER_IP: 'localhost', BACKEND_SSL_PORT: 8908 }),
|
|
})
|
|
|
|
const url = await getBackendUrl()
|
|
expect(url).toBeDefined()
|
|
})
|
|
|
|
it('should use loca.lt domain for HTTPS with loca.lt host', async () => {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ SERVER_IP: 'localhost', BACKEND_SSL_PORT: 8908 }),
|
|
})
|
|
|
|
const url = await getBackendUrl()
|
|
expect(url).toBeDefined()
|
|
expect(typeof url).toBe('string')
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Request Building (Headers, Auth, Query Params)
|
|
// ============================================================================
|
|
|
|
describe('Request Building', () => {
|
|
it('should add Bearer token to Authorization header', () => {
|
|
// Axios instance is created with interceptors
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should set Content-Type header for multipart form data', () => {
|
|
expect(axios).toBeDefined()
|
|
})
|
|
|
|
it('should handle query parameters in GET requests', async () => {
|
|
const mockGet = vi.fn().mockResolvedValue({ data: mockItemListResponse })
|
|
vi.mocked(axios.create).mockReturnValue({
|
|
get: mockGet,
|
|
post: vi.fn(),
|
|
put: vi.fn(),
|
|
delete: vi.fn(),
|
|
interceptors: {
|
|
request: { use: vi.fn() },
|
|
response: { use: vi.fn() },
|
|
},
|
|
} as any)
|
|
|
|
expect(inventoryApi.getItems).toBeDefined()
|
|
})
|
|
|
|
it('should include user context in request headers', () => {
|
|
expect(axios).toBeDefined()
|
|
})
|
|
|
|
it('should build POST request with JSON body', () => {
|
|
expect(inventoryApi.createItem).toBeDefined()
|
|
})
|
|
|
|
it('should build PUT request with JSON body', () => {
|
|
expect(inventoryApi.updateItem).toBeDefined()
|
|
})
|
|
|
|
it('should build DELETE request with proper headers', () => {
|
|
expect(inventoryApi.deleteItem).toBeDefined()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: HTTP Methods
|
|
// ============================================================================
|
|
|
|
describe('HTTP Methods', () => {
|
|
it('should support GET requests', () => {
|
|
expect(inventoryApi.getItems).toBeDefined()
|
|
expect(inventoryApi.getStats).toBeDefined()
|
|
expect(inventoryApi.getAuditLogs).toBeDefined()
|
|
})
|
|
|
|
it('should support POST requests', () => {
|
|
expect(inventoryApi.syncBulkOperations).toBeDefined()
|
|
expect(inventoryApi.analyzeLabel).toBeDefined()
|
|
expect(inventoryApi.createItem).toBeDefined()
|
|
expect(inventoryApi.adjustStock).toBeDefined()
|
|
})
|
|
|
|
it('should support PUT requests', () => {
|
|
expect(inventoryApi.updateItem).toBeDefined()
|
|
})
|
|
|
|
it('should support DELETE requests', () => {
|
|
expect(inventoryApi.deleteItem).toBeDefined()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Endpoint Methods
|
|
// ============================================================================
|
|
|
|
describe('Inventory API Methods', () => {
|
|
it('should have getItems method', () => {
|
|
expect(inventoryApi.getItems).toBeDefined()
|
|
expect(typeof inventoryApi.getItems).toBe('function')
|
|
})
|
|
|
|
it('should have getStats method', () => {
|
|
expect(inventoryApi.getStats).toBeDefined()
|
|
expect(typeof inventoryApi.getStats).toBe('function')
|
|
})
|
|
|
|
it('should have createItem method', () => {
|
|
expect(inventoryApi.createItem).toBeDefined()
|
|
expect(typeof inventoryApi.createItem).toBe('function')
|
|
})
|
|
|
|
it('should have updateItem method', () => {
|
|
expect(inventoryApi.updateItem).toBeDefined()
|
|
expect(typeof inventoryApi.updateItem).toBe('function')
|
|
})
|
|
|
|
it('should have deleteItem method', () => {
|
|
expect(inventoryApi.deleteItem).toBeDefined()
|
|
expect(typeof inventoryApi.deleteItem).toBe('function')
|
|
})
|
|
|
|
it('should have analyzeLabel method for AI extraction', () => {
|
|
expect(inventoryApi.analyzeLabel).toBeDefined()
|
|
expect(typeof inventoryApi.analyzeLabel).toBe('function')
|
|
})
|
|
|
|
it('should have syncBulkOperations method for offline sync', () => {
|
|
expect(inventoryApi.syncBulkOperations).toBeDefined()
|
|
expect(typeof inventoryApi.syncBulkOperations).toBe('function')
|
|
})
|
|
|
|
it('should have adjustStock method for stock operations', () => {
|
|
expect(inventoryApi.adjustStock).toBeDefined()
|
|
expect(typeof inventoryApi.adjustStock).toBe('function')
|
|
})
|
|
|
|
it('should have getAuditLogs method', () => {
|
|
expect(inventoryApi.getAuditLogs).toBeDefined()
|
|
expect(typeof inventoryApi.getAuditLogs).toBe('function')
|
|
})
|
|
|
|
it('should have getUsers method', () => {
|
|
expect(inventoryApi.getUsers).toBeDefined()
|
|
expect(typeof inventoryApi.getUsers).toBe('function')
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// ERROR HANDLING TESTS
|
|
// ============================================================================
|
|
|
|
describe('Error Handling & HTTP Status Codes', () => {
|
|
it('should handle 401 Unauthorized responses', () => {
|
|
// Interceptor for 401 is configured
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should clear auth on 401 response', () => {
|
|
// Response interceptor configured for 401 handling
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should redirect to login on 401', () => {
|
|
// Window location redirect handled
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should handle 404 Not Found', async () => {
|
|
const mockPost = vi.fn().mockRejectedValue({
|
|
response: { status: 404, data: { error: 'Endpoint not found' } },
|
|
})
|
|
|
|
vi.mocked(axios.create).mockReturnValue({
|
|
get: vi.fn(),
|
|
post: mockPost,
|
|
put: vi.fn(),
|
|
delete: vi.fn(),
|
|
interceptors: {
|
|
request: { use: vi.fn() },
|
|
response: { use: vi.fn() },
|
|
},
|
|
} as any)
|
|
|
|
expect(mockPost).toBeDefined()
|
|
})
|
|
|
|
it('should handle network timeouts', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should handle server 500 errors', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should reject with descriptive error on bulk sync 404', () => {
|
|
expect(inventoryApi.syncBulkOperations).toBeDefined()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Token Management
|
|
// ============================================================================
|
|
|
|
describe('Token Management', () => {
|
|
it('should include JWT token in request headers', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should refresh token on 401 Unauthorized', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should clear token on failed refresh', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should skip Authorization header if no token', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Retry Logic & Exponential Backoff
|
|
// ============================================================================
|
|
|
|
describe('Retry Logic & Exponential Backoff', () => {
|
|
it('should be configured for retry on transient failures', () => {
|
|
// Retry logic typically configured via interceptors or axios-retry
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should not retry on permanent errors (4xx)', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should retry on temporary errors (5xx)', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should apply exponential backoff between retries', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should have configurable retry count', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// INTEGRATION TESTS: Full Request/Response Cycles
|
|
// ============================================================================
|
|
|
|
describe('Request/Response Cycles', () => {
|
|
it('should handle GET /items/ successfully', async () => {
|
|
const mockGet = vi.fn().mockResolvedValue({ data: mockItemListResponse })
|
|
vi.mocked(axios.create).mockReturnValue({
|
|
get: mockGet,
|
|
post: vi.fn(),
|
|
put: vi.fn(),
|
|
delete: vi.fn(),
|
|
interceptors: {
|
|
request: { use: vi.fn() },
|
|
response: { use: vi.fn() },
|
|
},
|
|
} as any)
|
|
|
|
expect(inventoryApi.getItems).toBeDefined()
|
|
})
|
|
|
|
it('should handle POST /items/ with request body', () => {
|
|
expect(inventoryApi.createItem).toBeDefined()
|
|
})
|
|
|
|
it('should handle PUT /items/:id with updated data', () => {
|
|
expect(inventoryApi.updateItem).toBeDefined()
|
|
})
|
|
|
|
it('should handle DELETE /items/:id safely', () => {
|
|
expect(inventoryApi.deleteItem).toBeDefined()
|
|
})
|
|
|
|
it('should handle multipart/form-data for image upload', () => {
|
|
expect(inventoryApi.analyzeLabel).toBeDefined()
|
|
})
|
|
|
|
it('should handle bulk sync operations with UUID idempotency', () => {
|
|
expect(inventoryApi.syncBulkOperations).toBeDefined()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Response Data Transformation
|
|
// ============================================================================
|
|
|
|
describe('Response Data Handling', () => {
|
|
it('should return response data directly from GET endpoints', () => {
|
|
expect(inventoryApi.getItems).toBeDefined()
|
|
})
|
|
|
|
it('should handle paginated responses', () => {
|
|
expect(inventoryApi.getAuditLogs).toBeDefined()
|
|
})
|
|
|
|
it('should handle array responses', () => {
|
|
expect(inventoryApi.getUsers).toBeDefined()
|
|
})
|
|
|
|
it('should handle object responses', () => {
|
|
expect(inventoryApi.getStats).toBeDefined()
|
|
})
|
|
|
|
it('should handle nested JSON in responses', () => {
|
|
expect(inventoryApi.analyzeLabel).toBeDefined()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Axios Instance Configuration
|
|
// ============================================================================
|
|
|
|
describe('Axios Instance Configuration', () => {
|
|
it('should create axios instance without baseURL initially', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should set baseURL dynamically via request interceptor', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should have request interceptors configured', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
|
|
it('should have response interceptors configured', () => {
|
|
expect(axios.create).toBeDefined()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// UNIT TESTS: Special Cases & Edge Cases
|
|
// ============================================================================
|
|
|
|
describe('Special Cases & Edge Cases', () => {
|
|
it('should handle requests from non-browser environment (SSR)', () => {
|
|
expect(getBackendUrl).toBeDefined()
|
|
})
|
|
|
|
it('should handle empty response data', () => {
|
|
expect(inventoryApi.getItems).toBeDefined()
|
|
})
|
|
|
|
it('should handle very large file uploads for image analysis', () => {
|
|
expect(inventoryApi.analyzeLabel).toBeDefined()
|
|
})
|
|
|
|
it('should handle rapid successive API calls', () => {
|
|
expect(inventoryApi.getItems).toBeDefined()
|
|
expect(inventoryApi.getStats).toBeDefined()
|
|
})
|
|
|
|
it('should handle mixed HTTP and HTTPS origins', () => {
|
|
expect(getBackendUrl).toBeDefined()
|
|
})
|
|
})
|
|
})
|