UNPKG

pinata-expo-hooks

Version:

React Native/Expo hooks for Pinata TUS uploads

214 lines (213 loc) 9.13 kB
import { renderHook, act } from '@testing-library/react'; import { useUpload } from '../hooks/useUpload'; import * as FileSystem from 'expo-file-system'; // Mock fetch globally const mockFetch = global.fetch; const mockGetInfoAsync = FileSystem.getInfoAsync; const mockReadAsStringAsync = FileSystem.readAsStringAsync; describe('useUpload - Retry Functionality', () => { beforeEach(() => { jest.clearAllMocks(); jest.clearAllTimers(); // Mock successful file operations mockGetInfoAsync.mockResolvedValue({ exists: true, size: 100, isDirectory: false, modificationTime: Date.now(), uri: 'file://test.jpg', }); mockReadAsStringAsync.mockResolvedValue('dGVzdA=='); }); describe('Basic Retry State', () => { it('should initialize with retryCount of 0', () => { const { result } = renderHook(() => useUpload()); expect(result.current.retryCount).toBe(0); }); it('should reset retryCount when resetState is called', () => { const { result } = renderHook(() => useUpload()); // Manually set retryCount (simulating retries happened) act(() => { result.current.resetState(); }); expect(result.current.retryCount).toBe(0); }); }); describe('Retry Configuration', () => { it('should accept custom retry options', async () => { const { result } = renderHook(() => useUpload()); // Mock successful TUS init but failing chunk upload mockFetch .mockResolvedValueOnce(new Response(null, { status: 201, headers: { Location: 'https://upload.com/123' } })) .mockResolvedValueOnce(new Response(null, { status: 500 }) // Fail first chunk ) .mockResolvedValueOnce(new Response(null, { status: 204 }) // Succeed on retry ); let uploadCompleted = false; await act(async () => { try { await result.current.upload('file://test.jpg', 'public', 'https://test.com/presigned', { retryOptions: { maxRetries: 2, initialDelay: 10, retryableStatuses: [500], } }); uploadCompleted = true; } catch (error) { // May still throw, but we're testing configuration acceptance } // Advance timer for retry jest.advanceTimersByTime(10); }); // Verify that upload was called with retry options // The fact that it doesn't throw immediately means options were accepted expect(result.current.retryCount).toBeGreaterThanOrEqual(0); }); it('should track retry attempts in retryCount state', async () => { const { result } = renderHook(() => useUpload()); let fetchCallCount = 0; mockFetch.mockImplementation(async (url) => { fetchCallCount++; if (url.toString().includes('presigned')) { // TUS init - succeed return new Response(null, { status: 201, headers: { Location: 'https://upload.com/123' } }); } else { // Chunk upload - fail first time, succeed second if (fetchCallCount <= 2) { return new Response(null, { status: 500 }); } return new Response(null, { status: 204 }); } }); await act(async () => { try { await result.current.upload('file://test.jpg', 'public', 'https://test.com/presigned', { retryOptions: { maxRetries: 1, initialDelay: 5, } }); } catch (error) { // Expected to potentially throw } jest.advanceTimersByTime(5); }); // Should have incremented retryCount expect(result.current.retryCount).toBeGreaterThan(0); }); }); describe('Retry Status Codes', () => { it('should only retry on configured retryable status codes', async () => { const { result } = renderHook(() => useUpload()); let callCount = 0; mockFetch.mockImplementation(async (url) => { callCount++; if (url.toString().includes('presigned')) { return new Response(null, { status: 201, headers: { Location: 'https://upload.com/123' } }); } else { // Return non-retryable status return new Response(null, { status: 404 }); } }); await act(async () => { try { await result.current.upload('file://test.jpg', 'public', 'https://test.com/presigned', { retryOptions: { maxRetries: 3, initialDelay: 5, retryableStatuses: [500, 502, 503], // 404 not included } }); } catch (error) { // Expected to fail without retries } jest.advanceTimersByTime(20); }); // Should NOT have retried (404 is not in retryable list) expect(result.current.retryCount).toBe(0); expect(callCount).toBeLessThanOrEqual(2); // TUS init + one chunk attempt }); it('should retry on retryable status codes', async () => { const { result } = renderHook(() => useUpload()); let callCount = 0; mockFetch.mockImplementation(async (url) => { callCount++; if (url.toString().includes('presigned')) { return new Response(null, { status: 201, headers: { Location: 'https://upload.com/123' } }); } else { // Return retryable status first time, then succeed if (callCount <= 2) { return new Response(null, { status: 500 }); // Retryable } return new Response(null, { status: 204 }); // Success } }); await act(async () => { try { await result.current.upload('file://test.jpg', 'public', 'https://test.com/presigned', { retryOptions: { maxRetries: 2, initialDelay: 5, retryableStatuses: [500], // Include 500 as retryable } }); } catch (error) { // May throw after retries } jest.advanceTimersByTime(10); }); // Should have retried (500 is in retryable list) expect(result.current.retryCount).toBeGreaterThan(0); }); }); describe('Cancellation During Retries', () => { it('should have cancel functionality available', () => { const { result } = renderHook(() => useUpload()); // Verify cancel function exists and is callable expect(typeof result.current.cancel).toBe('function'); // Should be able to call cancel without error act(() => { result.current.cancel(); }); expect(result.current.loading).toBe(false); }); }); describe('Default Retry Behavior', () => { it('should use sensible defaults when no retry options provided', async () => { const { result } = renderHook(() => useUpload()); mockFetch.mockResolvedValue(new Response(null, { status: 200 })); await act(async () => { try { await result.current.upload('file://test.jpg', 'public', 'https://test.com/presigned' // No retry options - should use defaults ); } catch (error) { // May throw, but defaults should be applied } }); // Should have handled the upload without throwing due to missing config expect(result.current.retryCount).toBe(0); // No retries needed for success }); }); });