graphql-http 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -1
- package/README.md +127 -114
- package/lib/audits/common.d.mts +58 -0
- package/lib/audits/index.d.mts +2 -0
- package/lib/audits/server.d.mts +39 -0
- package/lib/audits/server.js +237 -32
- package/lib/audits/server.mjs +238 -33
- package/lib/audits/utils.d.mts +38 -0
- package/lib/audits/utils.d.ts +11 -0
- package/lib/audits/utils.js +33 -1
- package/lib/audits/utils.mjs +31 -0
- package/lib/client.d.mts +155 -0
- package/lib/common.d.mts +31 -0
- package/lib/handler.d.mts +287 -0
- package/lib/handler.d.ts +26 -24
- package/lib/handler.js +17 -7
- package/lib/handler.mjs +18 -8
- package/lib/index.d.mts +4 -0
- package/lib/index.mjs +1 -1
- package/lib/use/express.d.mts +21 -0
- package/lib/use/express.d.ts +21 -0
- package/lib/use/express.js +73 -0
- package/lib/use/express.mjs +69 -0
- package/lib/use/fastify.d.mts +21 -0
- package/lib/use/fastify.d.ts +21 -0
- package/lib/use/fastify.js +69 -0
- package/lib/use/fastify.mjs +65 -0
- package/lib/use/fetch.d.mts +40 -0
- package/lib/use/fetch.d.ts +40 -0
- package/lib/use/fetch.js +81 -0
- package/lib/use/fetch.mjs +77 -0
- package/lib/use/node.d.mts +21 -0
- package/lib/use/node.d.ts +21 -0
- package/lib/use/node.js +72 -0
- package/lib/use/node.mjs +68 -0
- package/lib/utils.d.mts +16 -0
- package/package.json +48 -22
- package/umd/graphql-http.js +0 -2
- package/umd/graphql-http.min.js +1 -1
- package/umd/graphql-http.min.js.gz +0 -0
package/lib/audits/server.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* audit/server
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
|
-
import { assert, audit, extendedTypeof } from './utils.mjs';
|
|
6
|
+
import { assert, assertBodyAsExecutionResult, audit, extendedTypeof, } from './utils.mjs';
|
|
7
7
|
/**
|
|
8
8
|
* List of server audits required to check GraphQL over HTTP spec conformance.
|
|
9
9
|
*
|
|
@@ -13,7 +13,9 @@ export function serverAudits(opts) {
|
|
|
13
13
|
const fetchFn = (opts.fetchFn || fetch);
|
|
14
14
|
return [
|
|
15
15
|
// Media Types
|
|
16
|
-
audit(
|
|
16
|
+
audit(
|
|
17
|
+
// TODO: convert to MUST after watershed
|
|
18
|
+
'SHOULD accept application/graphql-response+json and match the content-type', async () => {
|
|
17
19
|
const url = new URL(opts.url);
|
|
18
20
|
url.searchParams.set('query', '{ __typename }');
|
|
19
21
|
const res = await fetchFn(url.toString(), {
|
|
@@ -35,7 +37,9 @@ export function serverAudits(opts) {
|
|
|
35
37
|
assert('Status code', res.status).toBe(200);
|
|
36
38
|
assert('Content-Type header', res.headers.get('content-type')).toContain('application/json');
|
|
37
39
|
}),
|
|
38
|
-
audit(
|
|
40
|
+
audit(
|
|
41
|
+
// TODO: convert to MUST after watershed
|
|
42
|
+
'SHOULD accept */* and use application/graphql-response+json for the content-type', async () => {
|
|
39
43
|
const url = new URL(opts.url);
|
|
40
44
|
url.searchParams.set('query', '{ __typename }');
|
|
41
45
|
const res = await fetchFn(url.toString(), {
|
|
@@ -46,30 +50,61 @@ export function serverAudits(opts) {
|
|
|
46
50
|
assert('Status code', res.status).toBe(200);
|
|
47
51
|
assert('Content-Type header', res.headers.get('content-type')).toContain('application/graphql-response+json');
|
|
48
52
|
}),
|
|
49
|
-
audit(
|
|
53
|
+
audit(
|
|
54
|
+
// TODO: convert to MUST after watershed
|
|
55
|
+
'SHOULD assume application/graphql-response+json content-type when accept is missing', async () => {
|
|
50
56
|
const url = new URL(opts.url);
|
|
51
57
|
url.searchParams.set('query', '{ __typename }');
|
|
52
58
|
const res = await fetchFn(url.toString());
|
|
53
59
|
assert('Status code', res.status).toBe(200);
|
|
54
60
|
assert('Content-Type header', res.headers.get('content-type')).toContain('application/graphql-response+json');
|
|
55
61
|
}),
|
|
56
|
-
audit('MUST use utf-8
|
|
62
|
+
audit('MUST use utf-8 encoding when responding', async () => {
|
|
57
63
|
const url = new URL(opts.url);
|
|
58
64
|
url.searchParams.set('query', '{ __typename }');
|
|
59
65
|
const res = await fetchFn(url.toString());
|
|
60
66
|
assert('Status code', res.status).toBe(200);
|
|
61
|
-
|
|
67
|
+
// has charset set to utf-8
|
|
68
|
+
try {
|
|
69
|
+
assert('Content-Type header', res.headers.get('content-type')).toContain('charset=utf-8');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
catch (_a) {
|
|
73
|
+
// noop, continue
|
|
74
|
+
}
|
|
75
|
+
// has no charset specified
|
|
76
|
+
assert('Content-Type header', res.headers.get('content-type')).notToContain('charset');
|
|
77
|
+
// and the content is utf-8 encoded
|
|
78
|
+
try {
|
|
79
|
+
const decoder = new TextDecoder('utf-8');
|
|
80
|
+
const decoded = decoder.decode(await res.arrayBuffer());
|
|
81
|
+
assert('UTF-8 decoded body', decoded).toBe('{"data":{"__typename":"Query"}}');
|
|
82
|
+
}
|
|
83
|
+
catch (_b) {
|
|
84
|
+
throw 'Body is not UTF-8 encoded';
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
audit('MUST accept utf-8 encoding', async () => {
|
|
88
|
+
const url = new URL(opts.url);
|
|
89
|
+
url.searchParams.set('query', '{ __typename }');
|
|
90
|
+
const res = await fetchFn(url.toString(), {
|
|
91
|
+
headers: {
|
|
92
|
+
'content-type': 'application/json; charset=utf-8',
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
assert('Status code', res.status).toBe(200);
|
|
96
|
+
assert('Content-Type header', res.headers.get('content-type')).toContain('utf-8');
|
|
62
97
|
}),
|
|
63
|
-
audit('MUST
|
|
98
|
+
audit('MUST assume utf-8 if encoding is unspecified', async () => {
|
|
64
99
|
const url = new URL(opts.url);
|
|
65
100
|
url.searchParams.set('query', '{ __typename }');
|
|
66
101
|
const res = await fetchFn(url.toString(), {
|
|
67
102
|
headers: {
|
|
68
|
-
|
|
103
|
+
'content-type': 'application/json',
|
|
69
104
|
},
|
|
70
105
|
});
|
|
71
|
-
assert('Status code', res.status).toBe(
|
|
72
|
-
assert('
|
|
106
|
+
assert('Status code', res.status).toBe(200);
|
|
107
|
+
assert('Content-Type header', res.headers.get('content-type')).toContain('utf-8');
|
|
73
108
|
}),
|
|
74
109
|
// Request
|
|
75
110
|
audit('MUST accept POST requests', async () => {
|
|
@@ -103,7 +138,7 @@ export function serverAudits(opts) {
|
|
|
103
138
|
method: 'POST',
|
|
104
139
|
});
|
|
105
140
|
assert('Status code', res.status).toBeGreaterThanOrEqual(400);
|
|
106
|
-
assert('Status code', res.status).toBeLessThanOrEqual(
|
|
141
|
+
assert('Status code', res.status).toBeLessThanOrEqual(499);
|
|
107
142
|
}),
|
|
108
143
|
audit('MUST accept application/json POST requests', async () => {
|
|
109
144
|
const res = await fetchFn(opts.url, {
|
|
@@ -121,7 +156,9 @@ export function serverAudits(opts) {
|
|
|
121
156
|
assert('Status code', res.status).toBe(400);
|
|
122
157
|
}),
|
|
123
158
|
// Request Parameters
|
|
124
|
-
audit(
|
|
159
|
+
audit(
|
|
160
|
+
// TODO: convert to MUST after watershed
|
|
161
|
+
'SHOULD use 400 status code on missing {query} parameter when accepting application/graphql-response+json', async () => {
|
|
125
162
|
const res = await fetchFn(opts.url, {
|
|
126
163
|
method: 'POST',
|
|
127
164
|
headers: {
|
|
@@ -132,7 +169,21 @@ export function serverAudits(opts) {
|
|
|
132
169
|
});
|
|
133
170
|
assert('Status code', res.status).toBe(400);
|
|
134
171
|
}),
|
|
135
|
-
|
|
172
|
+
audit('SHOULD use 200 status code with errors field on missing {query} parameter when accepting application/json', async () => {
|
|
173
|
+
const res = await fetchFn(opts.url, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
'content-type': 'application/json',
|
|
177
|
+
accept: 'application/json',
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify({ notquery: '{ __typename }' }),
|
|
180
|
+
});
|
|
181
|
+
assert('Status code', res.status).toBe(200);
|
|
182
|
+
assert('Execution result', await assertBodyAsExecutionResult(res)).toHaveProperty('errors');
|
|
183
|
+
}),
|
|
184
|
+
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid) => audit(
|
|
185
|
+
// TODO: convert to MUST after watershed
|
|
186
|
+
`SHOULD use 400 status code on ${extendedTypeof(invalid)} {query} parameter when accepting application/graphql-response+json`, async () => {
|
|
136
187
|
const res = await fetchFn(opts.url, {
|
|
137
188
|
method: 'POST',
|
|
138
189
|
headers: {
|
|
@@ -145,7 +196,23 @@ export function serverAudits(opts) {
|
|
|
145
196
|
});
|
|
146
197
|
assert('Status code', res.status).toBe(400);
|
|
147
198
|
})),
|
|
148
|
-
audit(
|
|
199
|
+
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid) => audit(`SHOULD use 200 status code with errors field on ${extendedTypeof(invalid)} {query} parameter when accepting application/json`, async () => {
|
|
200
|
+
const res = await fetchFn(opts.url, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: {
|
|
203
|
+
'content-type': 'application/json',
|
|
204
|
+
accept: 'application/json',
|
|
205
|
+
},
|
|
206
|
+
body: JSON.stringify({
|
|
207
|
+
query: invalid,
|
|
208
|
+
}),
|
|
209
|
+
});
|
|
210
|
+
assert('Status code', res.status).toBe(200);
|
|
211
|
+
assert('Execution result', await assertBodyAsExecutionResult(res)).toHaveProperty('errors');
|
|
212
|
+
})),
|
|
213
|
+
audit(
|
|
214
|
+
// TODO: convert to MUST after watershed
|
|
215
|
+
'SHOULD allow string {query} parameter when accepting application/graphql-response+json', async () => {
|
|
149
216
|
const res = await fetchFn(opts.url, {
|
|
150
217
|
method: 'POST',
|
|
151
218
|
headers: {
|
|
@@ -158,7 +225,23 @@ export function serverAudits(opts) {
|
|
|
158
225
|
});
|
|
159
226
|
assert('Status code', res.status).toBe(200);
|
|
160
227
|
}),
|
|
161
|
-
|
|
228
|
+
audit('MUST allow string {query} parameter when accepting application/json', async () => {
|
|
229
|
+
const res = await fetchFn(opts.url, {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: {
|
|
232
|
+
'content-type': 'application/json',
|
|
233
|
+
accept: 'application/json',
|
|
234
|
+
},
|
|
235
|
+
body: JSON.stringify({
|
|
236
|
+
query: '{ __typename }',
|
|
237
|
+
}),
|
|
238
|
+
});
|
|
239
|
+
assert('Status code', res.status).toBe(200);
|
|
240
|
+
assert('Execution result', await assertBodyAsExecutionResult(res)).notToHaveProperty('errors');
|
|
241
|
+
}),
|
|
242
|
+
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid) => audit(
|
|
243
|
+
// TODO: convert to MUST after watershed
|
|
244
|
+
`SHOULD use 400 status code on ${extendedTypeof(invalid)} {operationName} parameter when accepting application/graphql-response+json`, async () => {
|
|
162
245
|
const res = await fetchFn(opts.url, {
|
|
163
246
|
method: 'POST',
|
|
164
247
|
headers: {
|
|
@@ -172,7 +255,24 @@ export function serverAudits(opts) {
|
|
|
172
255
|
});
|
|
173
256
|
assert('Status code', res.status).toBe(400);
|
|
174
257
|
})),
|
|
175
|
-
audit(
|
|
258
|
+
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid) => audit(`SHOULD use 200 status code with errors field on ${extendedTypeof(invalid)} {operationName} parameter when accepting application/json`, async () => {
|
|
259
|
+
const res = await fetchFn(opts.url, {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
headers: {
|
|
262
|
+
'content-type': 'application/json',
|
|
263
|
+
accept: 'application/json',
|
|
264
|
+
},
|
|
265
|
+
body: JSON.stringify({
|
|
266
|
+
operationName: invalid,
|
|
267
|
+
query: '{ __typename }',
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
270
|
+
assert('Status code', res.status).toBe(200);
|
|
271
|
+
assert('Execution result', await assertBodyAsExecutionResult(res)).toHaveProperty('errors');
|
|
272
|
+
})),
|
|
273
|
+
audit(
|
|
274
|
+
// TODO: convert to MUST after watershed
|
|
275
|
+
'SHOULD allow string {operationName} parameter when accepting application/graphql-response+json', async () => {
|
|
176
276
|
const res = await fetchFn(opts.url, {
|
|
177
277
|
method: 'POST',
|
|
178
278
|
headers: {
|
|
@@ -186,7 +286,24 @@ export function serverAudits(opts) {
|
|
|
186
286
|
});
|
|
187
287
|
assert('Status code', res.status).toBe(200);
|
|
188
288
|
}),
|
|
189
|
-
|
|
289
|
+
audit('MUST allow string {operationName} parameter when accepting application/json', async () => {
|
|
290
|
+
const res = await fetchFn(opts.url, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
headers: {
|
|
293
|
+
'content-type': 'application/json',
|
|
294
|
+
accept: 'application/json',
|
|
295
|
+
},
|
|
296
|
+
body: JSON.stringify({
|
|
297
|
+
operationName: 'Query',
|
|
298
|
+
query: 'query Query { __typename }',
|
|
299
|
+
}),
|
|
300
|
+
});
|
|
301
|
+
assert('Status code', res.status).toBe(200);
|
|
302
|
+
assert('Execution result', await assertBodyAsExecutionResult(res)).notToHaveProperty('errors');
|
|
303
|
+
}),
|
|
304
|
+
...['string', 0, false, ['array']].map((invalid) => audit(
|
|
305
|
+
// TODO: convert to MUST after watershed
|
|
306
|
+
`SHOULD use 400 status code on ${extendedTypeof(invalid)} {variables} parameter when accepting application/graphql-response+json`, async () => {
|
|
190
307
|
const res = await fetchFn(opts.url, {
|
|
191
308
|
method: 'POST',
|
|
192
309
|
headers: {
|
|
@@ -200,7 +317,24 @@ export function serverAudits(opts) {
|
|
|
200
317
|
});
|
|
201
318
|
assert('Status code', res.status).toBe(400);
|
|
202
319
|
})),
|
|
203
|
-
|
|
320
|
+
...['string', 0, false, ['array']].map((invalid) => audit(`SHOULD use 200 status code with errors field on ${extendedTypeof(invalid)} {variables} parameter when accepting application/json`, async () => {
|
|
321
|
+
const res = await fetchFn(opts.url, {
|
|
322
|
+
method: 'POST',
|
|
323
|
+
headers: {
|
|
324
|
+
'content-type': 'application/json',
|
|
325
|
+
accept: 'application/json',
|
|
326
|
+
},
|
|
327
|
+
body: JSON.stringify({
|
|
328
|
+
query: '{ __typename }',
|
|
329
|
+
variables: invalid,
|
|
330
|
+
}),
|
|
331
|
+
});
|
|
332
|
+
assert('Status code', res.status).toBe(200);
|
|
333
|
+
assert('Execution result', await assertBodyAsExecutionResult(res)).toHaveProperty('errors');
|
|
334
|
+
})),
|
|
335
|
+
audit(
|
|
336
|
+
// TODO: convert to MUST after watershed
|
|
337
|
+
'SHOULD allow map {variables} parameter when accepting application/graphql-response+json', async () => {
|
|
204
338
|
const res = await fetchFn(opts.url, {
|
|
205
339
|
method: 'POST',
|
|
206
340
|
headers: {
|
|
@@ -213,21 +347,52 @@ export function serverAudits(opts) {
|
|
|
213
347
|
}),
|
|
214
348
|
});
|
|
215
349
|
assert('Status code', res.status).toBe(200);
|
|
216
|
-
const result = (await res.json());
|
|
217
|
-
assert('Execution result', result).notToHaveProperty('errors');
|
|
218
350
|
}),
|
|
219
|
-
audit('MUST
|
|
351
|
+
audit('MUST allow map {variables} parameter when accepting application/json', async () => {
|
|
352
|
+
const res = await fetchFn(opts.url, {
|
|
353
|
+
method: 'POST',
|
|
354
|
+
headers: {
|
|
355
|
+
'content-type': 'application/json',
|
|
356
|
+
accept: 'application/json',
|
|
357
|
+
},
|
|
358
|
+
body: JSON.stringify({
|
|
359
|
+
query: 'query Type($name: String!) { __type(name: $name) { name } }',
|
|
360
|
+
variables: { name: 'sometype' },
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
assert('Status code', res.status).toBe(200);
|
|
364
|
+
assert('Execution result', await assertBodyAsExecutionResult(res)).notToHaveProperty('errors');
|
|
365
|
+
}),
|
|
366
|
+
audit(
|
|
367
|
+
// TODO: convert to MUST after watershed
|
|
368
|
+
'SHOULD allow URL-encoded JSON string {variables} parameter in GETs when accepting application/graphql-response+json', async () => {
|
|
369
|
+
const url = new URL(opts.url);
|
|
370
|
+
url.searchParams.set('query', 'query Type($name: String!) { __type(name: $name) { name } }');
|
|
371
|
+
url.searchParams.set('variables', JSON.stringify({ name: 'sometype' }));
|
|
372
|
+
const res = await fetchFn(url.toString(), {
|
|
373
|
+
method: 'GET',
|
|
374
|
+
headers: {
|
|
375
|
+
accept: 'application/graphql-response+json',
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
assert('Status code', res.status).toBe(200);
|
|
379
|
+
}),
|
|
380
|
+
audit('MUST allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json', async () => {
|
|
220
381
|
const url = new URL(opts.url);
|
|
221
382
|
url.searchParams.set('query', 'query Type($name: String!) { __type(name: $name) { name } }');
|
|
222
383
|
url.searchParams.set('variables', JSON.stringify({ name: 'sometype' }));
|
|
223
384
|
const res = await fetchFn(url.toString(), {
|
|
224
385
|
method: 'GET',
|
|
386
|
+
headers: {
|
|
387
|
+
accept: 'application/json',
|
|
388
|
+
},
|
|
225
389
|
});
|
|
226
390
|
assert('Status code', res.status).toBe(200);
|
|
227
|
-
|
|
228
|
-
assert('Execution result', result).notToHaveProperty('errors');
|
|
391
|
+
assert('Execution result', await assertBodyAsExecutionResult(res)).notToHaveProperty('errors');
|
|
229
392
|
}),
|
|
230
|
-
...['string', 0, false, ['array']].map((invalid) => audit(
|
|
393
|
+
...['string', 0, false, ['array']].map((invalid) => audit(
|
|
394
|
+
// TODO: convert to MUST after watershed
|
|
395
|
+
`SHOULD use 400 status code on ${extendedTypeof(invalid)} {extensions} parameter when accepting application/graphql-response+json`, async () => {
|
|
231
396
|
const res = await fetchFn(opts.url, {
|
|
232
397
|
method: 'POST',
|
|
233
398
|
headers: {
|
|
@@ -241,7 +406,24 @@ export function serverAudits(opts) {
|
|
|
241
406
|
});
|
|
242
407
|
assert('Status code', res.status).toBe(400);
|
|
243
408
|
})),
|
|
244
|
-
|
|
409
|
+
...['string', 0, false, ['array']].map((invalid) => audit(`SHOULD use 200 status code with errors field on ${extendedTypeof(invalid)} {extensions} parameter when accepting application/json`, async () => {
|
|
410
|
+
const res = await fetchFn(opts.url, {
|
|
411
|
+
method: 'POST',
|
|
412
|
+
headers: {
|
|
413
|
+
'content-type': 'application/json',
|
|
414
|
+
accept: 'application/json',
|
|
415
|
+
},
|
|
416
|
+
body: JSON.stringify({
|
|
417
|
+
query: '{ __typename }',
|
|
418
|
+
extensions: invalid,
|
|
419
|
+
}),
|
|
420
|
+
});
|
|
421
|
+
assert('Status code', res.status).toBe(200);
|
|
422
|
+
assert('Execution result', await assertBodyAsExecutionResult(res)).toHaveProperty('errors');
|
|
423
|
+
})),
|
|
424
|
+
audit(
|
|
425
|
+
// TODO: convert to MUST after watershed
|
|
426
|
+
'SHOULD allow map {extensions} parameter when accepting application/graphql-response+json', async () => {
|
|
245
427
|
const res = await fetchFn(opts.url, {
|
|
246
428
|
method: 'POST',
|
|
247
429
|
headers: {
|
|
@@ -255,6 +437,21 @@ export function serverAudits(opts) {
|
|
|
255
437
|
});
|
|
256
438
|
assert('Status code', res.status).toBe(200);
|
|
257
439
|
}),
|
|
440
|
+
audit('MUST allow map {extensions} parameter when accepting application/json', async () => {
|
|
441
|
+
const res = await fetchFn(opts.url, {
|
|
442
|
+
method: 'POST',
|
|
443
|
+
headers: {
|
|
444
|
+
'content-type': 'application/json',
|
|
445
|
+
accept: 'application/json',
|
|
446
|
+
},
|
|
447
|
+
body: JSON.stringify({
|
|
448
|
+
query: '{ __typename }',
|
|
449
|
+
extensions: { some: 'value' },
|
|
450
|
+
}),
|
|
451
|
+
});
|
|
452
|
+
assert('Status code', res.status).toBe(200);
|
|
453
|
+
assert('Execution result', await assertBodyAsExecutionResult(res)).notToHaveProperty('errors');
|
|
454
|
+
}),
|
|
258
455
|
// TODO: audit('MUST accept a map for the {extensions} parameter'),
|
|
259
456
|
// Response application/json
|
|
260
457
|
audit('SHOULD use 200 status code on JSON parsing failure when accepting application/json', async () => {
|
|
@@ -296,7 +493,9 @@ export function serverAudits(opts) {
|
|
|
296
493
|
assert('Status code', res.status).toBe(200);
|
|
297
494
|
}),
|
|
298
495
|
// Response application/graphql-response+json
|
|
299
|
-
audit(
|
|
496
|
+
audit(
|
|
497
|
+
// TODO: convert to MUST after watershed
|
|
498
|
+
'SHOULD use 4xx or 5xx status codes on JSON parsing failure when accepting application/graphql-response+json', async () => {
|
|
300
499
|
const res = await fetchFn(opts.url, {
|
|
301
500
|
method: 'POST',
|
|
302
501
|
headers: {
|
|
@@ -328,9 +527,11 @@ export function serverAudits(opts) {
|
|
|
328
527
|
},
|
|
329
528
|
body: '{ "not a JSON',
|
|
330
529
|
});
|
|
331
|
-
assert('Data entry', (await res
|
|
530
|
+
assert('Data entry', (await assertBodyAsExecutionResult(res)).data).toBe(undefined);
|
|
332
531
|
}),
|
|
333
|
-
audit(
|
|
532
|
+
audit(
|
|
533
|
+
// TODO: convert to MUST after watershed
|
|
534
|
+
'SHOULD use 4xx or 5xx status codes if parameters are invalid when accepting application/graphql-response+json', async () => {
|
|
334
535
|
const url = new URL(opts.url);
|
|
335
536
|
url.searchParams.set('qeury' /* typo */, '{ __typename }');
|
|
336
537
|
const res = await fetchFn(url.toString(), {
|
|
@@ -356,9 +557,11 @@ export function serverAudits(opts) {
|
|
|
356
557
|
method: 'GET',
|
|
357
558
|
headers: { accept: 'application/graphql-response+json' },
|
|
358
559
|
});
|
|
359
|
-
assert('Data entry', (await res
|
|
560
|
+
assert('Data entry', (await assertBodyAsExecutionResult(res)).data).toBe(undefined);
|
|
360
561
|
}),
|
|
361
|
-
audit(
|
|
562
|
+
audit(
|
|
563
|
+
// TODO: convert to MUST after watershed
|
|
564
|
+
'SHOULD use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json', async () => {
|
|
362
565
|
const url = new URL(opts.url);
|
|
363
566
|
url.searchParams.set('query', '{');
|
|
364
567
|
const res = await fetchFn(url.toString(), {
|
|
@@ -384,9 +587,11 @@ export function serverAudits(opts) {
|
|
|
384
587
|
method: 'GET',
|
|
385
588
|
headers: { accept: 'application/graphql-response+json' },
|
|
386
589
|
});
|
|
387
|
-
assert('Data entry', (await res
|
|
590
|
+
assert('Data entry', (await assertBodyAsExecutionResult(res)).data).toBe(undefined);
|
|
388
591
|
}),
|
|
389
|
-
audit(
|
|
592
|
+
audit(
|
|
593
|
+
// TODO: convert to MUST after watershed
|
|
594
|
+
'SHOULD use 4xx or 5xx status codes on document validation failure when accepting application/graphql-response+json', async () => {
|
|
390
595
|
const url = new URL(opts.url);
|
|
391
596
|
url.searchParams.set('query', '{ 8f31403dfe404bccbb0e835f2629c6a7 }'); // making sure the field doesnt exist
|
|
392
597
|
const res = await fetchFn(url.toString(), {
|
|
@@ -412,7 +617,7 @@ export function serverAudits(opts) {
|
|
|
412
617
|
method: 'GET',
|
|
413
618
|
headers: { accept: 'application/graphql-response+json' },
|
|
414
619
|
});
|
|
415
|
-
assert('Data entry', (await res
|
|
620
|
+
assert('Data entry', (await assertBodyAsExecutionResult(res)).data).toBe(undefined);
|
|
416
621
|
}),
|
|
417
622
|
// TODO: how to fail and have the data entry?
|
|
418
623
|
// audit('MUST use 2xx status code if response contains the data entry and it is not null when accepting application/graphql-response+json'),
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* audit/utils
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
import type { ExecutionResult } from 'graphql';
|
|
7
|
+
import { Audit, AuditName } from './common';
|
|
8
|
+
export * from '../utils';
|
|
9
|
+
/**
|
|
10
|
+
* Wrap and prepare an audit for testing.
|
|
11
|
+
*
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
export declare function audit(name: AuditName, fn: () => Promise<void>): Audit;
|
|
15
|
+
/**
|
|
16
|
+
* Will throw a string if the assertion fails.
|
|
17
|
+
*
|
|
18
|
+
* All fatal problems will throw an instance of Error.
|
|
19
|
+
*
|
|
20
|
+
* @private
|
|
21
|
+
*/
|
|
22
|
+
export declare function assert<T = unknown>(name: string, actual: T): {
|
|
23
|
+
toBe: (expected: T) => void;
|
|
24
|
+
toBeLessThanOrEqual: (expected: T extends number ? T : never) => void;
|
|
25
|
+
toBeGreaterThanOrEqual: (expected: T extends number ? T : never) => void;
|
|
26
|
+
toContain: (expected: T extends (infer U)[] ? U : T extends string ? T : never) => void;
|
|
27
|
+
notToContain: (expected: T extends (infer U_1)[] ? U_1 : T extends string ? T : never) => void;
|
|
28
|
+
toHaveProperty: (prop: T extends Record<any, any> ? PropertyKey : never) => void;
|
|
29
|
+
notToHaveProperty: (prop: T extends Record<any, any> ? PropertyKey : never) => void;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Parses the string as JSON and safely reports parsing issues for audits.
|
|
33
|
+
*
|
|
34
|
+
* Assumes the parsed JSON will be an `ExecutionResult`.
|
|
35
|
+
*
|
|
36
|
+
* @private
|
|
37
|
+
* */
|
|
38
|
+
export declare function assertBodyAsExecutionResult(res: Response): Promise<ExecutionResult<import("graphql/jsutils/ObjMap").ObjMap<unknown>, import("graphql/jsutils/ObjMap").ObjMap<unknown>>>;
|
package/lib/audits/utils.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* audit/utils
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
|
+
import type { ExecutionResult } from 'graphql';
|
|
6
7
|
import { Audit, AuditName } from './common';
|
|
7
8
|
export * from '../utils';
|
|
8
9
|
/**
|
|
@@ -23,5 +24,15 @@ export declare function assert<T = unknown>(name: string, actual: T): {
|
|
|
23
24
|
toBeLessThanOrEqual: (expected: T extends number ? T : never) => void;
|
|
24
25
|
toBeGreaterThanOrEqual: (expected: T extends number ? T : never) => void;
|
|
25
26
|
toContain: (expected: T extends (infer U)[] ? U : T extends string ? T : never) => void;
|
|
27
|
+
notToContain: (expected: T extends (infer U_1)[] ? U_1 : T extends string ? T : never) => void;
|
|
28
|
+
toHaveProperty: (prop: T extends Record<any, any> ? PropertyKey : never) => void;
|
|
26
29
|
notToHaveProperty: (prop: T extends Record<any, any> ? PropertyKey : never) => void;
|
|
27
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Parses the string as JSON and safely reports parsing issues for audits.
|
|
33
|
+
*
|
|
34
|
+
* Assumes the parsed JSON will be an `ExecutionResult`.
|
|
35
|
+
*
|
|
36
|
+
* @private
|
|
37
|
+
* */
|
|
38
|
+
export declare function assertBodyAsExecutionResult(res: Response): Promise<ExecutionResult<import("graphql/jsutils/ObjMap").ObjMap<unknown>, import("graphql/jsutils/ObjMap").ObjMap<unknown>>>;
|
package/lib/audits/utils.js
CHANGED
|
@@ -19,7 +19,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
19
19
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
20
20
|
};
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.assert = exports.audit = void 0;
|
|
22
|
+
exports.assertBodyAsExecutionResult = exports.assert = exports.audit = void 0;
|
|
23
23
|
__exportStar(require("../utils"), exports);
|
|
24
24
|
/**
|
|
25
25
|
* Wrap and prepare an audit for testing.
|
|
@@ -87,6 +87,21 @@ function assert(name, actual) {
|
|
|
87
87
|
throw `${name} ${JSON.stringify(actual)} does not contain ${JSON.stringify(expected)}`;
|
|
88
88
|
}
|
|
89
89
|
},
|
|
90
|
+
notToContain: (expected) => {
|
|
91
|
+
// @ts-expect-error types will match, otherwise never
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
if (actual.includes(expected)) {
|
|
94
|
+
throw `${name} ${JSON.stringify(actual)} contains ${JSON.stringify(expected)}`;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
toHaveProperty: (
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
prop) => {
|
|
100
|
+
// @ts-expect-error types will match, otherwise never
|
|
101
|
+
if (!(prop in actual)) {
|
|
102
|
+
throw `${name} ${JSON.stringify(actual)} does not have a property '${String(prop)}'`;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
90
105
|
notToHaveProperty: (
|
|
91
106
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
107
|
prop) => {
|
|
@@ -98,3 +113,20 @@ function assert(name, actual) {
|
|
|
98
113
|
};
|
|
99
114
|
}
|
|
100
115
|
exports.assert = assert;
|
|
116
|
+
/**
|
|
117
|
+
* Parses the string as JSON and safely reports parsing issues for audits.
|
|
118
|
+
*
|
|
119
|
+
* Assumes the parsed JSON will be an `ExecutionResult`.
|
|
120
|
+
*
|
|
121
|
+
* @private
|
|
122
|
+
* */
|
|
123
|
+
async function assertBodyAsExecutionResult(res) {
|
|
124
|
+
const str = await res.text();
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(str);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
throw `Response body is not valid JSON. Got ${JSON.stringify(str)}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
exports.assertBodyAsExecutionResult = assertBodyAsExecutionResult;
|
package/lib/audits/utils.mjs
CHANGED
|
@@ -69,6 +69,21 @@ export function assert(name, actual) {
|
|
|
69
69
|
throw `${name} ${JSON.stringify(actual)} does not contain ${JSON.stringify(expected)}`;
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
|
+
notToContain: (expected) => {
|
|
73
|
+
// @ts-expect-error types will match, otherwise never
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
if (actual.includes(expected)) {
|
|
76
|
+
throw `${name} ${JSON.stringify(actual)} contains ${JSON.stringify(expected)}`;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
toHaveProperty: (
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
prop) => {
|
|
82
|
+
// @ts-expect-error types will match, otherwise never
|
|
83
|
+
if (!(prop in actual)) {
|
|
84
|
+
throw `${name} ${JSON.stringify(actual)} does not have a property '${String(prop)}'`;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
72
87
|
notToHaveProperty: (
|
|
73
88
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
89
|
prop) => {
|
|
@@ -79,3 +94,19 @@ export function assert(name, actual) {
|
|
|
79
94
|
},
|
|
80
95
|
};
|
|
81
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Parses the string as JSON and safely reports parsing issues for audits.
|
|
99
|
+
*
|
|
100
|
+
* Assumes the parsed JSON will be an `ExecutionResult`.
|
|
101
|
+
*
|
|
102
|
+
* @private
|
|
103
|
+
* */
|
|
104
|
+
export async function assertBodyAsExecutionResult(res) {
|
|
105
|
+
const str = await res.text();
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(str);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
throw `Response body is not valid JSON. Got ${JSON.stringify(str)}`;
|
|
111
|
+
}
|
|
112
|
+
}
|