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
text/typescript
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();
});
});