// 导入类以便访问静态方法 import { JoliGemPaymentService, clearNativePriceCache, resetFailureCounters, getFailureCount } from '../payment-service'; // Mock dependencies jest.mock('@jolibox/native-bridge', () => ({ applyNative: jest.fn(), onNative: jest.fn(), invokeNative: jest.fn() })); jest.mock('@/native/network', () => ({ innerFetch: jest.fn() })); jest.mock('@/native/api/storage', () => ({ setGlobalStorage: jest.fn().mockResolvedValue({ code: 'SUCCESS', message: 'success' }), getGlobalStorage: jest.fn().mockResolvedValue({ code: 'SUCCESS', message: 'success', data: null }) })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { applyNative } = require('@jolibox/native-bridge'); // eslint-disable-next-line @typescript-eslint/no-var-requires const { innerFetch: fetch } = require('@/native/network'); const mockApplyNative = applyNative; const mockFetch = fetch; describe('PaymentService - Basic Tests', () => { const mockApiEndpoint = '/api/joli-gem/balance-detail'; let paymentService: JoliGemPaymentService; const mockServerData = { balance: 100, enableAutoDeduct: true, paymentChoices: [ { productId: 'coin_100', quantity: 100, appStoreProductId: 'com.joli.coin.100' } ] }; const mockNativeData = { 'com.joli.coin.100': { price: '$0.99' } }; beforeEach(() => { jest.clearAllMocks(); jest.clearAllTimers(); jest.useFakeTimers(); paymentService = new JoliGemPaymentService(); // 每次创建新实例 clearNativePriceCache(); // Clear native price cache resetFailureCounters(); // Clear failure counters }); afterEach(() => { jest.useRealTimers(); jest.restoreAllMocks(); }); it('should fetch and cache payment choices on first call', async () => { mockFetch.mockResolvedValue({ response: { data: { data: mockServerData } } }); mockApplyNative.mockResolvedValue({ data: mockNativeData }); const result = await paymentService.getProductsInfo(); expect(mockFetch).toHaveBeenCalledTimes(1); expect(mockApplyNative).toHaveBeenCalledTimes(1); expect(result).toEqual({ balance: 100, enableAutoDeduct: true, paymentChoices: [ { productId: 'coin_100', quantity: 100, appStoreProductId: 'com.joli.coin.100', totalAmountStr: '$0.99' } ] }); // Check cache stats using RequestCacheService methods const stats = paymentService.getCacheStats(); expect(stats.cacheCount).toBe(1); expect(stats.validCount).toBe(1); }); it('should use cache on second call', async () => { mockFetch.mockResolvedValue({ response: { data: { data: mockServerData } } }); mockApplyNative.mockResolvedValue({ data: mockNativeData }); // First call await paymentService.getProductsInfo(); // Second call - should use cache for native prices but still fetch real-time data const result = await paymentService.getProductsInfo(); expect(mockFetch).toHaveBeenCalledTimes(1); // Still fetch for real-time data (balance, enableAutoDeduct) expect(mockApplyNative).toHaveBeenCalledTimes(1); // Only called once due to native price caching expect(result).toEqual({ balance: 0, enableAutoDeduct: false, paymentChoices: [ { productId: 'coin_100', quantity: 100, appStoreProductId: 'com.joli.coin.100', totalAmountStr: '$0.99' } ] }); }); it('should timeout after 1 second', async () => { // Mock a slow server response mockFetch.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 2000))); const promise = paymentService.getProductsInfo(); // Fast forward past timeout (1 second) jest.advanceTimersByTime(3000); await expect(promise).rejects.toThrow('[RequestCacheService] Request timeout after 3 seconds'); }); it('should handle cache expiration', async () => { mockFetch.mockResolvedValue({ response: { data: { data: mockServerData } } }); mockApplyNative.mockResolvedValue({ data: mockNativeData }); // First call await paymentService.getProductsInfo(); // Fast forward time beyond cache duration (10 minutes) jest.advanceTimersByTime(40 * 60 * 1000); // Second call - cache should be expired await paymentService.getProductsInfo(); expect(mockApplyNative).toHaveBeenCalledTimes(2); // Called again due to expiration }); it('should throw error when mockApplyNative fails', async () => { mockFetch.mockResolvedValue({ response: { data: { data: mockServerData } } }); // Mock applyNative to reject mockApplyNative.mockRejectedValue(new Error('Native call failed')); await expect(paymentService.getProductsInfo()).rejects.toThrow('Native call failed'); }); it('should throw error when mockFetch fails', async () => { // Mock fetch to reject mockFetch.mockRejectedValue(new Error('Network error')); await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error'); }); it('should throw error when request exceeds 1 second timeout', async () => { // Mock a very slow fetch request that never resolves within 1 second mockFetch.mockImplementation( () => new Promise((resolve) => { // This will resolve after 2 seconds, but timeout should trigger at 1 second setTimeout(() => { resolve({ response: { data: { data: mockServerData } } }); }, 5000); }) ); const promise = paymentService.getProductsInfo(); // Advance timers to 1 second to trigger timeout jest.advanceTimersByTime(3000); await expect(promise).rejects.toThrow('[RequestCacheService] Request timeout after 3 seconds'); }); it('should increment failure count on failed requests', async () => { // Mock fetch to reject mockFetch.mockRejectedValue(new Error('Network error')); // First failure await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error'); expect(getFailureCount('/api/joli-gem/balance-detail')).toBe(1); // Second failure await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error'); expect(getFailureCount('/api/joli-gem/balance-detail')).toBe(2); }); it('should block requests after 2 failures', async () => { // Mock fetch to reject mockFetch.mockRejectedValue(new Error('Network error')); // First two failures await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error'); await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error'); // Third call should be blocked immediately (failure count is now 2, which >= MAX_FAILURE_COUNT) await expect(paymentService.getProductsInfo()).rejects.toThrow( 'getProductsInfo has failed more than 2 times for /api/joli-gem/balance-detail' ); // Verify fetch was only called twice (not three times) expect(mockFetch).toHaveBeenCalledTimes(2); }); it('should reset failure count on successful request', async () => { // Mock first call to fail mockFetch.mockRejectedValueOnce(new Error('Network error')); // First failure await expect(paymentService.getProductsInfo()).rejects.toThrow('Network error'); expect(getFailureCount('/api/joli-gem/balance-detail')).toBe(1); // Mock second call to succeed mockFetch.mockResolvedValue({ response: { data: { data: mockServerData } } }); mockApplyNative.mockResolvedValue({ data: mockNativeData }); // Successful call should reset counter await paymentService.getProductsInfo(); expect(getFailureCount('/api/joli-gem/balance-detail')).toBe(0); }); });