graphql-http 1.4.0 → 1.6.1

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.
@@ -0,0 +1,637 @@
1
+ /**
2
+ *
3
+ * audit/server
4
+ *
5
+ */
6
+ import { assert, assertBodyAsExecutionResult, audit, extendedTypeof, } from './utils.mjs';
7
+ /**
8
+ * List of server audits required to check GraphQL over HTTP spec conformance.
9
+ *
10
+ * @category Audits
11
+ */
12
+ export function serverAudits(opts) {
13
+ const fetchFn = (opts.fetchFn || fetch);
14
+ return [
15
+ // Media Types
16
+ audit(
17
+ // TODO: convert to MUST after watershed
18
+ 'SHOULD accept application/graphql-response+json and match the content-type', async () => {
19
+ const url = new URL(opts.url);
20
+ url.searchParams.set('query', '{ __typename }');
21
+ const res = await fetchFn(url.toString(), {
22
+ headers: {
23
+ accept: 'application/graphql-response+json',
24
+ },
25
+ });
26
+ assert('Status code', res.status).toBe(200);
27
+ assert('Content-Type header', res.headers.get('content-type')).toContain('application/graphql-response+json');
28
+ }),
29
+ audit('MUST accept application/json and match the content-type', async () => {
30
+ const url = new URL(opts.url);
31
+ url.searchParams.set('query', '{ __typename }');
32
+ const res = await fetchFn(url.toString(), {
33
+ headers: {
34
+ accept: 'application/json',
35
+ },
36
+ });
37
+ assert('Status code', res.status).toBe(200);
38
+ assert('Content-Type header', res.headers.get('content-type')).toContain('application/json');
39
+ }),
40
+ audit(
41
+ // TODO: convert to MUST after watershed
42
+ 'SHOULD accept */* and use application/graphql-response+json for the content-type', async () => {
43
+ const url = new URL(opts.url);
44
+ url.searchParams.set('query', '{ __typename }');
45
+ const res = await fetchFn(url.toString(), {
46
+ headers: {
47
+ accept: '*/*',
48
+ },
49
+ });
50
+ assert('Status code', res.status).toBe(200);
51
+ assert('Content-Type header', res.headers.get('content-type')).toContain('application/graphql-response+json');
52
+ }),
53
+ audit(
54
+ // TODO: convert to MUST after watershed
55
+ 'SHOULD assume application/graphql-response+json content-type when accept is missing', async () => {
56
+ const url = new URL(opts.url);
57
+ url.searchParams.set('query', '{ __typename }');
58
+ const res = await fetchFn(url.toString());
59
+ assert('Status code', res.status).toBe(200);
60
+ assert('Content-Type header', res.headers.get('content-type')).toContain('application/graphql-response+json');
61
+ }),
62
+ audit('MUST use utf-8 encoding when responding', async () => {
63
+ const url = new URL(opts.url);
64
+ url.searchParams.set('query', '{ __typename }');
65
+ const res = await fetchFn(url.toString());
66
+ assert('Status code', res.status).toBe(200);
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 only utf-8 charset', async () => {
88
+ const url = new URL(opts.url);
89
+ url.searchParams.set('query', '{ __typename }');
90
+ const res = await fetchFn(url.toString(), {
91
+ headers: {
92
+ accept: 'application/graphql-response+json; charset=iso-8859-1',
93
+ },
94
+ });
95
+ // application/json is 200 + errors
96
+ const contentType = res.headers.get('content-type') || '';
97
+ if (contentType.includes('application/json')) {
98
+ assert(`Content-Type ${contentType} status code`, res.status).toBe(200);
99
+ assert('Execution result', await assertBodyAsExecutionResult(res)).toHaveProperty('errors');
100
+ return;
101
+ }
102
+ // other content-types must be 4xx
103
+ assert(`Content-Type ${contentType} status code`, res.status).toBeGreaterThanOrEqual(400);
104
+ assert(`Content-Type ${contentType} status code`, res.status).toBeLessThanOrEqual(499);
105
+ }),
106
+ // Request
107
+ audit('MUST accept POST requests', async () => {
108
+ const res = await fetchFn(opts.url, {
109
+ method: 'POST',
110
+ headers: { 'content-type': 'application/json' },
111
+ body: JSON.stringify({ query: '{ __typename }' }),
112
+ });
113
+ assert('Status code', res.status).toBe(200);
114
+ }),
115
+ audit('MAY accept application/x-www-form-urlencoded formatted GET requests', async () => {
116
+ const url = new URL(opts.url);
117
+ url.searchParams.set('query', '{ __typename }');
118
+ const res = await fetchFn(url.toString());
119
+ assert('Status code', res.status).toBe(200);
120
+ }),
121
+ // Request GET
122
+ audit('MUST NOT allow executing mutations on GET requests', async () => {
123
+ const url = new URL(opts.url);
124
+ url.searchParams.set('query', 'mutation { __typename }');
125
+ const res = await fetchFn(url.toString(), {
126
+ headers: {
127
+ accept: 'application/graphql-response+json',
128
+ },
129
+ });
130
+ assert('Status code', res.status).toBe(405);
131
+ }),
132
+ // Request POST
133
+ audit('SHOULD respond with 4xx status code if content-type is not supplied on POST requests', async () => {
134
+ const res = await fetchFn(opts.url, {
135
+ method: 'POST',
136
+ });
137
+ assert('Status code', res.status).toBeGreaterThanOrEqual(400);
138
+ assert('Status code', res.status).toBeLessThanOrEqual(499);
139
+ }),
140
+ audit('MUST accept application/json POST requests', async () => {
141
+ const res = await fetchFn(opts.url, {
142
+ method: 'POST',
143
+ headers: { 'content-type': 'application/json' },
144
+ body: JSON.stringify({ query: '{ __typename }' }),
145
+ });
146
+ assert('Status code', res.status).toBe(200);
147
+ }),
148
+ audit('MUST require a request body on POST', async () => {
149
+ const res = await fetchFn(opts.url, {
150
+ method: 'POST',
151
+ headers: { 'content-type': 'application/json' },
152
+ });
153
+ assert('Status code', res.status).toBe(400);
154
+ }),
155
+ // Request Parameters
156
+ audit(
157
+ // TODO: convert to MUST after watershed
158
+ 'SHOULD use 400 status code on missing {query} parameter when accepting application/graphql-response+json', async () => {
159
+ const res = await fetchFn(opts.url, {
160
+ method: 'POST',
161
+ headers: {
162
+ 'content-type': 'application/json',
163
+ accept: 'application/graphql-response+json',
164
+ },
165
+ body: JSON.stringify({ notquery: '{ __typename }' }),
166
+ });
167
+ assert('Status code', res.status).toBe(400);
168
+ }),
169
+ audit('SHOULD use 200 status code with errors field on missing {query} parameter when accepting application/json', async () => {
170
+ const res = await fetchFn(opts.url, {
171
+ method: 'POST',
172
+ headers: {
173
+ 'content-type': 'application/json',
174
+ accept: 'application/json',
175
+ },
176
+ body: JSON.stringify({ notquery: '{ __typename }' }),
177
+ });
178
+ assert('Status code', res.status).toBe(200);
179
+ assert('Execution result', await assertBodyAsExecutionResult(res)).toHaveProperty('errors');
180
+ }),
181
+ ...[{ obj: 'ect' }, 0, false, ['array']].map((invalid) => audit(
182
+ // TODO: convert to MUST after watershed
183
+ `SHOULD use 400 status code on ${extendedTypeof(invalid)} {query} parameter when accepting application/graphql-response+json`, async () => {
184
+ const res = await fetchFn(opts.url, {
185
+ method: 'POST',
186
+ headers: {
187
+ 'content-type': 'application/json',
188
+ accept: 'application/graphql-response+json',
189
+ },
190
+ body: JSON.stringify({
191
+ query: invalid,
192
+ }),
193
+ });
194
+ assert('Status code', res.status).toBe(400);
195
+ })),
196
+ ...[{ 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 () => {
197
+ const res = await fetchFn(opts.url, {
198
+ method: 'POST',
199
+ headers: {
200
+ 'content-type': 'application/json',
201
+ accept: 'application/json',
202
+ },
203
+ body: JSON.stringify({
204
+ query: invalid,
205
+ }),
206
+ });
207
+ assert('Status code', res.status).toBe(200);
208
+ assert('Execution result', await assertBodyAsExecutionResult(res)).toHaveProperty('errors');
209
+ })),
210
+ audit(
211
+ // TODO: convert to MUST after watershed
212
+ 'SHOULD allow string {query} parameter when accepting application/graphql-response+json', async () => {
213
+ const res = await fetchFn(opts.url, {
214
+ method: 'POST',
215
+ headers: {
216
+ 'content-type': 'application/json',
217
+ accept: 'application/graphql-response+json',
218
+ },
219
+ body: JSON.stringify({
220
+ query: '{ __typename }',
221
+ }),
222
+ });
223
+ assert('Status code', res.status).toBe(200);
224
+ }),
225
+ audit('MUST allow string {query} parameter when accepting application/json', async () => {
226
+ const res = await fetchFn(opts.url, {
227
+ method: 'POST',
228
+ headers: {
229
+ 'content-type': 'application/json',
230
+ accept: 'application/json',
231
+ },
232
+ body: JSON.stringify({
233
+ query: '{ __typename }',
234
+ }),
235
+ });
236
+ assert('Status code', res.status).toBe(200);
237
+ assert('Execution result', await assertBodyAsExecutionResult(res)).notToHaveProperty('errors');
238
+ }),
239
+ ...[{ obj: 'ect' }, 0, false, ['array']].map((invalid) => audit(
240
+ // TODO: convert to MUST after watershed
241
+ `SHOULD use 400 status code on ${extendedTypeof(invalid)} {operationName} parameter when accepting application/graphql-response+json`, async () => {
242
+ const res = await fetchFn(opts.url, {
243
+ method: 'POST',
244
+ headers: {
245
+ 'content-type': 'application/json',
246
+ accept: 'application/graphql-response+json',
247
+ },
248
+ body: JSON.stringify({
249
+ operationName: invalid,
250
+ query: '{ __typename }',
251
+ }),
252
+ });
253
+ assert('Status code', res.status).toBe(400);
254
+ })),
255
+ ...[{ 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 () => {
256
+ const res = await fetchFn(opts.url, {
257
+ method: 'POST',
258
+ headers: {
259
+ 'content-type': 'application/json',
260
+ accept: 'application/json',
261
+ },
262
+ body: JSON.stringify({
263
+ operationName: invalid,
264
+ query: '{ __typename }',
265
+ }),
266
+ });
267
+ assert('Status code', res.status).toBe(200);
268
+ assert('Execution result', await assertBodyAsExecutionResult(res)).toHaveProperty('errors');
269
+ })),
270
+ audit(
271
+ // TODO: convert to MUST after watershed
272
+ 'SHOULD allow string {operationName} parameter when accepting application/graphql-response+json', async () => {
273
+ const res = await fetchFn(opts.url, {
274
+ method: 'POST',
275
+ headers: {
276
+ 'content-type': 'application/json',
277
+ accept: 'application/graphql-response+json',
278
+ },
279
+ body: JSON.stringify({
280
+ operationName: 'Query',
281
+ query: 'query Query { __typename }',
282
+ }),
283
+ });
284
+ assert('Status code', res.status).toBe(200);
285
+ }),
286
+ audit('MUST allow string {operationName} parameter when accepting application/json', async () => {
287
+ const res = await fetchFn(opts.url, {
288
+ method: 'POST',
289
+ headers: {
290
+ 'content-type': 'application/json',
291
+ accept: 'application/json',
292
+ },
293
+ body: JSON.stringify({
294
+ operationName: 'Query',
295
+ query: 'query Query { __typename }',
296
+ }),
297
+ });
298
+ assert('Status code', res.status).toBe(200);
299
+ assert('Execution result', await assertBodyAsExecutionResult(res)).notToHaveProperty('errors');
300
+ }),
301
+ ...['string', 0, false, ['array']].map((invalid) => audit(
302
+ // TODO: convert to MUST after watershed
303
+ `SHOULD use 400 status code on ${extendedTypeof(invalid)} {variables} parameter when accepting application/graphql-response+json`, async () => {
304
+ const res = await fetchFn(opts.url, {
305
+ method: 'POST',
306
+ headers: {
307
+ 'content-type': 'application/json',
308
+ accept: 'application/graphql-response+json',
309
+ },
310
+ body: JSON.stringify({
311
+ query: '{ __typename }',
312
+ variables: invalid,
313
+ }),
314
+ });
315
+ assert('Status code', res.status).toBe(400);
316
+ })),
317
+ ...['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 () => {
318
+ const res = await fetchFn(opts.url, {
319
+ method: 'POST',
320
+ headers: {
321
+ 'content-type': 'application/json',
322
+ accept: 'application/json',
323
+ },
324
+ body: JSON.stringify({
325
+ query: '{ __typename }',
326
+ variables: invalid,
327
+ }),
328
+ });
329
+ assert('Status code', res.status).toBe(200);
330
+ assert('Execution result', await assertBodyAsExecutionResult(res)).toHaveProperty('errors');
331
+ })),
332
+ audit(
333
+ // TODO: convert to MUST after watershed
334
+ 'SHOULD allow map {variables} parameter when accepting application/graphql-response+json', async () => {
335
+ const res = await fetchFn(opts.url, {
336
+ method: 'POST',
337
+ headers: {
338
+ 'content-type': 'application/json',
339
+ accept: 'application/graphql-response+json',
340
+ },
341
+ body: JSON.stringify({
342
+ query: 'query Type($name: String!) { __type(name: $name) { name } }',
343
+ variables: { name: 'sometype' },
344
+ }),
345
+ });
346
+ assert('Status code', res.status).toBe(200);
347
+ }),
348
+ audit('MUST allow map {variables} parameter when accepting application/json', async () => {
349
+ const res = await fetchFn(opts.url, {
350
+ method: 'POST',
351
+ headers: {
352
+ 'content-type': 'application/json',
353
+ accept: 'application/json',
354
+ },
355
+ body: JSON.stringify({
356
+ query: 'query Type($name: String!) { __type(name: $name) { name } }',
357
+ variables: { name: 'sometype' },
358
+ }),
359
+ });
360
+ assert('Status code', res.status).toBe(200);
361
+ assert('Execution result', await assertBodyAsExecutionResult(res)).notToHaveProperty('errors');
362
+ }),
363
+ audit(
364
+ // TODO: convert to MUST after watershed
365
+ 'SHOULD allow URL-encoded JSON string {variables} parameter in GETs when accepting application/graphql-response+json', async () => {
366
+ const url = new URL(opts.url);
367
+ url.searchParams.set('query', 'query Type($name: String!) { __type(name: $name) { name } }');
368
+ url.searchParams.set('variables', JSON.stringify({ name: 'sometype' }));
369
+ const res = await fetchFn(url.toString(), {
370
+ method: 'GET',
371
+ headers: {
372
+ accept: 'application/graphql-response+json',
373
+ },
374
+ });
375
+ assert('Status code', res.status).toBe(200);
376
+ }),
377
+ audit('MUST allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json', async () => {
378
+ const url = new URL(opts.url);
379
+ url.searchParams.set('query', 'query Type($name: String!) { __type(name: $name) { name } }');
380
+ url.searchParams.set('variables', JSON.stringify({ name: 'sometype' }));
381
+ const res = await fetchFn(url.toString(), {
382
+ method: 'GET',
383
+ headers: {
384
+ accept: 'application/json',
385
+ },
386
+ });
387
+ assert('Status code', res.status).toBe(200);
388
+ assert('Execution result', await assertBodyAsExecutionResult(res)).notToHaveProperty('errors');
389
+ }),
390
+ ...['string', 0, false, ['array']].map((invalid) => audit(
391
+ // TODO: convert to MUST after watershed
392
+ `SHOULD use 400 status code on ${extendedTypeof(invalid)} {extensions} parameter when accepting application/graphql-response+json`, async () => {
393
+ const res = await fetchFn(opts.url, {
394
+ method: 'POST',
395
+ headers: {
396
+ 'content-type': 'application/json',
397
+ accept: 'application/graphql-response+json',
398
+ },
399
+ body: JSON.stringify({
400
+ query: '{ __typename }',
401
+ extensions: invalid,
402
+ }),
403
+ });
404
+ assert('Status code', res.status).toBe(400);
405
+ })),
406
+ ...['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 () => {
407
+ const res = await fetchFn(opts.url, {
408
+ method: 'POST',
409
+ headers: {
410
+ 'content-type': 'application/json',
411
+ accept: 'application/json',
412
+ },
413
+ body: JSON.stringify({
414
+ query: '{ __typename }',
415
+ extensions: invalid,
416
+ }),
417
+ });
418
+ assert('Status code', res.status).toBe(200);
419
+ assert('Execution result', await assertBodyAsExecutionResult(res)).toHaveProperty('errors');
420
+ })),
421
+ audit(
422
+ // TODO: convert to MUST after watershed
423
+ 'SHOULD allow map {extensions} parameter when accepting application/graphql-response+json', async () => {
424
+ const res = await fetchFn(opts.url, {
425
+ method: 'POST',
426
+ headers: {
427
+ 'content-type': 'application/json',
428
+ accept: 'application/graphql-response+json',
429
+ },
430
+ body: JSON.stringify({
431
+ query: '{ __typename }',
432
+ extensions: { some: 'value' },
433
+ }),
434
+ });
435
+ assert('Status code', res.status).toBe(200);
436
+ }),
437
+ audit('MUST allow map {extensions} parameter when accepting application/json', async () => {
438
+ const res = await fetchFn(opts.url, {
439
+ method: 'POST',
440
+ headers: {
441
+ 'content-type': 'application/json',
442
+ accept: 'application/json',
443
+ },
444
+ body: JSON.stringify({
445
+ query: '{ __typename }',
446
+ extensions: { some: 'value' },
447
+ }),
448
+ });
449
+ assert('Status code', res.status).toBe(200);
450
+ assert('Execution result', await assertBodyAsExecutionResult(res)).notToHaveProperty('errors');
451
+ }),
452
+ // TODO: audit('MUST accept a map for the {extensions} parameter'),
453
+ // Response application/json
454
+ audit('SHOULD use 200 status code on JSON parsing failure when accepting application/json', async () => {
455
+ const res = await fetchFn(opts.url, {
456
+ method: 'POST',
457
+ headers: {
458
+ 'content-type': 'application/json',
459
+ accept: 'application/json',
460
+ },
461
+ body: '{ "not a JSON',
462
+ });
463
+ assert('Status code', res.status).toBe(200);
464
+ }),
465
+ audit('SHOULD use 200 status code if parameters are invalid when accepting application/json', async () => {
466
+ const url = new URL(opts.url);
467
+ url.searchParams.set('qeury' /* typo */, '{ __typename }');
468
+ const res = await fetchFn(url.toString(), {
469
+ method: 'GET',
470
+ headers: { accept: 'application/json' },
471
+ });
472
+ assert('Status code', res.status).toBe(200);
473
+ }),
474
+ audit('SHOULD use 200 status code on document parsing failure when accepting application/json', async () => {
475
+ const url = new URL(opts.url);
476
+ url.searchParams.set('query', '{');
477
+ const res = await fetchFn(url.toString(), {
478
+ method: 'GET',
479
+ headers: { accept: 'application/json' },
480
+ });
481
+ assert('Status code', res.status).toBe(200);
482
+ }),
483
+ audit('SHOULD use 200 status code on document validation failure when accepting application/json', async () => {
484
+ const url = new URL(opts.url);
485
+ url.searchParams.set('query', '{ 8f31403dfe404bccbb0e835f2629c6a7 }'); // making sure the field doesnt exist
486
+ const res = await fetchFn(url.toString(), {
487
+ method: 'GET',
488
+ headers: { accept: 'application/json' },
489
+ });
490
+ assert('Status code', res.status).toBe(200);
491
+ }),
492
+ // Response application/graphql-response+json
493
+ audit(
494
+ // TODO: convert to MUST after watershed
495
+ 'SHOULD use 4xx or 5xx status codes on JSON parsing failure when accepting application/graphql-response+json', async () => {
496
+ const res = await fetchFn(opts.url, {
497
+ method: 'POST',
498
+ headers: {
499
+ 'content-type': 'application/json',
500
+ accept: 'application/graphql-response+json',
501
+ },
502
+ body: '{ "not a JSON',
503
+ });
504
+ assert('Status code', res.status).toBeGreaterThanOrEqual(400);
505
+ assert('Status code', res.status).toBeLessThanOrEqual(599);
506
+ }),
507
+ audit('SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json', async () => {
508
+ const res = await fetchFn(opts.url, {
509
+ method: 'POST',
510
+ headers: {
511
+ 'content-type': 'application/json',
512
+ accept: 'application/graphql-response+json',
513
+ },
514
+ body: '{ "not a JSON',
515
+ });
516
+ assert('Status code', res.status).toBe(400);
517
+ }),
518
+ audit('SHOULD not contain the data entry on JSON parsing failure when accepting application/graphql-response+json', async () => {
519
+ const res = await fetchFn(opts.url, {
520
+ method: 'POST',
521
+ headers: {
522
+ 'content-type': 'application/json',
523
+ accept: 'application/graphql-response+json',
524
+ },
525
+ body: '{ "not a JSON',
526
+ });
527
+ assert('Data entry', (await assertBodyAsExecutionResult(res)).data).toBe(undefined);
528
+ }),
529
+ audit(
530
+ // TODO: convert to MUST after watershed
531
+ 'SHOULD use 4xx or 5xx status codes if parameters are invalid when accepting application/graphql-response+json', async () => {
532
+ const url = new URL(opts.url);
533
+ url.searchParams.set('qeury' /* typo */, '{ __typename }');
534
+ const res = await fetchFn(url.toString(), {
535
+ method: 'GET',
536
+ headers: { accept: 'application/graphql-response+json' },
537
+ });
538
+ assert('Status code', res.status).toBeGreaterThanOrEqual(400);
539
+ assert('Status code', res.status).toBeLessThanOrEqual(599);
540
+ }),
541
+ audit('SHOULD use 400 status code if parameters are invalid when accepting application/graphql-response+json', async () => {
542
+ const url = new URL(opts.url);
543
+ url.searchParams.set('qeury' /* typo */, '{ __typename }');
544
+ const res = await fetchFn(url.toString(), {
545
+ method: 'GET',
546
+ headers: { accept: 'application/graphql-response+json' },
547
+ });
548
+ assert('Status code', res.status).toBe(400);
549
+ }),
550
+ audit('SHOULD not contain the data entry if parameters are invalid when accepting application/graphql-response+json', async () => {
551
+ const url = new URL(opts.url);
552
+ url.searchParams.set('qeury' /* typo */, '{ __typename }');
553
+ const res = await fetchFn(url.toString(), {
554
+ method: 'GET',
555
+ headers: { accept: 'application/graphql-response+json' },
556
+ });
557
+ assert('Data entry', (await assertBodyAsExecutionResult(res)).data).toBe(undefined);
558
+ }),
559
+ audit(
560
+ // TODO: convert to MUST after watershed
561
+ 'SHOULD use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json', async () => {
562
+ const url = new URL(opts.url);
563
+ url.searchParams.set('query', '{');
564
+ const res = await fetchFn(url.toString(), {
565
+ method: 'GET',
566
+ headers: { accept: 'application/graphql-response+json' },
567
+ });
568
+ assert('Status code', res.status).toBeGreaterThanOrEqual(400);
569
+ assert('Status code', res.status).toBeLessThanOrEqual(599);
570
+ }),
571
+ audit('SHOULD use 400 status code on document parsing failure when accepting application/graphql-response+json', async () => {
572
+ const url = new URL(opts.url);
573
+ url.searchParams.set('query', '{');
574
+ const res = await fetchFn(url.toString(), {
575
+ method: 'GET',
576
+ headers: { accept: 'application/graphql-response+json' },
577
+ });
578
+ assert('Status code', res.status).toBe(400);
579
+ }),
580
+ audit('SHOULD not contain the data entry on document parsing failure when accepting application/graphql-response+json', async () => {
581
+ const url = new URL(opts.url);
582
+ url.searchParams.set('query', '{');
583
+ const res = await fetchFn(url.toString(), {
584
+ method: 'GET',
585
+ headers: { accept: 'application/graphql-response+json' },
586
+ });
587
+ assert('Data entry', (await assertBodyAsExecutionResult(res)).data).toBe(undefined);
588
+ }),
589
+ audit(
590
+ // TODO: convert to MUST after watershed
591
+ 'SHOULD use 4xx or 5xx status codes on document validation failure when accepting application/graphql-response+json', async () => {
592
+ const url = new URL(opts.url);
593
+ url.searchParams.set('query', '{ 8f31403dfe404bccbb0e835f2629c6a7 }'); // making sure the field doesnt exist
594
+ const res = await fetchFn(url.toString(), {
595
+ method: 'GET',
596
+ headers: { accept: 'application/graphql-response+json' },
597
+ });
598
+ assert('Status code', res.status).toBeGreaterThanOrEqual(400);
599
+ assert('Status code', res.status).toBeLessThanOrEqual(599);
600
+ }),
601
+ audit('SHOULD use 400 status code on document validation failure when accepting application/graphql-response+json', async () => {
602
+ const url = new URL(opts.url);
603
+ url.searchParams.set('query', '{ 8f31403dfe404bccbb0e835f2629c6a7 }'); // making sure the field doesnt exist
604
+ const res = await fetchFn(url.toString(), {
605
+ method: 'GET',
606
+ headers: { accept: 'application/graphql-response+json' },
607
+ });
608
+ assert('Status code', res.status).toBe(400);
609
+ }),
610
+ audit('SHOULD not contain the data entry on document validation failure when accepting application/graphql-response+json', async () => {
611
+ const url = new URL(opts.url);
612
+ url.searchParams.set('query', '{ 8f31403dfe404bccbb0e835f2629c6a7 }'); // making sure the field doesnt exist
613
+ const res = await fetchFn(url.toString(), {
614
+ method: 'GET',
615
+ headers: { accept: 'application/graphql-response+json' },
616
+ });
617
+ assert('Data entry', (await assertBodyAsExecutionResult(res)).data).toBe(undefined);
618
+ }),
619
+ // TODO: how to fail and have the data entry?
620
+ // audit('MUST use 2xx status code if response contains the data entry and it is not null when accepting application/graphql-response+json'),
621
+ // TODO: how to make an unauthorized request?
622
+ // https://graphql.github.io/graphql-over-http/draft/#sel-EANNNDTAAEVBAAqqc
623
+ // audit('SHOULD use 401 or 403 status codes when the request is not permitted')
624
+ ];
625
+ }
626
+ /**
627
+ * Performs the full list of server audits required for GraphQL over HTTP spec conformance.
628
+ *
629
+ * Please consult the `AuditResult` for more information.
630
+ *
631
+ * @category Audits
632
+ */
633
+ export async function auditServer(opts) {
634
+ const audits = serverAudits(opts);
635
+ // audit tests will throw only on fatal errors, tests are contained within the AuditResult
636
+ return await Promise.all(audits.map(({ fn }) => fn()));
637
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ *
3
+ * audit/utils
4
+ *
5
+ */
6
+ import { 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>>>;