react-relay-network-modern
Version:
Network Layer for React Relay and Express (Batch Queries, AuthToken, Logging, Retry)
601 lines • 16.7 kB
JavaScript
import fetchMock from 'fetch-mock';
import FormData from 'form-data';
import { RelayNetworkLayer, batchMiddleware } from '../..';
import { mockReq, mockReqWithSize, mockMutationReq, mockReqWithFiles } from '../../__mocks__/mockReq';
global.FormData = FormData;
describe('middlewares/batch', () => {
beforeEach(() => {
fetchMock.restore();
});
it('should make a successfull single request', async () => {
fetchMock.post('/graphql', {
data: {
ok: 1
}
});
const rnl = new RelayNetworkLayer([batchMiddleware()]);
const req = mockReq(1);
const res = await req.execute(rnl);
expect(res.data).toEqual({
ok: 1
});
expect(fetchMock.lastOptions()).toMatchSnapshot();
});
it('should make a successfully batch request', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
data: {
ok: 1
}
}, {
data: {
ok: 2
}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware()]);
const req1 = mockReq(1);
const req2 = mockReq(2);
const [res1, res2] = await Promise.all([req1.execute(rnl), req2.execute(rnl)]);
expect(res1.data).toEqual({
ok: 1
});
expect(res2.data).toEqual({
ok: 2
});
expect(fetchMock.lastOptions()).toMatchSnapshot();
});
it('should make a successfully batch request with duplicate request payloads', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
data: {
ok: 1
}
}, {
data: {
ok: 2
}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware()]);
const req1 = mockReq(2);
const req2 = mockReq(2, {
query: 'duplicate',
variables: {
duplicate: true
}
});
const req3 = mockReq(2, {
query: 'duplicate',
variables: {
duplicate: true
}
});
const [res1, res2, res3] = await Promise.all([req1.execute(rnl), req2.execute(rnl), req3.execute(rnl)]);
expect(res1.data).toEqual({
ok: 1
});
expect(res2.data).toEqual({
ok: 2
});
expect(res3.data).toEqual({
ok: 2
});
expect(fetchMock.lastOptions()).toMatchSnapshot();
});
it('should reject if server does not return response for duplicate request payloads', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
data: {
ok: 1
}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware()]);
const req1 = mockReq(1);
const req2 = mockReq(2);
const req3 = mockReq(3);
const [res1, res2, res3] = await Promise.all([req1.execute(rnl), req2.execute(rnl).catch(e => e), req3.execute(rnl).catch(e => e)]);
expect(res1.data).toEqual({
ok: 1
});
expect(res2).toBeInstanceOf(Error);
expect(res2.toString()).toMatch('Server does not return response for request');
expect(res3).toBeInstanceOf(Error);
expect(res3.toString()).toMatch('Server does not return response for request');
expect(fetchMock.lastOptions()).toMatchSnapshot();
});
it('should handle network failure', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
throws: new Error('Network connection error')
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware()]);
const req1 = mockReq(1);
const req2 = mockReq(2);
const [res1, res2] = await Promise.all([req1.execute(rnl).catch(e => e), req2.execute(rnl).catch(e => e)]);
expect(res1).toBeInstanceOf(Error);
expect(res1.toString()).toMatch('Network connection error');
expect(res2).toBeInstanceOf(Error);
expect(res2.toString()).toMatch('Network connection error');
expect(fetchMock.lastOptions()).toMatchSnapshot();
});
it('should handle server errors for one request', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
payload: {
errors: [{
location: 1,
message: 'major error'
}]
}
}, {
payload: {
data: {
ok: 2
}
}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware()]);
const req1 = mockReq(1);
const req2 = mockReq(2);
// prettier-ignore
const [res1, res2] = await Promise.all([req1.execute(rnl).catch(e => e), req2.execute(rnl)]);
expect(res1).toBeInstanceOf(Error);
expect(res1.toString()).toMatch('major error');
expect(res2.data).toEqual({
ok: 2
});
expect(res2.errors).toBeUndefined();
expect(fetchMock.lastOptions()).toMatchSnapshot();
});
it('should handle server errors for all requests', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: {
errors: [{
location: 1,
message: 'major error'
}]
}
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware()]);
const req1 = mockReq(1);
const req2 = mockReq(2);
const req3 = mockReq(3);
const [res1, res2, res3] = await Promise.all([req1.execute(rnl).catch(e => e), req2.execute(rnl).catch(e => e), req3.execute(rnl).catch(e => e)]);
expect(res1.toString()).toMatch('Wrong response');
expect(res2.toString()).toMatch('Wrong response');
expect(res3.toString()).toMatch('Wrong response');
expect(fetchMock.lastOptions()).toMatchSnapshot();
});
it('should handle responses without payload wrapper', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
errors: [{
location: 1,
message: 'major error'
}]
}, {
data: {
ok: 2
}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware()]);
const req1 = mockReq(1);
const req2 = mockReq(2);
// prettier-ignore
const [res1, res2] = await Promise.all([req1.execute(rnl).catch(e => e), req2.execute(rnl)]);
expect(res1).toBeInstanceOf(Error);
expect(res1.toString()).toMatch('major error');
expect(res2.data).toEqual({
ok: 2
});
expect(res2.errors).toBeUndefined();
expect(fetchMock.lastOptions()).toMatchSnapshot();
});
it('should not batch requests cacheConfig `skipBatch=true`', async () => {
fetchMock.mock({
matcher: '/graphql',
response: {
status: 200,
body: {
data: {
ok: 1
}
}
},
method: 'POST'
});
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
data: {
ok: 2
}
}, {
data: {
ok: 3
}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware()]);
const req1 = mockReq(1, {
cacheConfig: {
skipBatch: true
}
});
const req2 = mockReq(2);
const req3 = mockReq(3);
const [res1, res2, res3] = await Promise.all([req1.execute(rnl), req2.execute(rnl), req3.execute(rnl)]);
expect(res1.data).toEqual({
ok: 1
});
expect(res2.data).toEqual({
ok: 2
});
expect(res3.data).toEqual({
ok: 3
});
});
describe('option `batchTimeout`', () => {
beforeEach(() => {
fetchMock.restore();
});
it('should gather different requests into one batch request', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
data: {}
}, {
data: {}
}, {
data: {}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware({
batchTimeout: 50
})]);
mockReq(1).execute(rnl);
setTimeout(() => mockReq(2).execute(rnl), 30);
await mockReq(3).execute(rnl);
const reqs = fetchMock.calls('/graphql/batch');
expect(reqs).toHaveLength(1);
expect(fetchMock.lastOptions()).toMatchSnapshot();
});
it('should gather different requests into two batch request', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
data: {}
}, {
data: {}
}, {
data: {}
}, {
data: {}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware({
batchTimeout: 100
})]);
mockReq(1).execute(rnl);
setTimeout(() => mockReq(2).execute(rnl), 160);
setTimeout(() => mockReq(3).execute(rnl), 170);
mockReq(4).execute(rnl);
await new Promise((resolve, reject) => {
setTimeout(() => {
try {
const reqs = fetchMock.calls('/graphql/batch');
expect(reqs).toHaveLength(2);
expect(fetchMock.calls('/graphql/batch')).toMatchSnapshot();
resolve();
} catch (e) {
reject(e);
}
}, 300);
});
});
});
describe('option `maxBatchSize`', () => {
beforeEach(() => {
fetchMock.restore();
});
it('should split large batched requests into multiple requests', async () => {
fetchMock.mock({
matcher: '/graphql',
response: {
status: 200,
body: {
data: {}
}
},
method: 'POST'
});
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
data: {}
}, {
data: {}
}, {
data: {}
}, {
data: {}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware({
maxBatchSize: 1024 * 10
})]);
const req1 = mockReqWithSize(1, 1024 * 7);
const req2 = mockReqWithSize(2, 1024 * 2);
const req3 = mockReqWithSize(3, 1024 * 5);
const req4 = mockReqWithSize(4, 1024 * 4);
const req5 = mockReqWithSize(5, 1024 * 11);
await Promise.all([req1.execute(rnl), req2.execute(rnl), req3.execute(rnl), req4.execute(rnl), req5.execute(rnl)]);
const batchReqs = fetchMock.calls('/graphql/batch');
const singleReqs = fetchMock.calls('/graphql');
expect(batchReqs).toHaveLength(2);
expect(singleReqs).toHaveLength(1);
});
});
describe('option `allowMutations`', () => {
beforeEach(() => {
fetchMock.restore();
});
it('should not batch mutations by default', async () => {
fetchMock.mock({
matcher: '/graphql',
response: {
status: 200,
body: {
data: {}
}
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware({
batchTimeout: 20
})]);
mockMutationReq(1).execute(rnl);
await mockMutationReq(1).execute(rnl);
const singleReqs = fetchMock.calls('/graphql');
expect(singleReqs).toHaveLength(2);
expect(fetchMock.calls('/graphql')).toMatchSnapshot();
});
it('should not batch requests with FormData', async () => {
fetchMock.mock({
matcher: '/graphql',
response: {
status: 200,
body: {
data: {}
}
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware({
batchTimeout: 20
})]);
mockReqWithFiles(1).execute(rnl);
await mockReqWithFiles(1).execute(rnl);
const singleReqs = fetchMock.calls('/graphql');
expect(singleReqs).toHaveLength(2);
});
it('should batch mutations if `allowMutations=true`', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
data: {}
}, {
data: {}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware({
batchTimeout: 20,
allowMutations: true
})]);
const req1 = mockMutationReq(1);
req1.execute(rnl);
const req2 = mockMutationReq(2);
await req2.execute(rnl);
const batchReqs = fetchMock.calls('/graphql/batch');
expect(batchReqs).toHaveLength(1);
expect(fetchMock.lastOptions()).toMatchSnapshot();
});
it('should not batch mutations with files if `allowMutations=true`', async () => {
fetchMock.mock({
matcher: '/graphql',
response: {
status: 200,
body: {
data: {}
}
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware({
batchTimeout: 20,
allowMutations: true
})]);
const req1 = mockMutationReq(1, {
files: {
file1: 'data'
}
});
req1.execute(rnl);
const req2 = mockMutationReq(2, {
files: {
file1: 'data'
}
});
await req2.execute(rnl);
const singleReqs = fetchMock.calls('/graphql');
expect(singleReqs).toHaveLength(2);
});
});
it('should pass fetch options', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
data: {}
}, {
data: {}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware({
batchTimeout: 20,
credentials: 'include',
mode: 'cors',
cache: 'no-store',
redirect: 'follow'
})]);
const req1 = mockReq(1);
req1.execute(rnl);
const req2 = mockReq(2);
await req2.execute(rnl);
const batchReqs = fetchMock.calls('/graphql/batch');
expect(batchReqs).toHaveLength(1);
expect(fetchMock.lastOptions()).toEqual(expect.objectContaining({
credentials: 'include',
mode: 'cors',
cache: 'no-store',
redirect: 'follow'
}));
});
describe('headers option', () => {
it('`headers` option as Object', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
data: {}
}, {
data: {}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware({
headers: {
'custom-header': '123'
}
})]);
const req1 = mockReq(1);
const req2 = mockReq(2);
await Promise.all([req1.execute(rnl), req2.execute(rnl)]);
expect(fetchMock.lastOptions().headers).toEqual(expect.objectContaining({
'custom-header': '123'
}));
});
it('`headers` option as thunk', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
data: {}
}, {
data: {}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware({
headers: () => ({
'thunk-header': '333'
})
})]);
const req1 = mockReq(1);
const req2 = mockReq(2);
await Promise.all([req1.execute(rnl), req2.execute(rnl)]);
expect(fetchMock.lastOptions().headers).toEqual(expect.objectContaining({
'thunk-header': '333'
}));
});
it('`headers` option as thunk with Promise', async () => {
fetchMock.mock({
matcher: '/graphql/batch',
response: {
status: 200,
body: [{
data: {}
}, {
data: {}
}]
},
method: 'POST'
});
const rnl = new RelayNetworkLayer([batchMiddleware({
headers: () => Promise.resolve({
'thunk-header': 'as promise'
})
})]);
const req1 = mockReq(1);
const req2 = mockReq(2);
await Promise.all([req1.execute(rnl), req2.execute(rnl)]);
expect(fetchMock.lastOptions().headers).toEqual(expect.objectContaining({
'thunk-header': 'as promise'
}));
});
});
});