altair-graphql-core
Version:
Several of the core logic for altair graphql client
661 lines • 28 kB
JavaScript
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
import { delay } from 'msw';
import { setupServer } from 'msw/node';
import { HttpRequestHandler } from './http';
import { MswMockRequestHandler } from '../../test-helpers';
const server = setupServer();
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
const testObserver = (o) => {
const values = [];
let s = null;
return new Promise((resolve, reject) => {
s = o.subscribe({
next: (value) => {
values.push(value);
},
error: (err) => {
s?.unsubscribe();
return reject(err);
},
complete: () => {
s?.unsubscribe();
return resolve(values);
},
});
}).finally(() => {
s?.unsubscribe();
});
};
describe('HTTP handler', () => {
it('should properly handle normal successful HTTP requests', async () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/graphql', async () => {
return Response.json({ data: { hello: 'world' } });
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/graphql',
method: 'POST',
additionalParams: {},
headers: [
{
key: 'X-GraphQL-Token',
value: 'asd7-237s-2bdk-nsdk4',
},
],
query: 'query { hello }',
variables: {},
selectedOperation: 'hello',
};
const httpHandler = new HttpRequestHandler();
const res = await testObserver(httpHandler.handle(request));
const receivedRequest = mockHandler.receivedRequest();
expect(receivedRequest?.url).toEqual('http://localhost:4000/graphql');
expect(receivedRequest?.headers.get('X-GraphQL-Token')).toEqual('asd7-237s-2bdk-nsdk4');
expect(await receivedRequest?.json()).toEqual({
query: 'query { hello }',
variables: {},
operationName: 'hello',
});
expect(res).toEqual([
expect.objectContaining({
ok: true,
data: '{"data":{"hello":"world"}}',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/graphql',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
]);
});
it('should properly handle normal successful HTTP GET requests', async () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/graphql', async () => {
return Response.json({ data: { hello: 'world' } });
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/graphql',
method: 'GET',
additionalParams: {
testData: [
{
hello: 'world',
},
],
},
headers: [],
query: 'query { hello }',
variables: {},
selectedOperation: 'hello',
};
const httpHandler = new HttpRequestHandler();
const res = await testObserver(httpHandler.handle(request));
const receivedRequest = mockHandler.receivedRequest();
expect(receivedRequest?.url).toEqual('http://localhost:4000/graphql?query=query+%7B+hello+%7D&variables=%7B%7D&operationName=hello');
expect(receivedRequest?.body).toBeNull();
expect(res).toEqual([
expect.objectContaining({
ok: true,
data: '{"data":{"hello":"world"}}',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/graphql?query=query+%7B+hello+%7D&variables=%7B%7D&operationName=hello',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
]);
});
it('should handle requests with file variables', async () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/graphql', async () => {
return Response.json({ data: { hello: 'world' } });
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/graphql',
method: 'POST',
additionalParams: {
testData: [
{
hello: 'world',
},
],
},
headers: [],
query: 'query { hello }',
variables: {},
files: [
{
data: new File([Buffer.from('asdfghjkl')], 'test.txt'),
name: 'myfile',
},
],
selectedOperation: 'hello',
};
const httpHandler = new HttpRequestHandler();
const res = await testObserver(httpHandler.handle(request));
const receivedRequest = mockHandler.receivedRequest();
expect(receivedRequest?.url).toEqual('http://localhost:4000/graphql');
const formData = await receivedRequest?.formData();
expect(formData?.get('operations')).toEqual(JSON.stringify({
query: 'query { hello }',
variables: { myfile: null },
operationName: 'hello',
}));
expect(formData?.get('map')).toEqual(JSON.stringify({
'0': ['variables.myfile'],
}));
// TODO: figure out why formData.get() returns the file is stringified object `[object File]`
// expect(formData?.get('0')).toEqual(new File(['asdfghjkl'], 'test.txt'));
expect(res).toEqual([
expect.objectContaining({
ok: true,
data: '{"data":{"hello":"world"}}',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/graphql',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
]);
});
it('should properly handle batched requests', async () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/graphql', async () => {
return Response.json([
{ data: { hello: 'world' } },
{ data: { bye: 'longer' } },
]);
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/graphql',
method: 'POST',
additionalParams: {
testData: [
{
hello: 'world',
},
{
bye: 'longer',
},
],
},
headers: [],
query: 'query a { hello } query b { bye }',
variables: {},
selectedOperation: 'hello',
batchedRequest: true,
};
const httpHandler = new HttpRequestHandler();
const res = await testObserver(httpHandler.handle(request));
const receivedRequest = mockHandler.receivedRequest();
expect(await receivedRequest?.json()).toEqual([
{
query: 'query a{hello}',
variables: {},
operationName: 'a',
},
{
query: 'query b{bye}',
variables: {},
operationName: 'b',
},
]);
expect(res).toEqual([
expect.objectContaining({
ok: true,
data: '[{"data":{"hello":"world"}},{"data":{"bye":"longer"}}]',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/graphql',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
]);
});
it('should handle empty response', async () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/graphql', async () => {
return new Response(null, { status: 204 });
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/graphql',
method: 'POST',
additionalParams: {},
headers: [],
query: 'query { hello }',
variables: {},
selectedOperation: 'hello',
};
const httpHandler = new HttpRequestHandler();
const res = await testObserver(httpHandler.handle(request));
expect(res).toEqual([
expect.objectContaining({
ok: true,
data: '',
headers: expect.any(Object),
status: 204,
url: 'http://localhost:4000/graphql',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
]);
});
it('should properly handle normal unsuccessful HTTP GET requests', async () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/graphql', async () => {
return new Response('my data is not found', {
status: 404,
});
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/graphql',
method: 'GET',
additionalParams: {
testData: [
{
hello: 'world',
},
],
},
headers: [],
query: 'query { hello }',
variables: {},
selectedOperation: 'hello',
};
const httpHandler = new HttpRequestHandler();
const res = await testObserver(httpHandler.handle(request));
const receivedRequest = mockHandler.receivedRequest();
expect(receivedRequest?.url).toEqual('http://localhost:4000/graphql?query=query+%7B+hello+%7D&variables=%7B%7D&operationName=hello');
expect(receivedRequest?.body).toBeNull();
expect(res).toEqual([
expect.objectContaining({
ok: false,
data: 'my data is not found',
headers: expect.any(Object),
status: 404,
url: 'http://localhost:4000/graphql?query=query+%7B+hello+%7D&variables=%7B%7D&operationName=hello',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
]);
});
it('should properly handle failed HTTP requests', async () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/error', async () => {
return Response.error();
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/error',
method: 'POST',
additionalParams: {
testData: [
{
hello: 'world',
},
],
},
headers: [],
query: 'query { hello }',
variables: {},
selectedOperation: 'hello',
};
const httpHandler = new HttpRequestHandler();
await expect(testObserver(httpHandler.handle(request))).rejects.toThrow();
});
it('should properly handle aborting the request', () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/delay', async () => {
await delay(1000);
return Response.json({ data: { hello: 'world' } });
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/delay',
method: 'POST',
additionalParams: {
testData: [
{
hello: 'world',
},
],
},
headers: [],
query: 'query { hello }',
variables: {},
selectedOperation: 'hello',
};
const httpHandler = new HttpRequestHandler();
const res = httpHandler.handle(request);
res.subscribe().unsubscribe();
});
it('should properly handle streamed responses', async () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/simple-stream', async () => {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
await delay(10);
controller.enqueue(encoder.encode('{"data":{"hello":"world"}}'));
await delay(10);
controller.enqueue(encoder.encode('{"data":{"bye":"longer"}}'));
await delay(10);
controller.enqueue(encoder.encode('{"data":{"rest":"afva"}}'));
await delay(10);
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'application/json',
},
});
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/simple-stream',
method: 'POST',
additionalParams: {
testData: [
{
hello: 'world',
},
],
},
headers: [],
query: 'query { hello }',
variables: {},
selectedOperation: 'hello',
};
const httpHandler = new HttpRequestHandler();
const res = await testObserver(httpHandler.handle(request));
expect(res).toEqual([
expect.objectContaining({
ok: true,
data: '{"data":{"hello":"world"}}',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/simple-stream',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
expect.objectContaining({
ok: true,
data: '{"data":{"bye":"longer"}}',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/simple-stream',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
expect.objectContaining({
ok: true,
data: '{"data":{"rest":"afva"}}',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/simple-stream',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
]);
});
it('should properly handle streamed responses with errors', async () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/error-stream', async () => {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
await delay(10);
controller.enqueue(encoder.encode('{"data":{"hello":"world"}}'));
await delay(10);
controller.error('random stream error');
await delay(10);
controller.enqueue(encoder.encode('{"data":{"bye":"longer"}}'));
await delay(10);
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'application/json',
},
});
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/error-stream',
method: 'POST',
additionalParams: {
testData: [
{
hello: 'world',
},
],
},
headers: [],
query: 'query { hello }',
variables: {},
selectedOperation: 'hello',
};
const httpHandler = new HttpRequestHandler();
try {
await testObserver(httpHandler.handle(request));
expect(true).toBe(false); // it should not get here
}
catch (err) {
expect(err).toBe('random stream error');
}
});
it('should properly handle multipart streamed responses', async () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/multipart-stream', async () => {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
await delay(10);
controller.enqueue(encoder.encode(`---\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 108\r\n\r\n{"data":{"hello":"Hello world","alphabet":[],"fastField":"This field resolves fast! ⚡️"},"hasNext":true}\r\n---\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 70\r\n\r\n{"incremental":[{"items":["a"],"path":["alphabet",0]}],"hasNext":true}\r\n---`));
await delay(10);
controller.enqueue(encoder.encode('\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 70\r\n\r\n{"incremental":[{"items":["b"],"path":["alphabet",1]}],"hasNext":true}\r\n---'));
await delay(10);
controller.enqueue(encoder.encode('\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 70\r\n\r\n{"incremental":[{"items":["c"],"path":["alphabet",2]}],"hasNext":true}\r\n---'));
await delay(10);
controller.enqueue(encoder.encode('\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 70\r\n\r\n{"incremental":[{"items":["d"],"path":["alphabet",3]}],"hasNext":true}\r\n---'));
await delay(10);
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'multipart/mixed; boundary=-',
},
});
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/multipart-stream',
method: 'POST',
additionalParams: {
testData: [
{
hello: 'world',
},
],
},
headers: [],
query: 'query { hello }',
variables: {},
selectedOperation: 'hello',
};
const httpHandler = new HttpRequestHandler();
const res = await testObserver(httpHandler.handle(request));
expect(res).toEqual([
expect.objectContaining({
ok: true,
data: '{"data":{"hello":"Hello world","alphabet":[],"fastField":"This field resolves fast! ⚡️"},"hasNext":true}',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/multipart-stream',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
expect.objectContaining({
ok: true,
data: '{"incremental":[{"items":["a"],"path":["alphabet",0]}],"hasNext":true}',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/multipart-stream',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
expect.objectContaining({
ok: true,
data: '{"incremental":[{"items":["b"],"path":["alphabet",1]}],"hasNext":true}',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/multipart-stream',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
expect.objectContaining({
ok: true,
data: '{"incremental":[{"items":["c"],"path":["alphabet",2]}],"hasNext":true}',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/multipart-stream',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
expect.objectContaining({
ok: true,
data: '{"incremental":[{"items":["d"],"path":["alphabet",3]}],"hasNext":true}',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/multipart-stream',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
]);
});
// https://github.com/felipe-gdr/spring-graphql-defer/issues/5
it('should properly handle multipart streamed responses - sample 2', async () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/multipart-stream-2', async () => {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
await delay(10);
// this is invalid since there is no boundary before the first content, so it should be ignored as the preamble as per the spec
controller.enqueue(encoder.encode(`\r\ncontent-type: application/json; charset=utf-8\r\n\r\n{"data":{"bookById":{"name":"Effective Java"}},"hasNext":true}\r\n---`));
await delay(10);
controller.enqueue(encoder.encode(`\r\ncontent-type: application/json; charset=utf-8\r\n\r\n{"hasNext":true,"incremental":[{"path":["bookById"],"data":{"author":{"firstName":"Joshua"}}}]}\r\n---`));
await delay(10);
controller.enqueue(encoder.encode(`content-type: application/json; charset=utf-8\r\n\r\n{"hasNext":false,"incremental":[{"path":[],"data":{"book2":{"name":"Hitchhiker's Guide to the Galaxy"}}}]}\r\n-----`));
await delay(10);
controller.close();
},
});
return new Response(stream, {
headers: {
'content-type': 'multipart/mixed; boundary="-"',
},
});
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/multipart-stream-2',
method: 'POST',
additionalParams: {
testData: [
{
hello: 'world',
},
],
},
headers: [],
query: 'query { hello }',
variables: {},
selectedOperation: 'hello',
};
const httpHandler = new HttpRequestHandler();
const res = await testObserver(httpHandler.handle(request));
expect(res).toEqual([
expect.objectContaining({
ok: true,
data: '{"hasNext":true,"incremental":[{"path":["bookById"],"data":{"author":{"firstName":"Joshua"}}}]}',
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/multipart-stream-2',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
expect.objectContaining({
ok: true,
data: `{"hasNext":false,"incremental":[{"path":[],"data":{"book2":{"name":"Hitchhiker's Guide to the Galaxy"}}}]}`,
headers: expect.any(Object),
status: 200,
url: 'http://localhost:4000/multipart-stream-2',
requestStartTimestamp: expect.any(Number),
requestEndTimestamp: expect.any(Number),
responseTimeMs: expect.any(Number),
}),
]);
});
it('should properly handle multipart streamed responses with errors', async () => {
const mockHandler = new MswMockRequestHandler('http://localhost:4000/error-multipart-stream', async () => {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
await delay(10);
controller.enqueue(encoder.encode(`---\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 108\r\n\r\n{"data":{"hello":"Hello world","alphabet":[],"fastField":"This field resolves fast! ⚡️"},"hasNext":true}\r\n---\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 70\r\n\r\n{"incremental":[{"items":["a"],"path":["alphabet",0]}],"hasNext":true}\r\n---`));
await delay(10);
controller.error('random stream error');
await delay(10);
controller.enqueue(encoder.encode('\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 70\r\n\r\n{"incremental":[{"items":["b"],"path":["alphabet",1]}],"hasNext":true}\r\n---'));
await delay(10);
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'multipart/mixed; boundary=-',
},
});
});
server.use(mockHandler);
const request = {
url: 'http://localhost:4000/error-multipart-stream',
method: 'POST',
additionalParams: {
testData: [
{
hello: 'world',
},
],
},
headers: [],
query: 'query { hello }',
variables: {},
selectedOperation: 'hello',
};
const httpHandler = new HttpRequestHandler();
try {
await testObserver(httpHandler.handle(request));
expect(true).toBe(false); // it should not get here
}
catch (err) {
expect(err).toBe('random stream error');
}
});
});
//# sourceMappingURL=http.spec.js.map