graphql-http 1.17.1 → 1.19.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/README.md +17 -1
- package/lib/audits/common.d.mts +1 -1
- package/lib/audits/common.d.ts +1 -1
- package/lib/audits/render.js +15 -0
- package/lib/audits/render.mjs +15 -0
- package/lib/audits/server.js +42 -172
- package/lib/audits/server.mjs +42 -172
- package/lib/audits/utils.js +6 -3
- package/lib/audits/utils.mjs +6 -3
- package/lib/handler.d.mts +20 -3
- package/lib/handler.d.ts +20 -3
- package/lib/handler.js +36 -14
- package/lib/handler.mjs +37 -15
- package/lib/use/express.js +1 -18
- package/lib/use/express.mjs +1 -18
- package/lib/use/fastify.js +1 -19
- package/lib/use/fastify.mjs +1 -19
- package/lib/use/fetch.d.mts +1 -1
- package/lib/use/fetch.d.ts +1 -1
- package/lib/use/fetch.js +2 -22
- package/lib/use/fetch.mjs +2 -22
- package/lib/use/http.d.mts +1 -1
- package/lib/use/http.d.ts +1 -1
- package/lib/use/http.js +2 -19
- package/lib/use/http.mjs +2 -19
- package/lib/use/http2.d.mts +1 -1
- package/lib/use/http2.d.ts +1 -1
- package/lib/use/http2.js +2 -19
- package/lib/use/http2.mjs +2 -19
- package/lib/use/koa.js +0 -14
- package/lib/use/koa.mjs +0 -14
- package/lib/use/node.d.mts +1 -1
- package/lib/use/node.d.ts +1 -1
- package/lib/use/node.js +1 -1
- package/lib/use/node.mjs +1 -1
- package/lib/use/uWebSockets.d.mts +36 -0
- package/lib/use/uWebSockets.d.ts +36 -0
- package/lib/use/uWebSockets.js +84 -0
- package/lib/use/uWebSockets.mjs +80 -0
- package/lib/utils.d.mts +2 -0
- package/lib/utils.d.ts +2 -0
- package/lib/utils.js +15 -1
- package/lib/utils.mjs +13 -0
- package/package.json +30 -25
- package/umd/graphql-http-audits.js +63 -175
- package/umd/graphql-http-audits.min.js +1 -1
- package/umd/graphql-http-audits.min.js.gz +0 -0
|
@@ -52,10 +52,13 @@
|
|
|
52
52
|
id,
|
|
53
53
|
name,
|
|
54
54
|
status: name.startsWith('MUST')
|
|
55
|
-
? //
|
|
55
|
+
? // failing MUSTs are considered errors
|
|
56
56
|
'error'
|
|
57
|
-
:
|
|
58
|
-
|
|
57
|
+
: name.startsWith('SHOULD')
|
|
58
|
+
? // recommendations are warnings
|
|
59
|
+
'warn'
|
|
60
|
+
: // everything else is truly optional
|
|
61
|
+
'notice',
|
|
59
62
|
reason: err.reason,
|
|
60
63
|
response: err.response,
|
|
61
64
|
};
|
|
@@ -216,9 +219,13 @@
|
|
|
216
219
|
ressert(res).header('content-type').toContain('application/json');
|
|
217
220
|
}),
|
|
218
221
|
audit('80D8', 'SHOULD assume application/json content-type when accept is missing', async () => {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
const res = await fetchFn(await getUrl(opts.url), {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: {
|
|
225
|
+
'content-type': 'application/json',
|
|
226
|
+
},
|
|
227
|
+
body: JSON.stringify({ query: '{ __typename }' }),
|
|
228
|
+
});
|
|
222
229
|
ressert(res).status.toBe(200);
|
|
223
230
|
ressert(res).header('content-type').toContain('application/json');
|
|
224
231
|
}),
|
|
@@ -289,7 +296,7 @@
|
|
|
289
296
|
ressert(res).status.toBeBetween(400, 499);
|
|
290
297
|
}),
|
|
291
298
|
// Request POST
|
|
292
|
-
audit('9ABE', '
|
|
299
|
+
audit('9ABE', 'MAY respond with 4xx status code if content-type is not supplied on POST requests', async () => {
|
|
293
300
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
294
301
|
method: 'POST',
|
|
295
302
|
});
|
|
@@ -303,23 +310,15 @@
|
|
|
303
310
|
});
|
|
304
311
|
ressert(res).status.toBe(200);
|
|
305
312
|
}),
|
|
306
|
-
audit('
|
|
307
|
-
var _a;
|
|
313
|
+
audit('A5BF', 'MAY use 400 status code when request body is missing on POST', async () => {
|
|
308
314
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
309
315
|
method: 'POST',
|
|
310
316
|
headers: { 'content-type': 'application/json' },
|
|
311
317
|
});
|
|
312
|
-
|
|
313
|
-
await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
ressert(res).status.toBe(400);
|
|
317
|
-
}
|
|
318
|
+
ressert(res).status.toBe(400);
|
|
318
319
|
}),
|
|
319
320
|
// Request Parameters
|
|
320
|
-
audit(
|
|
321
|
-
// TODO: convert to MUST after watershed
|
|
322
|
-
'6610', 'SHOULD use 400 status code on missing {query} parameter when accepting application/graphql-response+json', async () => {
|
|
321
|
+
audit('423L', 'MAY use 400 status code on missing {query} parameter', async () => {
|
|
323
322
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
324
323
|
method: 'POST',
|
|
325
324
|
headers: {
|
|
@@ -330,26 +329,11 @@
|
|
|
330
329
|
});
|
|
331
330
|
ressert(res).status.toBe(400);
|
|
332
331
|
}),
|
|
333
|
-
|
|
334
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
335
|
-
method: 'POST',
|
|
336
|
-
headers: {
|
|
337
|
-
'content-type': 'application/json',
|
|
338
|
-
accept: 'application/json',
|
|
339
|
-
},
|
|
340
|
-
body: JSON.stringify({ notquery: '{ __typename }' }),
|
|
341
|
-
});
|
|
342
|
-
ressert(res).status.toBe(200);
|
|
343
|
-
await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
|
|
344
|
-
}),
|
|
345
|
-
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`4F5${index}`,
|
|
346
|
-
// TODO: convert to MUST after watershed
|
|
347
|
-
`SHOULD use 400 status code on ${extendedTypeof(invalid)} {query} parameter when accepting application/graphql-response+json`, async () => {
|
|
332
|
+
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`LKJ${index}`, `MAY use 400 status code on ${extendedTypeof(invalid)} {query} parameter`, async () => {
|
|
348
333
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
349
334
|
method: 'POST',
|
|
350
335
|
headers: {
|
|
351
336
|
'content-type': 'application/json',
|
|
352
|
-
accept: 'application/graphql-response+json',
|
|
353
337
|
},
|
|
354
338
|
body: JSON.stringify({
|
|
355
339
|
query: invalid,
|
|
@@ -357,20 +341,6 @@
|
|
|
357
341
|
});
|
|
358
342
|
ressert(res).status.toBe(400);
|
|
359
343
|
})),
|
|
360
|
-
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`9FE${index}`, `SHOULD use 200 status code with errors field on ${extendedTypeof(invalid)} {query} parameter when accepting application/json`, async () => {
|
|
361
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
362
|
-
method: 'POST',
|
|
363
|
-
headers: {
|
|
364
|
-
'content-type': 'application/json',
|
|
365
|
-
accept: 'application/json',
|
|
366
|
-
},
|
|
367
|
-
body: JSON.stringify({
|
|
368
|
-
query: invalid,
|
|
369
|
-
}),
|
|
370
|
-
});
|
|
371
|
-
ressert(res).status.toBe(200);
|
|
372
|
-
await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
|
|
373
|
-
})),
|
|
374
344
|
audit(
|
|
375
345
|
// TODO: convert to MUST after watershed
|
|
376
346
|
'34A2', 'SHOULD allow string {query} parameter when accepting application/graphql-response+json', async () => {
|
|
@@ -400,14 +370,11 @@
|
|
|
400
370
|
ressert(res).status.toBe(200);
|
|
401
371
|
await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
402
372
|
}),
|
|
403
|
-
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`
|
|
404
|
-
// TODO: convert to MUST after watershed
|
|
405
|
-
`SHOULD use 400 status code on ${extendedTypeof(invalid)} {operationName} parameter when accepting application/graphql-response+json`, async () => {
|
|
373
|
+
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`6C0${index}`, `MAY use 400 status code on ${extendedTypeof(invalid)} {operationName} parameter`, async () => {
|
|
406
374
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
407
375
|
method: 'POST',
|
|
408
376
|
headers: {
|
|
409
377
|
'content-type': 'application/json',
|
|
410
|
-
accept: 'application/graphql-response+json',
|
|
411
378
|
},
|
|
412
379
|
body: JSON.stringify({
|
|
413
380
|
operationName: invalid,
|
|
@@ -416,21 +383,6 @@
|
|
|
416
383
|
});
|
|
417
384
|
ressert(res).status.toBe(400);
|
|
418
385
|
})),
|
|
419
|
-
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`FB9${index}`, `SHOULD use 200 status code with errors field on ${extendedTypeof(invalid)} {operationName} parameter when accepting application/json`, async () => {
|
|
420
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
421
|
-
method: 'POST',
|
|
422
|
-
headers: {
|
|
423
|
-
'content-type': 'application/json',
|
|
424
|
-
accept: 'application/json',
|
|
425
|
-
},
|
|
426
|
-
body: JSON.stringify({
|
|
427
|
-
operationName: invalid,
|
|
428
|
-
query: '{ __typename }',
|
|
429
|
-
}),
|
|
430
|
-
});
|
|
431
|
-
ressert(res).status.toBe(200);
|
|
432
|
-
await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
|
|
433
|
-
})),
|
|
434
386
|
audit(
|
|
435
387
|
// TODO: convert to MUST after watershed
|
|
436
388
|
'8161', 'SHOULD allow string {operationName} parameter when accepting application/graphql-response+json', async () => {
|
|
@@ -496,14 +448,11 @@
|
|
|
496
448
|
await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
497
449
|
}),
|
|
498
450
|
]),
|
|
499
|
-
...['string', 0, false, ['array']].map((invalid, index) => audit(`
|
|
500
|
-
// TODO: convert to MUST after watershed
|
|
501
|
-
`SHOULD use 400 status code on ${extendedTypeof(invalid)} {variables} parameter when accepting application/graphql-response+json`, async () => {
|
|
451
|
+
...['string', 0, false, ['array']].map((invalid, index) => audit(`476${index}`, `MAY use 400 status code on ${extendedTypeof(invalid)} {variables} parameter`, async () => {
|
|
502
452
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
503
453
|
method: 'POST',
|
|
504
454
|
headers: {
|
|
505
455
|
'content-type': 'application/json',
|
|
506
|
-
accept: 'application/graphql-response+json',
|
|
507
456
|
},
|
|
508
457
|
body: JSON.stringify({
|
|
509
458
|
query: '{ __typename }',
|
|
@@ -512,21 +461,6 @@
|
|
|
512
461
|
});
|
|
513
462
|
ressert(res).status.toBe(400);
|
|
514
463
|
})),
|
|
515
|
-
...['string', 0, false, ['array']].map((invalid, index) => audit(`F05${index}`, `SHOULD use 200 status code with errors field on ${extendedTypeof(invalid)} {variables} parameter when accepting application/json`, async () => {
|
|
516
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
517
|
-
method: 'POST',
|
|
518
|
-
headers: {
|
|
519
|
-
'content-type': 'application/json',
|
|
520
|
-
accept: 'application/json',
|
|
521
|
-
},
|
|
522
|
-
body: JSON.stringify({
|
|
523
|
-
query: '{ __typename }',
|
|
524
|
-
variables: invalid,
|
|
525
|
-
}),
|
|
526
|
-
});
|
|
527
|
-
ressert(res).status.toBe(200);
|
|
528
|
-
await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
|
|
529
|
-
})),
|
|
530
464
|
audit(
|
|
531
465
|
// TODO: convert to MUST after watershed
|
|
532
466
|
'2EA1', 'SHOULD allow map {variables} parameter when accepting application/graphql-response+json', async () => {
|
|
@@ -583,14 +517,13 @@
|
|
|
583
517
|
ressert(res).status.toBe(200);
|
|
584
518
|
await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
585
519
|
}),
|
|
586
|
-
...['string', 0, false, ['array']].map((invalid, index) => audit(`
|
|
520
|
+
...['string', 0, false, ['array']].map((invalid, index) => audit(`58B${index}`,
|
|
587
521
|
// TODO: convert to MUST after watershed
|
|
588
|
-
`
|
|
522
|
+
`MAY use 400 status code on ${extendedTypeof(invalid)} {extensions} parameter`, async () => {
|
|
589
523
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
590
524
|
method: 'POST',
|
|
591
525
|
headers: {
|
|
592
526
|
'content-type': 'application/json',
|
|
593
|
-
accept: 'application/graphql-response+json',
|
|
594
527
|
},
|
|
595
528
|
body: JSON.stringify({
|
|
596
529
|
query: '{ __typename }',
|
|
@@ -599,21 +532,6 @@
|
|
|
599
532
|
});
|
|
600
533
|
ressert(res).status.toBe(400);
|
|
601
534
|
})),
|
|
602
|
-
...['string', 0, false, ['array']].map((invalid, index) => audit(`368${index}`, `SHOULD use 200 status code with errors field on ${extendedTypeof(invalid)} {extensions} parameter when accepting application/json`, async () => {
|
|
603
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
604
|
-
method: 'POST',
|
|
605
|
-
headers: {
|
|
606
|
-
'content-type': 'application/json',
|
|
607
|
-
accept: 'application/json',
|
|
608
|
-
},
|
|
609
|
-
body: JSON.stringify({
|
|
610
|
-
query: '{ __typename }',
|
|
611
|
-
extensions: invalid,
|
|
612
|
-
}),
|
|
613
|
-
});
|
|
614
|
-
ressert(res).status.toBe(200);
|
|
615
|
-
await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
|
|
616
|
-
})),
|
|
617
535
|
audit(
|
|
618
536
|
// TODO: convert to MUST after watershed
|
|
619
537
|
'428F', 'SHOULD allow map {extensions} parameter when accepting application/graphql-response+json', async () => {
|
|
@@ -645,136 +563,91 @@
|
|
|
645
563
|
ressert(res).status.toBe(200);
|
|
646
564
|
await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
647
565
|
}),
|
|
648
|
-
|
|
649
|
-
// Response application/json
|
|
650
|
-
audit('D477', 'SHOULD use 200 status code on JSON parsing failure when accepting application/json', async () => {
|
|
566
|
+
audit('B6DC', 'MAY use 4xx or 5xx status codes on JSON parsing failure', async () => {
|
|
651
567
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
652
568
|
method: 'POST',
|
|
653
569
|
headers: {
|
|
654
570
|
'content-type': 'application/json',
|
|
655
|
-
accept: 'application/json',
|
|
656
571
|
},
|
|
657
572
|
body: '{ "not a JSON',
|
|
658
573
|
});
|
|
659
|
-
ressert(res).status.
|
|
660
|
-
}),
|
|
661
|
-
audit('F5AF', 'SHOULD use 200 status code if parameters are invalid when accepting application/json', async () => {
|
|
662
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
663
|
-
method: 'POST',
|
|
664
|
-
headers: {
|
|
665
|
-
'content-type': 'application/json',
|
|
666
|
-
accept: 'application/json',
|
|
667
|
-
},
|
|
668
|
-
body: JSON.stringify({
|
|
669
|
-
qeury: /* typo */ '{ __typename }',
|
|
670
|
-
}),
|
|
671
|
-
});
|
|
672
|
-
ressert(res).status.toBe(200);
|
|
574
|
+
ressert(res).status.toBeBetween(400, 499);
|
|
673
575
|
}),
|
|
674
|
-
audit('
|
|
576
|
+
audit('BCF8', 'MAY use 400 status code on JSON parsing failure', async () => {
|
|
675
577
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
676
578
|
method: 'POST',
|
|
677
579
|
headers: {
|
|
678
580
|
'content-type': 'application/json',
|
|
679
|
-
accept: 'application/json',
|
|
680
581
|
},
|
|
681
|
-
body:
|
|
582
|
+
body: '{ "not a JSON',
|
|
682
583
|
});
|
|
683
|
-
ressert(res).status.toBe(
|
|
584
|
+
ressert(res).status.toBe(400);
|
|
684
585
|
}),
|
|
685
|
-
audit('
|
|
586
|
+
audit('8764', 'MAY use 4xx or 5xx status codes if parameters are invalid', async () => {
|
|
686
587
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
687
588
|
method: 'POST',
|
|
688
589
|
headers: {
|
|
689
590
|
'content-type': 'application/json',
|
|
690
|
-
accept: 'application/json',
|
|
691
591
|
},
|
|
692
592
|
body: JSON.stringify({
|
|
693
|
-
|
|
593
|
+
qeury /* typo */: '{ __typename }',
|
|
694
594
|
}),
|
|
695
595
|
});
|
|
696
|
-
ressert(res).status.
|
|
596
|
+
ressert(res).status.toBeBetween(400, 599);
|
|
697
597
|
}),
|
|
698
|
-
audit('
|
|
598
|
+
audit('3E3A', 'MAY use 400 status code if parameters are invalid', async () => {
|
|
699
599
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
700
600
|
method: 'POST',
|
|
701
601
|
headers: {
|
|
702
602
|
'content-type': 'application/json',
|
|
703
|
-
accept: 'application/json',
|
|
704
603
|
},
|
|
705
604
|
body: JSON.stringify({
|
|
706
|
-
|
|
707
|
-
variables: { id: null },
|
|
605
|
+
qeury: /* typo */ '{ __typename }',
|
|
708
606
|
}),
|
|
709
607
|
});
|
|
710
|
-
ressert(res).status.toBe(200);
|
|
711
|
-
}),
|
|
712
|
-
// Response application/graphql-response+json
|
|
713
|
-
audit(
|
|
714
|
-
// TODO: convert to MUST after watershed
|
|
715
|
-
'60AA', 'SHOULD use 4xx or 5xx status codes on JSON parsing failure when accepting application/graphql-response+json', async () => {
|
|
716
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
717
|
-
method: 'POST',
|
|
718
|
-
headers: {
|
|
719
|
-
'content-type': 'application/json',
|
|
720
|
-
accept: 'application/graphql-response+json',
|
|
721
|
-
},
|
|
722
|
-
body: '{ "not a JSON',
|
|
723
|
-
});
|
|
724
|
-
ressert(res).status.toBeBetween(400, 499);
|
|
725
|
-
}),
|
|
726
|
-
audit('2163', 'SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json', async () => {
|
|
727
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
728
|
-
method: 'POST',
|
|
729
|
-
headers: {
|
|
730
|
-
'content-type': 'application/json',
|
|
731
|
-
accept: 'application/graphql-response+json',
|
|
732
|
-
},
|
|
733
|
-
body: '{ "not a JSON',
|
|
734
|
-
});
|
|
735
608
|
ressert(res).status.toBe(400);
|
|
736
609
|
}),
|
|
737
|
-
audit(
|
|
738
|
-
//
|
|
739
|
-
'
|
|
610
|
+
// TODO: audit('39AA', 'MUST accept a map for the {extensions} parameter'),
|
|
611
|
+
// Response application/json
|
|
612
|
+
audit('572B', 'SHOULD use 200 status code on document parsing failure when accepting application/json', async () => {
|
|
740
613
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
741
614
|
method: 'POST',
|
|
742
615
|
headers: {
|
|
743
616
|
'content-type': 'application/json',
|
|
744
|
-
accept: 'application/
|
|
617
|
+
accept: 'application/json',
|
|
745
618
|
},
|
|
746
|
-
body: JSON.stringify({
|
|
747
|
-
qeury /* typo */: '{ __typename }',
|
|
748
|
-
}),
|
|
619
|
+
body: JSON.stringify({ query: '{' }),
|
|
749
620
|
});
|
|
750
|
-
ressert(res).status.
|
|
621
|
+
ressert(res).status.toBe(200);
|
|
751
622
|
}),
|
|
752
|
-
audit('
|
|
623
|
+
audit('FDE2', 'SHOULD use 200 status code on document validation failure when accepting application/json', async () => {
|
|
753
624
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
754
625
|
method: 'POST',
|
|
755
626
|
headers: {
|
|
756
627
|
'content-type': 'application/json',
|
|
757
|
-
accept: 'application/
|
|
628
|
+
accept: 'application/json',
|
|
758
629
|
},
|
|
759
630
|
body: JSON.stringify({
|
|
760
|
-
|
|
631
|
+
query: '{ 8f31403dfe404bccbb0e835f2629c6a7 }', // making sure the field doesnt exist
|
|
761
632
|
}),
|
|
762
633
|
});
|
|
763
|
-
ressert(res).status.toBe(
|
|
634
|
+
ressert(res).status.toBe(200);
|
|
764
635
|
}),
|
|
765
|
-
audit('
|
|
636
|
+
audit('7B9B', 'SHOULD use a status code of 200 on variable coercion failure when accepting application/json', async () => {
|
|
766
637
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
767
638
|
method: 'POST',
|
|
768
639
|
headers: {
|
|
769
640
|
'content-type': 'application/json',
|
|
770
|
-
accept: 'application/
|
|
641
|
+
accept: 'application/json',
|
|
771
642
|
},
|
|
772
643
|
body: JSON.stringify({
|
|
773
|
-
|
|
644
|
+
query: 'query CoerceFailure($id: ID!){ __typename }',
|
|
645
|
+
variables: { id: null },
|
|
774
646
|
}),
|
|
775
647
|
});
|
|
776
|
-
|
|
648
|
+
ressert(res).status.toBe(200);
|
|
777
649
|
}),
|
|
650
|
+
// Response application/graphql-response+json
|
|
778
651
|
audit(
|
|
779
652
|
// TODO: convert to MUST after watershed
|
|
780
653
|
'865D', 'SHOULD use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json', async () => {
|
|
@@ -908,6 +781,7 @@
|
|
|
908
781
|
const grouped = {
|
|
909
782
|
total: 0,
|
|
910
783
|
ok: [],
|
|
784
|
+
notice: [],
|
|
911
785
|
warn: [],
|
|
912
786
|
error: [],
|
|
913
787
|
};
|
|
@@ -930,6 +804,9 @@
|
|
|
930
804
|
if (grouped.ok.length) {
|
|
931
805
|
report += `<li><span style="font-family: monospace">✅</span> <b>${grouped.ok.length}</b> pass</li>\n`;
|
|
932
806
|
}
|
|
807
|
+
if (grouped.notice.length) {
|
|
808
|
+
report += `<li><span style="font-family: monospace">💡</span> <b>${grouped.notice.length}</b> notices (suggestions)</li>\n`;
|
|
809
|
+
}
|
|
933
810
|
if (grouped.warn.length) {
|
|
934
811
|
// TODO: warning sign is rendered as "⚠️" in markdown instead of the emoji
|
|
935
812
|
report += `<li><span style="font-family: monospace">⚠️</span> <b>${grouped.warn.length}</b> warnings (optional)</li>\n`;
|
|
@@ -948,6 +825,17 @@
|
|
|
948
825
|
report += '</ol>\n';
|
|
949
826
|
report += '\n';
|
|
950
827
|
}
|
|
828
|
+
if (grouped.notice.length) {
|
|
829
|
+
report += `<h2>Notices</h2>\n`;
|
|
830
|
+
report +=
|
|
831
|
+
'The server <i>MAY</i> support these, but are truly optional. These are suggestions following recommended conventions.\n';
|
|
832
|
+
report += '<ol>\n';
|
|
833
|
+
for (const [, result] of grouped.notice.entries()) {
|
|
834
|
+
report += await printAuditFail(result);
|
|
835
|
+
}
|
|
836
|
+
report += '</ol>\n';
|
|
837
|
+
report += '\n';
|
|
838
|
+
}
|
|
951
839
|
if (grouped.warn.length) {
|
|
952
840
|
report += `<h2>Warnings</h2>\n`;
|
|
953
841
|
report += 'The server <i>SHOULD</i> support these, but is not required.\n';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("graphql")):"function"==typeof define&&define.amd?define(["exports","graphql"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).graphqlHttpAudits={})}(this,(function(e){"use strict";function t(e){return null===e?"null":Array.isArray(e)?"array":typeof e}function a(e,t,a){return{id:e,name:t,fn:async()=>{try{return await a(),{id:e,name:t,status:"ok"}}catch(a){if(!(a instanceof n))throw a;return{id:e,name:t,status:t.startsWith("MUST")?"error":"warn",reason:a.reason,response:a.response}}}}}class n{constructor(e,t){this.response=e,this.reason=t}}function o(e){return{status:{toBe(t){if(e.status!==t)throw new n(e,`Response status code is not ${t}`)},toBeBetween:(t,a)=>{if(!(t<=e.status&&e.status<=a))throw new n(e,`Response status is not between ${t} and ${a}`)}},header:t=>({toContain(a){var o;if(!(null===(o=e.headers.get(t))||void 0===o?void 0:o.includes(a)))throw new n(e,`Response header ${t} does not contain ${a}`)},notToContain(a){var o;if(null===(o=e.headers.get(t))||void 0===o?void 0:o.includes(a))throw new n(e,`Response header ${t} contains ${a}`)}}),bodyAsExecutionResult:{data:{async toBe(t){const a=e.clone();if((await s(e)).data!==t)throw new n(a,`Response body execution result data is not "${t}"`)}},async toHaveProperty(t){const a=e.clone();if(!(t in await s(e)))throw new n(a,`Response body execution result does not have a property "${t}"`)},async notToHaveProperty(t){const a=e.clone();if(t in await s(e))throw new n(a,`Response body execution result has a property "${t}"`)}}}}async function s(e){let t,a;try{const a=new TextDecoder("utf-8"),n=await e.arrayBuffer();t=a.decode(n)}catch(t){throw new n(e,"Response body is not UTF-8 encoded")}try{a=JSON.parse(t)}catch(t){throw new n(e,"Response body is not valid JSON")}return a}function i(e){const s=e.fetchFn||fetch;return[a("22EB","SHOULD accept application/graphql-response+json and match the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/graphql-response+json")})),a("4655","MUST accept application/json and match the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("47DE","SHOULD accept */* and use application/json for the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"*/*"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("80D8","SHOULD assume application/json content-type when accept is missing",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","{ __typename }");const a=await s(t.toString());o(a).status.toBe(200),o(a).header("content-type").toContain("application/json")})),a("82A3","MUST use utf-8 encoding when responding",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200);try{new TextDecoder("utf-8").decode(await t.arrayBuffer())}catch(e){throw new n(t,"Response body is not UTF-8 encoded")}})),a("BF61","MUST accept utf-8 encoded request",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json; charset=utf-8"},body:JSON.stringify({query:'{ __type(name: "Run🏃Swim🏊") { name } }'})})).status.toBe(200)})),a("78D5","MUST assume utf-8 in request if encoding is unspecified",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("2C94","MUST accept POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("5A70","MAY accept application/x-www-form-urlencoded formatted GET requests",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","{ __typename }");o(await s(t.toString())).status.toBe(200)})),a("9C48","MAY NOT allow executing mutations on GET requests",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","mutation { __typename }");o(await s(t.toString(),{headers:{accept:"application/graphql-response+json"}})).status.toBeBetween(400,499)})),a("9ABE","SHOULD respond with 4xx status code if content-type is not supplied on POST requests",(async()=>{o(await s(await r(e.url),{method:"POST"})).status.toBeBetween(400,499)})),a("03D4","MUST accept application/json POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("7267","MUST require a request body on POST",(async()=>{var t;const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"}});(null===(t=a.headers.get("content-type"))||void 0===t?void 0:t.includes("application/json"))?await o(a).bodyAsExecutionResult.toHaveProperty("errors"):o(a).status.toBe(400)})),a("6610","SHOULD use 400 status code on missing {query} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({notquery:"{ __typename }"})})).status.toBe(400)})),a("3715","SHOULD use 200 status code with errors field on missing {query} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({notquery:"{ __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.toHaveProperty("errors")})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`4F5${i}`,`SHOULD use 400 status code on ${t(n)} {query} parameter when accepting application/graphql-response+json`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:n})})).status.toBe(400)})))),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`9FE${i}`,`SHOULD use 200 status code with errors field on ${t(n)} {query} parameter when accepting application/json`,(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:n})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.toHaveProperty("errors")})))),a("34A2","SHOULD allow string {query} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("13EE","MUST allow string {query} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`E3E${i}`,`SHOULD use 400 status code on ${t(n)} {operationName} parameter when accepting application/graphql-response+json`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({operationName:n,query:"{ __typename }"})})).status.toBe(400)})))),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`FB9${i}`,`SHOULD use 200 status code with errors field on ${t(n)} {operationName} parameter when accepting application/json`,(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({operationName:n,query:"{ __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.toHaveProperty("errors")})))),a("8161","SHOULD allow string {operationName} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})})).status.toBe(200)})),a("B8B3","MUST allow string {operationName} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),...["variables","operationName","extensions"].flatMap(((t,n)=>[a(`94B${n}`,`SHOULD allow null {${t}} parameter when accepting application/graphql-response+json`,(async()=>{const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",[t]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),a(`022${n}`,`MUST allow null {${t}} parameter when accepting application/json`,(async()=>{const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",[t]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")}))])),...["string",0,!1,["array"]].map(((n,i)=>a(`69B${i}`,`SHOULD use 400 status code on ${t(n)} {variables} parameter when accepting application/graphql-response+json`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",variables:n})})).status.toBe(400)})))),...["string",0,!1,["array"]].map(((n,i)=>a(`F05${i}`,`SHOULD use 200 status code with errors field on ${t(n)} {variables} parameter when accepting application/json`,(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",variables:n})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.toHaveProperty("errors")})))),a("2EA1","SHOULD allow map {variables} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"query Type($name: String!) { __type(name: $name) { name } }",variables:{name:"sometype"}})})).status.toBe(200)})),a("28B9","MUST allow map {variables} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"query Type($name: String!) { __type(name: $name) { name } }",variables:{name:"sometype"}})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),a("D6D5","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/graphql-response+json",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),t.searchParams.set("variables",JSON.stringify({name:"sometype"}));o(await s(t.toString(),{method:"GET",headers:{accept:"application/graphql-response+json"}})).status.toBe(200)})),a("6A70","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),t.searchParams.set("variables",JSON.stringify({name:"sometype"}));const a=await s(t.toString(),{method:"GET",headers:{accept:"application/json"}});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),...["string",0,!1,["array"]].map(((n,i)=>a(`904${i}`,`SHOULD use 400 status code on ${t(n)} {extensions} parameter when accepting application/graphql-response+json`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",extensions:n})})).status.toBe(400)})))),...["string",0,!1,["array"]].map(((n,i)=>a(`368${i}`,`SHOULD use 200 status code with errors field on ${t(n)} {extensions} parameter when accepting application/json`,(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:n})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.toHaveProperty("errors")})))),a("428F","SHOULD allow map {extensions} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})})).status.toBe(200)})),a("1B7A","MUST allow map {extensions} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),a("D477","SHOULD use 200 status code on JSON parsing failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:'{ "not a JSON'})).status.toBe(200)})),a("F5AF","SHOULD use 200 status code if parameters are invalid when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBe(200)})),a("572B","SHOULD use 200 status code on document parsing failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{"})})).status.toBe(200)})),a("FDE2","SHOULD use 200 status code on document validation failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBe(200)})),a("7B9B","SHOULD use a status code of 200 on variable coercion failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"query CoerceFailure($id: ID!){ __typename }",variables:{id:null}})})).status.toBe(200)})),a("60AA","SHOULD use 4xx or 5xx status codes on JSON parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:'{ "not a JSON'})).status.toBeBetween(400,499)})),a("2163","SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:'{ "not a JSON'})).status.toBe(400)})),a("3E36","SHOULD use 4xx or 5xx status codes if parameters are invalid when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBeBetween(400,599)})),a("17C5","SHOULD use 400 status code if parameters are invalid when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBe(400)})),a("34D6","SHOULD not contain the data entry if parameters are invalid when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({qeury:"{ __typename }"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("865D","SHOULD use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})})).status.toBeBetween(400,599)})),a("556A","SHOULD use 400 status code on document parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})})).status.toBe(400)})),a("D586","SHOULD not contain the data entry on document parsing failure when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("51FE","SHOULD use 4xx or 5xx status codes on document validation failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBeBetween(400,599)})),a("74FF","SHOULD use 400 status code on document validation failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBe(400)})),a("5E5B","SHOULD not contain the data entry on document validation failure when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("86EE","SHOULD use a status code of 400 on variable coercion failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"query CoerceFailure($id: ID!){ __typename }",variables:{id:null}})})).status.toBe(400)}))]}async function r(e){return"function"==typeof e?await e():e}async function p(e){let t="";t+=`<li><code>${e.id}</code> ${e.name}\n`,t+="<details>\n",t+=`<summary>${function(e,t=1024){if(e.length>t)return e.substring(0,t)+"...";return e}(e.reason)}</summary>\n`,t+='<pre><code class="lang-json">';const a=e.response,n={};for(const[e,t]of a.headers.entries())"date"===e?n[e]="<timestamp>":["cf-ray","server-timing"].includes(e)?n[e]="<omitted>":n[e]=t;let o,s="";try{s=await a.text(),o=JSON.parse(s)}catch(e){}return t+=JSON.stringify({status:a.status,statusText:a.statusText,headers:n,body:o||((null==s?void 0:s.length)>5120?"<body is too long>":s)||null},((e,t)=>{if(null!=t&&"object"==typeof t&&!Array.isArray(t)){const e={};return Object.keys(t).sort().reverse().reduce(((e,a)=>(e[a]=t[a],e)),e)}return t}),2)+"\n",t+="</code></pre>\n",t+="</details>\n",t+="</li>\n",t}e.auditServer=async function(e){const t=i(e);return await Promise.all(t.map((({fn:e})=>e())))},e.renderAuditResultsToHTML=async function(e){const t={total:0,ok:[],warn:[],error:[]};for(const a of e)t.total++,a.status,t[a.status].push(a);let a="<i>* This report was auto-generated by graphql-http</i>\n";if(a+="\n",a+="<h1>GraphQL over HTTP audit report</h1>\n",a+="\n",a+="<ul>\n",a+=`<li><b>${t.total}</b> audits in total</li>\n`,t.ok.length&&(a+=`<li><span style="font-family: monospace">✅</span> <b>${t.ok.length}</b> pass</li>\n`),t.warn.length&&(a+=`<li><span style="font-family: monospace">⚠️</span> <b>${t.warn.length}</b> warnings (optional)</li>\n`),t.error.length&&(a+=`<li><span style="font-family: monospace">❌</span> <b>${t.error.length}</b> errors (required)</li>\n`),a+="</ul>\n",a+="\n",t.ok.length){a+="<h2>Passing</h2>\n",a+="<ol>\n";for(const[,e]of t.ok.entries())a+=`<li><code>${e.id}</code> ${e.name}</li>\n`;a+="</ol>\n",a+="\n"}if(t.warn.length){a+="<h2>Warnings</h2>\n",a+="The server <i>SHOULD</i> support these, but is not required.\n",a+="<ol>\n";for(const[,e]of t.warn.entries())a+=await p(e);a+="</ol>\n",a+="\n"}if(t.error.length){a+="<h2>Errors</h2>\n",a+="The server <b>MUST</b> support these.\n",a+="<ol>\n";for(const[,e]of t.error.entries())a+=await p(e);a+="</ol>\n"}return a},e.serverAudits=i}));
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("graphql")):"function"==typeof define&&define.amd?define(["exports","graphql"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).graphqlHttpAudits={})}(this,(function(e){"use strict";function t(e){return null===e?"null":Array.isArray(e)?"array":typeof e}function a(e,t,a){return{id:e,name:t,fn:async()=>{try{return await a(),{id:e,name:t,status:"ok"}}catch(a){if(!(a instanceof n))throw a;return{id:e,name:t,status:t.startsWith("MUST")?"error":t.startsWith("SHOULD")?"warn":"notice",reason:a.reason,response:a.response}}}}}class n{constructor(e,t){this.response=e,this.reason=t}}function o(e){return{status:{toBe(t){if(e.status!==t)throw new n(e,`Response status code is not ${t}`)},toBeBetween:(t,a)=>{if(!(t<=e.status&&e.status<=a))throw new n(e,`Response status is not between ${t} and ${a}`)}},header:t=>({toContain(a){var o;if(!(null===(o=e.headers.get(t))||void 0===o?void 0:o.includes(a)))throw new n(e,`Response header ${t} does not contain ${a}`)},notToContain(a){var o;if(null===(o=e.headers.get(t))||void 0===o?void 0:o.includes(a))throw new n(e,`Response header ${t} contains ${a}`)}}),bodyAsExecutionResult:{data:{async toBe(t){const a=e.clone();if((await s(e)).data!==t)throw new n(a,`Response body execution result data is not "${t}"`)}},async toHaveProperty(t){const a=e.clone();if(!(t in await s(e)))throw new n(a,`Response body execution result does not have a property "${t}"`)},async notToHaveProperty(t){const a=e.clone();if(t in await s(e))throw new n(a,`Response body execution result has a property "${t}"`)}}}}async function s(e){let t,a;try{const a=new TextDecoder("utf-8"),n=await e.arrayBuffer();t=a.decode(n)}catch(t){throw new n(e,"Response body is not UTF-8 encoded")}try{a=JSON.parse(t)}catch(t){throw new n(e,"Response body is not valid JSON")}return a}function i(e){const s=e.fetchFn||fetch;return[a("22EB","SHOULD accept application/graphql-response+json and match the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/graphql-response+json")})),a("4655","MUST accept application/json and match the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("47DE","SHOULD accept */* and use application/json for the content-type",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"*/*"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("80D8","SHOULD assume application/json content-type when accept is missing",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),o(t).header("content-type").toContain("application/json")})),a("82A3","MUST use utf-8 encoding when responding",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200);try{new TextDecoder("utf-8").decode(await t.arrayBuffer())}catch(e){throw new n(t,"Response body is not UTF-8 encoded")}})),a("BF61","MUST accept utf-8 encoded request",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json; charset=utf-8"},body:JSON.stringify({query:'{ __type(name: "Run🏃Swim🏊") { name } }'})})).status.toBe(200)})),a("78D5","MUST assume utf-8 in request if encoding is unspecified",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("2C94","MUST accept POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("5A70","MAY accept application/x-www-form-urlencoded formatted GET requests",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","{ __typename }");o(await s(t.toString())).status.toBe(200)})),a("9C48","MAY NOT allow executing mutations on GET requests",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","mutation { __typename }");o(await s(t.toString(),{headers:{accept:"application/graphql-response+json"}})).status.toBeBetween(400,499)})),a("9ABE","MAY respond with 4xx status code if content-type is not supplied on POST requests",(async()=>{o(await s(await r(e.url),{method:"POST"})).status.toBeBetween(400,499)})),a("03D4","MUST accept application/json POST requests",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("A5BF","MAY use 400 status code when request body is missing on POST",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"}})).status.toBe(400)})),a("423L","MAY use 400 status code on missing {query} parameter",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({notquery:"{ __typename }"})})).status.toBe(400)})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`LKJ${i}`,`MAY use 400 status code on ${t(n)} {query} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:n})})).status.toBe(400)})))),a("34A2","SHOULD allow string {query} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("13EE","MUST allow string {query} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`6C0${i}`,`MAY use 400 status code on ${t(n)} {operationName} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({operationName:n,query:"{ __typename }"})})).status.toBe(400)})))),a("8161","SHOULD allow string {operationName} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})})).status.toBe(200)})),a("B8B3","MUST allow string {operationName} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),...["variables","operationName","extensions"].flatMap(((t,n)=>[a(`94B${n}`,`SHOULD allow null {${t}} parameter when accepting application/graphql-response+json`,(async()=>{const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",[t]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),a(`022${n}`,`MUST allow null {${t}} parameter when accepting application/json`,(async()=>{const a=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",[t]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")}))])),...["string",0,!1,["array"]].map(((n,i)=>a(`476${i}`,`MAY use 400 status code on ${t(n)} {variables} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }",variables:n})})).status.toBe(400)})))),a("2EA1","SHOULD allow map {variables} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"query Type($name: String!) { __type(name: $name) { name } }",variables:{name:"sometype"}})})).status.toBe(200)})),a("28B9","MUST allow map {variables} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"query Type($name: String!) { __type(name: $name) { name } }",variables:{name:"sometype"}})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),a("D6D5","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/graphql-response+json",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),t.searchParams.set("variables",JSON.stringify({name:"sometype"}));o(await s(t.toString(),{method:"GET",headers:{accept:"application/graphql-response+json"}})).status.toBe(200)})),a("6A70","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/json",(async()=>{const t=new URL(await r(e.url));t.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),t.searchParams.set("variables",JSON.stringify({name:"sometype"}));const a=await s(t.toString(),{method:"GET",headers:{accept:"application/json"}});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),...["string",0,!1,["array"]].map(((n,i)=>a(`58B${i}`,`MAY use 400 status code on ${t(n)} {extensions} parameter`,(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:n})})).status.toBe(400)})))),a("428F","SHOULD allow map {extensions} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})})).status.toBe(200)})),a("1B7A","MUST allow map {extensions} parameter when accepting application/json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})});o(t).status.toBe(200),await o(t).bodyAsExecutionResult.notToHaveProperty("errors")})),a("B6DC","MAY use 4xx or 5xx status codes on JSON parsing failure",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:'{ "not a JSON'})).status.toBeBetween(400,499)})),a("BCF8","MAY use 400 status code on JSON parsing failure",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:'{ "not a JSON'})).status.toBe(400)})),a("8764","MAY use 4xx or 5xx status codes if parameters are invalid",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBeBetween(400,599)})),a("3E3A","MAY use 400 status code if parameters are invalid",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({qeury:"{ __typename }"})})).status.toBe(400)})),a("572B","SHOULD use 200 status code on document parsing failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{"})})).status.toBe(200)})),a("FDE2","SHOULD use 200 status code on document validation failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBe(200)})),a("7B9B","SHOULD use a status code of 200 on variable coercion failure when accepting application/json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"query CoerceFailure($id: ID!){ __typename }",variables:{id:null}})})).status.toBe(200)})),a("865D","SHOULD use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})})).status.toBeBetween(400,599)})),a("556A","SHOULD use 400 status code on document parsing failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})})).status.toBe(400)})),a("D586","SHOULD not contain the data entry on document parsing failure when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("51FE","SHOULD use 4xx or 5xx status codes on document validation failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBeBetween(400,599)})),a("74FF","SHOULD use 400 status code on document validation failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).status.toBe(400)})),a("5E5B","SHOULD not contain the data entry on document validation failure when accepting application/graphql-response+json",(async()=>{const t=await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})});await o(t).bodyAsExecutionResult.data.toBe(void 0)})),a("86EE","SHOULD use a status code of 400 on variable coercion failure when accepting application/graphql-response+json",(async()=>{o(await s(await r(e.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"query CoerceFailure($id: ID!){ __typename }",variables:{id:null}})})).status.toBe(400)}))]}async function r(e){return"function"==typeof e?await e():e}async function p(e){let t="";t+=`<li><code>${e.id}</code> ${e.name}\n`,t+="<details>\n",t+=`<summary>${function(e,t=1024){if(e.length>t)return e.substring(0,t)+"...";return e}(e.reason)}</summary>\n`,t+='<pre><code class="lang-json">';const a=e.response,n={};for(const[e,t]of a.headers.entries())"date"===e?n[e]="<timestamp>":["cf-ray","server-timing"].includes(e)?n[e]="<omitted>":n[e]=t;let o,s="";try{s=await a.text(),o=JSON.parse(s)}catch(e){}return t+=JSON.stringify({status:a.status,statusText:a.statusText,headers:n,body:o||((null==s?void 0:s.length)>5120?"<body is too long>":s)||null},((e,t)=>{if(null!=t&&"object"==typeof t&&!Array.isArray(t)){const e={};return Object.keys(t).sort().reverse().reduce(((e,a)=>(e[a]=t[a],e)),e)}return t}),2)+"\n",t+="</code></pre>\n",t+="</details>\n",t+="</li>\n",t}e.auditServer=async function(e){const t=i(e);return await Promise.all(t.map((({fn:e})=>e())))},e.renderAuditResultsToHTML=async function(e){const t={total:0,ok:[],notice:[],warn:[],error:[]};for(const a of e)t.total++,a.status,t[a.status].push(a);let a="<i>* This report was auto-generated by graphql-http</i>\n";if(a+="\n",a+="<h1>GraphQL over HTTP audit report</h1>\n",a+="\n",a+="<ul>\n",a+=`<li><b>${t.total}</b> audits in total</li>\n`,t.ok.length&&(a+=`<li><span style="font-family: monospace">✅</span> <b>${t.ok.length}</b> pass</li>\n`),t.notice.length&&(a+=`<li><span style="font-family: monospace">💡</span> <b>${t.notice.length}</b> notices (suggestions)</li>\n`),t.warn.length&&(a+=`<li><span style="font-family: monospace">⚠️</span> <b>${t.warn.length}</b> warnings (optional)</li>\n`),t.error.length&&(a+=`<li><span style="font-family: monospace">❌</span> <b>${t.error.length}</b> errors (required)</li>\n`),a+="</ul>\n",a+="\n",t.ok.length){a+="<h2>Passing</h2>\n",a+="<ol>\n";for(const[,e]of t.ok.entries())a+=`<li><code>${e.id}</code> ${e.name}</li>\n`;a+="</ol>\n",a+="\n"}if(t.notice.length){a+="<h2>Notices</h2>\n",a+="The server <i>MAY</i> support these, but are truly optional. These are suggestions following recommended conventions.\n",a+="<ol>\n";for(const[,e]of t.notice.entries())a+=await p(e);a+="</ol>\n",a+="\n"}if(t.warn.length){a+="<h2>Warnings</h2>\n",a+="The server <i>SHOULD</i> support these, but is not required.\n",a+="<ol>\n";for(const[,e]of t.warn.entries())a+=await p(e);a+="</ol>\n",a+="\n"}if(t.error.length){a+="<h2>Errors</h2>\n",a+="The server <b>MUST</b> support these.\n",a+="<ol>\n";for(const[,e]of t.error.entries())a+=await p(e);a+="</ol>\n"}return a},e.serverAudits=i}));
|
|
Binary file
|