mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 07:19:02 +08:00
- 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.
401 lines
11 KiB
TypeScript
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()
|
|
}
|
|
}
|