cherry-studio/tests/__mocks__/renderer/DataApiService.ts
fullex 773e9eac32 feat: enhance mock services for improved testing capabilities
- Updated `CacheService` mock to include TTL support and type-safe memory and shared cache methods, enhancing the accuracy of cache behavior during tests.
- Refactored `DataApiService` mock to implement realistic HTTP methods and subscription handling, including retry configuration and request management, improving the fidelity of API interactions in tests.
- Enhanced `useDataApi` mocks to align with actual hook signatures, providing a more accurate simulation of data fetching and mutation behaviors, including loading and error states.
- Introduced utility functions for managing mock state and triggering subscription callbacks, streamlining the testing process for components relying on these services.
2026-01-04 10:24:35 +08:00

401 lines
11 KiB
TypeScript

import type { ConcreteApiPaths } from '@shared/data/api/apiTypes'
import type { SubscriptionCallback, SubscriptionOptions } from '@shared/data/api/apiTypes'
import { SubscriptionEvent } from '@shared/data/api/apiTypes'
import { vi } from 'vitest'
/**
* Mock DataApiService for testing
* Provides a comprehensive mock of the DataApiService with realistic behavior
* Matches the actual DataApiService interface from src/renderer/src/data/DataApiService.ts
*/
/**
* Retry options interface (matches actual)
*/
interface RetryOptions {
maxRetries: number
retryDelay: number
backoffMultiplier: number
}
/**
* Get mock data based on API path and method
* Provides realistic mock responses for common API endpoints
*/
function getMockDataForPath(path: ConcreteApiPaths, method: string): any {
// Parse path to determine data type
if (path.includes('/topics')) {
if (method === 'GET' && path.endsWith('/topics')) {
return {
topics: [
{ id: 'topic1', name: 'Mock Topic 1', createdAt: '2024-01-01T00:00:00Z' },
{ id: 'topic2', name: 'Mock Topic 2', createdAt: '2024-01-02T00:00:00Z' }
],
total: 2
}
}
if (method === 'GET' && path.match(/\/topics\/[^/]+$/)) {
return {
id: 'topic1',
name: 'Mock Topic',
messages: [],
createdAt: '2024-01-01T00:00:00Z'
}
}
if (method === 'POST' && path.endsWith('/topics')) {
return {
id: 'new_topic',
name: 'New Mock Topic',
createdAt: new Date().toISOString()
}
}
}
if (path.includes('/messages')) {
if (method === 'GET') {
return {
messages: [
{ id: 'msg1', content: 'Mock message 1', role: 'user', timestamp: '2024-01-01T00:00:00Z' },
{ id: 'msg2', content: 'Mock message 2', role: 'assistant', timestamp: '2024-01-01T00:01:00Z' }
],
total: 2
}
}
if (method === 'POST') {
return {
id: 'new_message',
content: 'New mock message',
role: 'user',
timestamp: new Date().toISOString()
}
}
}
if (path.includes('/preferences')) {
if (method === 'GET') {
return {
preferences: {
'ui.theme': 'light',
'ui.language': 'en',
'data.export.format': 'markdown'
}
}
}
if (method === 'POST' || method === 'PUT') {
return { updated: true, timestamp: new Date().toISOString() }
}
}
// Default mock data
return {
id: 'mock_id',
data: 'mock_data',
timestamp: new Date().toISOString()
}
}
/**
* Create a mock DataApiService with realistic behavior
*/
export const createMockDataApiService = (customBehavior: Partial<ReturnType<typeof createMockDataApiService>> = {}) => {
// Track subscriptions
const subscriptions = new Map<
string,
{
callback: SubscriptionCallback
options: SubscriptionOptions
}
>()
// Retry configuration
let retryOptions: RetryOptions = {
maxRetries: 2,
retryDelay: 1000,
backoffMultiplier: 2
}
const mockService = {
// ============ HTTP Methods ============
get: vi.fn(
async <TPath extends ConcreteApiPaths>(
path: TPath,
_options?: { query?: any; headers?: Record<string, string> }
) => {
return getMockDataForPath(path, 'GET')
}
),
post: vi.fn(
async <TPath extends ConcreteApiPaths>(
path: TPath,
_options: { body?: any; query?: Record<string, any>; headers?: Record<string, string> }
) => {
return getMockDataForPath(path, 'POST')
}
),
put: vi.fn(
async <TPath extends ConcreteApiPaths>(
path: TPath,
_options: { body: any; query?: Record<string, any>; headers?: Record<string, string> }
) => {
return getMockDataForPath(path, 'PUT')
}
),
patch: vi.fn(
async <TPath extends ConcreteApiPaths>(
path: TPath,
_options: { body?: any; query?: Record<string, any>; headers?: Record<string, string> }
) => {
return getMockDataForPath(path, 'PATCH')
}
),
delete: vi.fn(
async <TPath extends ConcreteApiPaths>(
_path: TPath,
_options?: { query?: Record<string, any>; headers?: Record<string, string> }
) => {
return { deleted: true }
}
),
// ============ Subscription ============
subscribe: vi.fn(<T>(options: SubscriptionOptions, callback: SubscriptionCallback<T>): (() => void) => {
const subscriptionId = `sub_${Date.now()}_${Math.random()}`
subscriptions.set(subscriptionId, {
callback: callback as SubscriptionCallback,
options
})
// Return unsubscribe function
return () => {
subscriptions.delete(subscriptionId)
}
}),
// ============ Retry Configuration ============
configureRetry: vi.fn((options: Partial<RetryOptions>): void => {
retryOptions = {
...retryOptions,
...options
}
}),
getRetryConfig: vi.fn((): RetryOptions => {
return { ...retryOptions }
}),
// ============ Request Management (Deprecated) ============
/**
* @deprecated This method has no effect with direct IPC
*/
cancelRequest: vi.fn((_requestId: string): void => {
// No-op - direct IPC requests cannot be cancelled
}),
/**
* @deprecated This method has no effect with direct IPC
*/
cancelAllRequests: vi.fn((): void => {
// No-op - direct IPC requests cannot be cancelled
}),
// ============ Statistics ============
getRequestStats: vi.fn(() => ({
pendingRequests: 0,
activeSubscriptions: subscriptions.size
})),
// ============ Internal State Access for Testing ============
_getMockState: () => ({
subscriptions: new Map(subscriptions),
retryOptions: { ...retryOptions }
}),
_resetMockState: () => {
subscriptions.clear()
retryOptions = {
maxRetries: 2,
retryDelay: 1000,
backoffMultiplier: 2
}
},
_triggerSubscription: (path: string, data: any, event: SubscriptionEvent) => {
subscriptions.forEach(({ callback, options }) => {
if (options.path === path) {
callback(data, event)
}
})
},
// Apply custom behavior overrides
...customBehavior
}
return mockService
}
// Default mock instance
export const mockDataApiService = createMockDataApiService()
// Singleton instance mock
export const MockDataApiService = {
DataApiService: class MockDataApiService {
static getInstance() {
return mockDataApiService
}
// ============ HTTP Methods ============
async get<TPath extends ConcreteApiPaths>(
path: TPath,
options?: { query?: any; headers?: Record<string, string> }
) {
return mockDataApiService.get(path, options)
}
async post<TPath extends ConcreteApiPaths>(
path: TPath,
options: { body?: any; query?: Record<string, any>; headers?: Record<string, string> }
) {
return mockDataApiService.post(path, options)
}
async put<TPath extends ConcreteApiPaths>(
path: TPath,
options: { body: any; query?: Record<string, any>; headers?: Record<string, string> }
) {
return mockDataApiService.put(path, options)
}
async patch<TPath extends ConcreteApiPaths>(
path: TPath,
options: { body?: any; query?: Record<string, any>; headers?: Record<string, string> }
) {
return mockDataApiService.patch(path, options)
}
async delete<TPath extends ConcreteApiPaths>(
path: TPath,
options?: { query?: Record<string, any>; headers?: Record<string, string> }
) {
return mockDataApiService.delete(path, options)
}
// ============ Subscription ============
subscribe<T>(options: SubscriptionOptions, callback: SubscriptionCallback<T>): () => void {
return mockDataApiService.subscribe(options, callback)
}
// ============ Retry Configuration ============
configureRetry(options: Partial<RetryOptions>): void {
return mockDataApiService.configureRetry(options)
}
getRetryConfig(): RetryOptions {
return mockDataApiService.getRetryConfig()
}
// ============ Request Management ============
cancelRequest(requestId: string): void {
return mockDataApiService.cancelRequest(requestId)
}
cancelAllRequests(): void {
return mockDataApiService.cancelAllRequests()
}
// ============ Statistics ============
getRequestStats() {
return mockDataApiService.getRequestStats()
}
},
dataApiService: mockDataApiService
}
/**
* Utility functions for testing
*/
export const MockDataApiUtils = {
/**
* Reset all mock function call counts and implementations
*/
resetMocks: () => {
Object.values(mockDataApiService).forEach((method) => {
if (vi.isMockFunction(method)) {
method.mockClear()
}
})
mockDataApiService._resetMockState()
},
/**
* Set custom response for a specific path and method
*/
setCustomResponse: (path: ConcreteApiPaths, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', response: any) => {
const methodFn = mockDataApiService[method.toLowerCase() as 'get' | 'post' | 'put' | 'patch' | 'delete']
if (vi.isMockFunction(methodFn)) {
methodFn.mockImplementation(async (requestPath: string) => {
if (requestPath === path) {
return response
}
return getMockDataForPath(requestPath as ConcreteApiPaths, method)
})
}
},
/**
* Set error response for a specific path and method
*/
setErrorResponse: (path: ConcreteApiPaths, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', error: Error) => {
const methodFn = mockDataApiService[method.toLowerCase() as 'get' | 'post' | 'put' | 'patch' | 'delete']
if (vi.isMockFunction(methodFn)) {
methodFn.mockImplementation(async (requestPath: string) => {
if (requestPath === path) {
throw error
}
return getMockDataForPath(requestPath as ConcreteApiPaths, method)
})
}
},
/**
* Get call count for a specific method
*/
getCallCount: (method: 'get' | 'post' | 'put' | 'patch' | 'delete'): number => {
const methodFn = mockDataApiService[method]
return vi.isMockFunction(methodFn) ? methodFn.mock.calls.length : 0
},
/**
* Get calls for a specific method
*/
getCalls: (method: 'get' | 'post' | 'put' | 'patch' | 'delete'): any[] => {
const methodFn = mockDataApiService[method]
return vi.isMockFunction(methodFn) ? methodFn.mock.calls : []
},
/**
* Trigger a subscription callback for testing
*/
triggerSubscription: (path: string, data: any, event: SubscriptionEvent = SubscriptionEvent.UPDATED) => {
mockDataApiService._triggerSubscription(path, data, event)
},
/**
* Get current mock state
*/
getCurrentState: () => {
return mockDataApiService._getMockState()
}
}