UNPKG

stream-chat-react-native-core

Version:

The official React Native and Expo components for Stream Chat, a service for building chat applications

438 lines (385 loc) 11.5 kB
import type { AxiosProgressEvent, AxiosRequestConfig } from 'axios'; import { getTestClient } from '../../mock-builders/mock'; import { NativeHandlers, NativeMultipartUploadProgressConfig } from '../../native'; import { installNativeMultipartAdapter, wrapAxiosAdapterWithNativeMultipart, } from '../installNativeMultipartAdapter'; type NativeMultipartTestAxiosConfig = AxiosRequestConfig & { uploadProgress?: (event: AxiosProgressEvent) => void; uploadProgressOptions?: NativeMultipartUploadProgressConfig; }; const nativeMultipartConfig = (config: NativeMultipartTestAxiosConfig) => config; describe('installNativeMultipartAdapter', () => { const originalMultipartUpload = NativeHandlers.multipartUpload; beforeEach(() => { NativeHandlers.multipartUpload = jest.fn().mockResolvedValue({ body: JSON.stringify({ file: 'https://example.com/file.jpg' }), headers: { 'content-type': 'application/json' }, status: 200, statusText: 'OK', }); }); const preserveRequestData = (client: ReturnType<typeof getTestClient>) => { client.axiosInstance.defaults.transformRequest = [(data) => data]; }; afterEach(() => { NativeHandlers.multipartUpload = originalMultipartUpload; jest.clearAllMocks(); }); it('routes multipart requests through the native handler', async () => { const client = getTestClient(); preserveRequestData(client); const defaultAdapter = jest.fn().mockResolvedValue({ config: {}, data: 'default', headers: {}, status: 200, statusText: 'OK', }); client.axiosInstance.defaults.adapter = defaultAdapter; installNativeMultipartAdapter(client); const formData = { _parts: [ [ 'file', { name: 'test.jpg', type: 'image/jpeg', uri: 'file:///tmp/test.jpg', }, ], ['user', JSON.stringify({ id: 'john' })], ], }; const response = await client.axiosInstance.post('/uploads/image', formData, { headers: { Authorization: 'token', 'Content-Type': 'multipart/form-data', 'X-Stream-Client': 'stream-test', }, params: { api_key: 'test-key', }, timeout: 1234, }); expect(defaultAdapter).not.toHaveBeenCalled(); expect(NativeHandlers.multipartUpload).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'token', 'Content-Type': 'multipart/form-data', 'X-Stream-Client': 'stream-test', }), timeoutMs: 1234, parts: [ { fieldName: 'file', fileName: 'test.jpg', kind: 'file', mimeType: 'image/jpeg', uri: 'file:///tmp/test.jpg', }, { fieldName: 'user', kind: 'text', value: JSON.stringify({ id: 'john' }), }, ], }), ); expect(response.status).toBe(200); }); it('leaves non-multipart requests on the fallback adapter', async () => { const client = getTestClient(); preserveRequestData(client); const defaultAdapter = jest.fn().mockResolvedValue({ config: {}, data: 'default', headers: {}, status: 200, statusText: 'OK', }); client.axiosInstance.defaults.adapter = defaultAdapter; installNativeMultipartAdapter(client); await client.axiosInstance.post('/messages', { text: 'hello' }); expect(defaultAdapter).toHaveBeenCalled(); expect(NativeHandlers.multipartUpload).not.toHaveBeenCalled(); }); it('forwards native upload progress to axios upload progress callbacks', async () => { NativeHandlers.multipartUpload = jest.fn().mockImplementation(({ onProgress }) => { onProgress?.({ loaded: '50' as unknown as number, total: '100' as unknown as number, }); return { body: JSON.stringify({ file: 'https://example.com/file.jpg' }), headers: { 'content-type': 'application/json' }, status: 200, statusText: 'OK', }; }); const client = getTestClient(); preserveRequestData(client); installNativeMultipartAdapter(client); const onUploadProgress = jest.fn(); const uploadProgress = jest.fn(); const formData = { _parts: [ [ 'file', { name: 'test.jpg', type: 'image/jpeg', uri: 'file:///tmp/test.jpg', }, ], ], }; await client.axiosInstance.post( '/uploads/image', formData, nativeMultipartConfig({ onUploadProgress, uploadProgressOptions: { count: 10, intervalMs: 25, }, uploadProgress, }), ); expect(onUploadProgress).toHaveBeenCalledWith( expect.objectContaining({ lengthComputable: true, loaded: 50, progress: 0.5, total: 100, }), ); expect(uploadProgress).toHaveBeenCalledWith( expect.objectContaining({ lengthComputable: true, loaded: 50, progress: 0.5, total: 100, }), ); expect(NativeHandlers.multipartUpload).toHaveBeenCalledWith( expect.objectContaining({ progress: { count: 10, intervalMs: 25, }, }), ); }); it('caps native multipart body progress to 90 percent while waiting for the response', async () => { NativeHandlers.multipartUpload = jest.fn().mockImplementation(({ onProgress }) => { onProgress?.({ loaded: 100, total: 100, }); return { body: JSON.stringify({ file: 'https://example.com/file.jpg' }), headers: { 'content-type': 'application/json' }, status: 200, statusText: 'OK', }; }); const client = getTestClient(); preserveRequestData(client); installNativeMultipartAdapter(client); const onUploadProgress = jest.fn(); const formData = { _parts: [ [ 'file', { name: 'test.jpg', type: 'image/jpeg', uri: 'file:///tmp/test.jpg', }, ], ], }; await client.axiosInstance.post('/uploads/image', formData, { onUploadProgress }); expect(onUploadProgress).toHaveBeenCalledTimes(1); expect(onUploadProgress).toHaveBeenCalledWith( expect.objectContaining({ bytes: 90, lengthComputable: true, loaded: 90, progress: 0.9, total: 100, }), ); }); it('allows overriding the native multipart completion progress cap', async () => { NativeHandlers.multipartUpload = jest.fn().mockImplementation(({ onProgress }) => { onProgress?.({ loaded: 100, total: 100, }); return { body: JSON.stringify({ file: 'https://example.com/file.jpg' }), headers: { 'content-type': 'application/json' }, status: 200, statusText: 'OK', }; }); const client = getTestClient(); preserveRequestData(client); installNativeMultipartAdapter(client); const onUploadProgress = jest.fn(); const formData = { _parts: [ [ 'file', { name: 'test.jpg', type: 'image/jpeg', uri: 'file:///tmp/test.jpg', }, ], ], }; await client.axiosInstance.post( '/uploads/image', formData, nativeMultipartConfig({ onUploadProgress, uploadProgressOptions: { completionProgressCap: 75, count: 10, intervalMs: 25, }, }), ); expect(onUploadProgress).toHaveBeenCalledTimes(1); expect(onUploadProgress).toHaveBeenCalledWith( expect.objectContaining({ bytes: 75, loaded: 75, progress: 0.75, total: 100, }), ); expect(NativeHandlers.multipartUpload).toHaveBeenCalledWith( expect.objectContaining({ progress: { count: 10, intervalMs: 25, }, }), ); }); it('uses the final config after user request interceptors run', async () => { const client = getTestClient(); preserveRequestData(client); const defaultAdapter = jest.fn().mockResolvedValue({ config: {}, data: 'default', headers: {}, status: 200, statusText: 'OK', }); client.axiosInstance.defaults.adapter = defaultAdapter; const interceptorId = client.axiosInstance.interceptors.request.use((config) => { config.headers.set('X-CDN-Route', 'custom-cdn'); config.url = '/uploads/file'; return config; }); installNativeMultipartAdapter(client); const formData = { _parts: [ [ 'file', { name: 'test.jpg', type: 'image/jpeg', uri: 'file:///tmp/test.jpg', }, ], ], }; await client.axiosInstance.post('/uploads/image', formData, { headers: { Authorization: 'token', }, }); expect(NativeHandlers.multipartUpload).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'token', 'X-CDN-Route': 'custom-cdn', }), url: expect.stringContaining('/uploads/file'), }), ); expect(defaultAdapter).not.toHaveBeenCalled(); client.axiosInstance.interceptors.request.eject(interceptorId); }); it('installs only once per client', async () => { const client = getTestClient(); preserveRequestData(client); const defaultAdapter = jest.fn().mockResolvedValue({ config: {}, data: 'default', headers: {}, status: 200, statusText: 'OK', }); client.axiosInstance.defaults.adapter = defaultAdapter; installNativeMultipartAdapter(client); const installedAdapter = client.axiosInstance.defaults.adapter; installNativeMultipartAdapter(client); const formData = { _parts: [ [ 'file', { name: 'test.jpg', type: 'image/jpeg', uri: 'file:///tmp/test.jpg', }, ], ], }; await client.axiosInstance.post('/uploads/image', formData); expect(client.axiosInstance.defaults.adapter).toBe(installedAdapter); expect(defaultAdapter).not.toHaveBeenCalled(); expect(NativeHandlers.multipartUpload).toHaveBeenCalled(); }); it('composes explicitly with a custom adapter', async () => { const client = getTestClient(); preserveRequestData(client); const customAdapter = jest.fn().mockResolvedValue({ config: {}, data: 'custom', headers: {}, status: 200, statusText: 'OK', }); client.axiosInstance.defaults.adapter = wrapAxiosAdapterWithNativeMultipart( client, customAdapter, ); const multipartFormData = { _parts: [ [ 'file', { name: 'test.jpg', type: 'image/jpeg', uri: 'file:///tmp/test.jpg', }, ], ], }; await client.axiosInstance.post('/uploads/image', multipartFormData); expect(NativeHandlers.multipartUpload).toHaveBeenCalled(); expect(customAdapter).not.toHaveBeenCalled(); await client.axiosInstance.post('/messages', { text: 'hello' }); expect(customAdapter).toHaveBeenCalled(); }); });