-
Notifications
You must be signed in to change notification settings - Fork 311
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This means that users no longer need to think about polyfilling fetch in their environment. This is especially helpful for node users. This could be considered a breaking change because by taking a ponyfill approach we're no longer relying on the host platform fetch implementation, and that might lead to some unexpected behaviours. In practice it should be fine given the battle tested libs under use here and the simple use-case graphql-request has for them.
- Loading branch information
1 parent
483119e
commit 1610d1e
Showing
6 changed files
with
573 additions
and
141 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import body from 'body-parser' | ||
import express, { Application, Request } from 'express' | ||
import { createServer, Server } from 'http' | ||
import { JsonObject } from 'type-fest' | ||
|
||
type CapturedRequest = Pick<Request, 'headers' | 'method' | 'body'> | ||
|
||
type Context = { | ||
server: Application | ||
nodeServer: Server | ||
url: string | ||
mock: <D extends JsonObject>(data: D) => D & { requests: CapturedRequest[] } | ||
} | ||
|
||
export function setupTestServer() { | ||
const ctx = {} as Context | ||
beforeAll((done) => { | ||
ctx.server = express() | ||
ctx.server.use(body.json()) | ||
ctx.nodeServer = createServer() | ||
ctx.nodeServer.listen({ port: 3210 }) | ||
ctx.url = 'http://localhost:3210' | ||
ctx.nodeServer.on('request', ctx.server) | ||
ctx.nodeServer.once('listening', done) | ||
ctx.mock = (spec) => { | ||
const requests: CapturedRequest[] = [] | ||
ctx.server.use('*', function mock(req, res) { | ||
requests.push({ | ||
method: req.method, | ||
headers: req.headers, | ||
body: req.body, | ||
}) | ||
if (spec.headers) { | ||
Object.entries(spec.headers).forEach(([name, value]) => { | ||
res.setHeader(name, value) | ||
}) | ||
} | ||
res.send(spec.body ?? {}) | ||
}) | ||
return { ...spec, requests } | ||
} | ||
}) | ||
|
||
afterEach(() => { | ||
// https://stackoverflow.com/questions/10378690/remove-route-mappings-in-nodejs-express/28369539#28369539 | ||
ctx.server._router.stack.forEach((item, i) => { | ||
if (item.name === 'mock') ctx.server._router.stack.splice(i, 1) | ||
}) | ||
}) | ||
|
||
afterAll((done) => { | ||
ctx.nodeServer.close(done) | ||
}) | ||
|
||
return ctx | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,139 +1,153 @@ | ||
import * as fetchMock from 'fetch-mock' | ||
import { GraphQLClient, rawRequest, request } from '../src' | ||
import { setupTestServer } from './__helpers' | ||
|
||
const ctx = setupTestServer() | ||
|
||
test('minimal query', async () => { | ||
const data = { | ||
viewer: { | ||
id: 'some-id', | ||
const data = ctx.mock({ | ||
body: { | ||
data: { | ||
viewer: { | ||
id: 'some-id', | ||
}, | ||
}, | ||
}, | ||
} | ||
}).body.data | ||
|
||
mock({ body: { data } }) | ||
expect(await request('https://mock-api.com/graphql', `{ viewer { id } }`)).toEqual(data) | ||
expect(await request(ctx.url, `{ viewer { id } }`)).toEqual(data) | ||
}) | ||
|
||
test('minimal raw query', async () => { | ||
const data = { | ||
viewer: { | ||
id: 'some-id', | ||
const { extensions, data } = ctx.mock({ | ||
body: { | ||
data: { | ||
viewer: { | ||
id: 'some-id', | ||
}, | ||
}, | ||
extensions: { | ||
version: '1', | ||
}, | ||
}, | ||
} | ||
|
||
const extensions = { | ||
version: '1', | ||
} | ||
|
||
mock({ body: { data, extensions } }) | ||
const { headers, ...result } = await rawRequest('https://mock-api.com/graphql', `{ viewer { id } }`) | ||
}).body | ||
const { headers, ...result } = await rawRequest(ctx.url, `{ viewer { id } }`) | ||
expect(result).toEqual({ data, extensions, status: 200 }) | ||
}) | ||
|
||
test('minimal raw query with response headers', async () => { | ||
const data = { | ||
viewer: { | ||
id: 'some-id', | ||
const { | ||
headers: reqHeaders, | ||
body: { data, extensions }, | ||
} = ctx.mock({ | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'X-Custom-Header': 'test-custom-header', | ||
}, | ||
} | ||
|
||
const extensions = { | ||
version: '1', | ||
} | ||
|
||
const reqHeaders = { | ||
'Content-Type': 'application/json', | ||
'X-Custom-Header': 'test-custom-header', | ||
} | ||
|
||
mock({ headers: reqHeaders, body: { data, extensions } }) | ||
const { headers, ...result } = await rawRequest('https://mock-api.com/graphql', `{ viewer { id } }`) | ||
body: { | ||
data: { | ||
viewer: { | ||
id: 'some-id', | ||
}, | ||
}, | ||
extensions: { | ||
version: '1', | ||
}, | ||
}, | ||
}) | ||
const { headers, ...result } = await rawRequest(ctx.url, `{ viewer { id } }`) | ||
|
||
expect(result).toEqual({ data, extensions, status: 200 }) | ||
expect(headers.get('X-Custom-Header')).toEqual(reqHeaders['X-Custom-Header']) | ||
}) | ||
|
||
test('basic error', async () => { | ||
const errors = { | ||
message: 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n', | ||
locations: [ | ||
{ | ||
line: 1, | ||
column: 1, | ||
test('content-type with charset', async () => { | ||
const { data } = ctx.mock({ | ||
// headers: { 'Content-Type': 'application/json; charset=utf-8' }, | ||
body: { | ||
data: { | ||
viewer: { | ||
id: 'some-id', | ||
}, | ||
}, | ||
], | ||
} | ||
}, | ||
}).body | ||
|
||
mock({ body: { errors } }) | ||
expect(() => request('https://mock-api.com/graphql', `x`)).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`"GraphQL Error (Code: 200): {\\"response\\":{\\"errors\\":{\\"message\\":\\"Syntax Error GraphQL request (1:1) Unexpected Name \\\\\\"x\\\\\\"\\\\n\\\\n1: x\\\\n ^\\\\n\\",\\"locations\\":[{\\"line\\":1,\\"column\\":1}]},\\"status\\":200},\\"request\\":{\\"query\\":\\"x\\"}}"` | ||
) | ||
expect(await request(ctx.url, `{ viewer { id } }`)).toEqual(data) | ||
}) | ||
|
||
test('raw request error', async () => { | ||
const errors = { | ||
message: 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n', | ||
locations: [ | ||
{ | ||
line: 1, | ||
column: 1, | ||
test('basic error', async () => { | ||
ctx.mock({ | ||
body: { | ||
errors: { | ||
message: 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n', | ||
locations: [ | ||
{ | ||
line: 1, | ||
column: 1, | ||
}, | ||
], | ||
}, | ||
], | ||
} | ||
}, | ||
}).body | ||
|
||
const res = await request(ctx.url, `x`).catch((x) => x) | ||
|
||
mock({ body: { errors } }) | ||
expect(rawRequest('https://mock-api.com/graphql', `x`)).rejects.toThrowErrorMatchingInlineSnapshot( | ||
`"GraphQL Error (Code: 200): {\\"response\\":{\\"errors\\":{\\"message\\":\\"Syntax Error GraphQL request (1:1) Unexpected Name \\\\\\"x\\\\\\"\\\\n\\\\n1: x\\\\n ^\\\\n\\",\\"locations\\":[{\\"line\\":1,\\"column\\":1}]},\\"status\\":200,\\"headers\\":{\\"_headers\\":{\\"content-type\\":[\\"application/json\\"]}}},\\"request\\":{\\"query\\":\\"x\\"}}"` | ||
expect(res).toMatchInlineSnapshot( | ||
`[Error: GraphQL Error (Code: 200): {"response":{"errors":{"message":"Syntax Error GraphQL request (1:1) Unexpected Name \\"x\\"\\n\\n1: x\\n ^\\n","locations":[{"line":1,"column":1}]},"status":200},"request":{"query":"x"}}]` | ||
) | ||
}) | ||
|
||
test('content-type with charset', async () => { | ||
const data = { | ||
viewer: { | ||
id: 'some-id', | ||
test('basic error with raw request', async () => { | ||
ctx.mock({ | ||
body: { | ||
errors: { | ||
message: 'Syntax Error GraphQL request (1:1) Unexpected Name "x"\n\n1: x\n ^\n', | ||
locations: [ | ||
{ | ||
line: 1, | ||
column: 1, | ||
}, | ||
], | ||
}, | ||
}, | ||
} | ||
|
||
mock({ | ||
headers: { 'Content-Type': 'application/json; charset=utf-8' }, | ||
body: { data }, | ||
}) | ||
expect(await request('https://mock-api.com/graphql', `{ viewer { id } }`)).toEqual(data) | ||
const res = await rawRequest(ctx.url, `x`).catch((x) => x) | ||
expect(res).toMatchInlineSnapshot( | ||
`[Error: GraphQL Error (Code: 200): {"response":{"errors":{"message":"Syntax Error GraphQL request (1:1) Unexpected Name \\"x\\"\\n\\n1: x\\n ^\\n","locations":[{"line":1,"column":1}]},"status":200,"headers":{}},"request":{"query":"x"}}]` | ||
) | ||
}) | ||
|
||
test('extra fetch options', async () => { | ||
// todo needs to be tested in browser environment | ||
// the options under test here aren't used by node-fetch | ||
test.skip('extra fetch options', async () => { | ||
const options: RequestInit = { | ||
credentials: 'include', | ||
mode: 'cors', | ||
cache: 'reload', | ||
} | ||
|
||
const client = new GraphQLClient('https://mock-api.com/graphql', options) | ||
mock({ | ||
const client = new GraphQLClient(ctx.url, options) | ||
const { requests } = ctx.mock({ | ||
body: { data: { test: 'test' } }, | ||
}) | ||
await client.request('{ test }') | ||
const actualOptions = fetchMock.lastCall()[1] | ||
for (let name in options) { | ||
expect(actualOptions[name]).toEqual(options[name]) | ||
} | ||
}) | ||
|
||
/** | ||
* Helpers | ||
*/ | ||
|
||
async function mock(response: any) { | ||
fetchMock.mock({ | ||
matcher: '*', | ||
response: { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
...response.headers, | ||
expect(requests).toMatchInlineSnapshot(` | ||
Array [ | ||
Object { | ||
"body": Object { | ||
"query": "{ test }", | ||
}, | ||
"headers": Object { | ||
"accept": "*/*", | ||
"accept-encoding": "gzip,deflate", | ||
"connection": "close", | ||
"content-length": "20", | ||
"content-type": "application/json", | ||
"host": "localhost:3210", | ||
"user-agent": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)", | ||
}, | ||
"method": "POST", | ||
}, | ||
body: JSON.stringify(response.body), | ||
}, | ||
}) | ||
} | ||
|
||
afterEach(() => { | ||
fetchMock.restore() | ||
] | ||
`) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
1610d1e
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's a breaking change! at least for tests. Any suggestions on how we can still keep using fetch-mock?
1610d1e
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
had to remove fetch-mock... a bit of a headache but am using your __helpers.ts file now...
my changes: cpmech/simple-state@25ca86e