graphql-http 1.17.0 → 1.18.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 +1 -0
- package/lib/audits/common.d.mts +1 -1
- package/lib/audits/common.d.ts +1 -1
- package/lib/audits/index.d.mts +3 -3
- package/lib/audits/render.d.mts +1 -1
- package/lib/audits/render.js +15 -0
- package/lib/audits/render.mjs +15 -0
- package/lib/audits/server.d.mts +1 -1
- package/lib/audits/server.js +61 -161
- package/lib/audits/server.mjs +61 -161
- package/lib/audits/utils.d.mts +2 -2
- package/lib/audits/utils.js +6 -3
- package/lib/audits/utils.mjs +6 -3
- package/lib/client.d.mts +2 -2
- package/lib/handler.d.mts +22 -5
- package/lib/handler.d.ts +20 -3
- package/lib/handler.js +36 -14
- package/lib/handler.mjs +37 -15
- package/lib/index.d.mts +4 -4
- package/lib/use/express.d.mts +2 -2
- package/lib/use/fastify.d.mts +2 -2
- package/lib/use/fetch.d.mts +2 -2
- package/lib/use/http.d.mts +2 -2
- package/lib/use/http2.d.mts +2 -2
- package/lib/use/koa.d.mts +2 -2
- package/lib/use/node.d.mts +3 -3
- 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 +16 -15
- package/umd/graphql-http-audits.js +82 -164
- 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
|
}),
|
|
@@ -245,7 +252,9 @@
|
|
|
245
252
|
headers: {
|
|
246
253
|
'content-type': 'application/json; charset=utf-8',
|
|
247
254
|
},
|
|
248
|
-
body: JSON.stringify({
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
query: '{ __type(name: "Run🏃Swim🏊") { name } }',
|
|
257
|
+
}),
|
|
249
258
|
});
|
|
250
259
|
ressert(res).status.toBe(200);
|
|
251
260
|
}),
|
|
@@ -287,7 +296,7 @@
|
|
|
287
296
|
ressert(res).status.toBeBetween(400, 499);
|
|
288
297
|
}),
|
|
289
298
|
// Request POST
|
|
290
|
-
audit('9ABE', '
|
|
299
|
+
audit('9ABE', 'MAY respond with 4xx status code if content-type is not supplied on POST requests', async () => {
|
|
291
300
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
292
301
|
method: 'POST',
|
|
293
302
|
});
|
|
@@ -301,23 +310,15 @@
|
|
|
301
310
|
});
|
|
302
311
|
ressert(res).status.toBe(200);
|
|
303
312
|
}),
|
|
304
|
-
audit('
|
|
305
|
-
var _a;
|
|
313
|
+
audit('A5BF', 'MAY use 400 status code when request body is missing on POST', async () => {
|
|
306
314
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
307
315
|
method: 'POST',
|
|
308
316
|
headers: { 'content-type': 'application/json' },
|
|
309
317
|
});
|
|
310
|
-
|
|
311
|
-
await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
ressert(res).status.toBe(400);
|
|
315
|
-
}
|
|
318
|
+
ressert(res).status.toBe(400);
|
|
316
319
|
}),
|
|
317
320
|
// Request Parameters
|
|
318
|
-
audit(
|
|
319
|
-
// TODO: convert to MUST after watershed
|
|
320
|
-
'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 () => {
|
|
321
322
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
322
323
|
method: 'POST',
|
|
323
324
|
headers: {
|
|
@@ -328,26 +329,11 @@
|
|
|
328
329
|
});
|
|
329
330
|
ressert(res).status.toBe(400);
|
|
330
331
|
}),
|
|
331
|
-
|
|
332
|
+
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`LKJ${index}`, `MAY use 400 status code on ${extendedTypeof(invalid)} {query} parameter`, async () => {
|
|
332
333
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
333
334
|
method: 'POST',
|
|
334
335
|
headers: {
|
|
335
336
|
'content-type': 'application/json',
|
|
336
|
-
accept: 'application/json',
|
|
337
|
-
},
|
|
338
|
-
body: JSON.stringify({ notquery: '{ __typename }' }),
|
|
339
|
-
});
|
|
340
|
-
ressert(res).status.toBe(200);
|
|
341
|
-
await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
|
|
342
|
-
}),
|
|
343
|
-
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`4F5${index}`,
|
|
344
|
-
// TODO: convert to MUST after watershed
|
|
345
|
-
`SHOULD use 400 status code on ${extendedTypeof(invalid)} {query} parameter when accepting application/graphql-response+json`, async () => {
|
|
346
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
347
|
-
method: 'POST',
|
|
348
|
-
headers: {
|
|
349
|
-
'content-type': 'application/json',
|
|
350
|
-
accept: 'application/graphql-response+json',
|
|
351
337
|
},
|
|
352
338
|
body: JSON.stringify({
|
|
353
339
|
query: invalid,
|
|
@@ -355,20 +341,6 @@
|
|
|
355
341
|
});
|
|
356
342
|
ressert(res).status.toBe(400);
|
|
357
343
|
})),
|
|
358
|
-
...[{ 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 () => {
|
|
359
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
360
|
-
method: 'POST',
|
|
361
|
-
headers: {
|
|
362
|
-
'content-type': 'application/json',
|
|
363
|
-
accept: 'application/json',
|
|
364
|
-
},
|
|
365
|
-
body: JSON.stringify({
|
|
366
|
-
query: invalid,
|
|
367
|
-
}),
|
|
368
|
-
});
|
|
369
|
-
ressert(res).status.toBe(200);
|
|
370
|
-
await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
|
|
371
|
-
})),
|
|
372
344
|
audit(
|
|
373
345
|
// TODO: convert to MUST after watershed
|
|
374
346
|
'34A2', 'SHOULD allow string {query} parameter when accepting application/graphql-response+json', async () => {
|
|
@@ -398,14 +370,11 @@
|
|
|
398
370
|
ressert(res).status.toBe(200);
|
|
399
371
|
await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
400
372
|
}),
|
|
401
|
-
...[{ obj: 'ect' }, 0, false, ['array']].map((invalid, index) => audit(`
|
|
402
|
-
// TODO: convert to MUST after watershed
|
|
403
|
-
`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 () => {
|
|
404
374
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
405
375
|
method: 'POST',
|
|
406
376
|
headers: {
|
|
407
377
|
'content-type': 'application/json',
|
|
408
|
-
accept: 'application/graphql-response+json',
|
|
409
378
|
},
|
|
410
379
|
body: JSON.stringify({
|
|
411
380
|
operationName: invalid,
|
|
@@ -414,21 +383,6 @@
|
|
|
414
383
|
});
|
|
415
384
|
ressert(res).status.toBe(400);
|
|
416
385
|
})),
|
|
417
|
-
...[{ 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 () => {
|
|
418
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
419
|
-
method: 'POST',
|
|
420
|
-
headers: {
|
|
421
|
-
'content-type': 'application/json',
|
|
422
|
-
accept: 'application/json',
|
|
423
|
-
},
|
|
424
|
-
body: JSON.stringify({
|
|
425
|
-
operationName: invalid,
|
|
426
|
-
query: '{ __typename }',
|
|
427
|
-
}),
|
|
428
|
-
});
|
|
429
|
-
ressert(res).status.toBe(200);
|
|
430
|
-
await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
|
|
431
|
-
})),
|
|
432
386
|
audit(
|
|
433
387
|
// TODO: convert to MUST after watershed
|
|
434
388
|
'8161', 'SHOULD allow string {operationName} parameter when accepting application/graphql-response+json', async () => {
|
|
@@ -494,14 +448,11 @@
|
|
|
494
448
|
await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
495
449
|
}),
|
|
496
450
|
]),
|
|
497
|
-
...['string', 0, false, ['array']].map((invalid, index) => audit(`
|
|
498
|
-
// TODO: convert to MUST after watershed
|
|
499
|
-
`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 () => {
|
|
500
452
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
501
453
|
method: 'POST',
|
|
502
454
|
headers: {
|
|
503
455
|
'content-type': 'application/json',
|
|
504
|
-
accept: 'application/graphql-response+json',
|
|
505
456
|
},
|
|
506
457
|
body: JSON.stringify({
|
|
507
458
|
query: '{ __typename }',
|
|
@@ -510,21 +461,6 @@
|
|
|
510
461
|
});
|
|
511
462
|
ressert(res).status.toBe(400);
|
|
512
463
|
})),
|
|
513
|
-
...['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 () => {
|
|
514
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
515
|
-
method: 'POST',
|
|
516
|
-
headers: {
|
|
517
|
-
'content-type': 'application/json',
|
|
518
|
-
accept: 'application/json',
|
|
519
|
-
},
|
|
520
|
-
body: JSON.stringify({
|
|
521
|
-
query: '{ __typename }',
|
|
522
|
-
variables: invalid,
|
|
523
|
-
}),
|
|
524
|
-
});
|
|
525
|
-
ressert(res).status.toBe(200);
|
|
526
|
-
await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
|
|
527
|
-
})),
|
|
528
464
|
audit(
|
|
529
465
|
// TODO: convert to MUST after watershed
|
|
530
466
|
'2EA1', 'SHOULD allow map {variables} parameter when accepting application/graphql-response+json', async () => {
|
|
@@ -581,14 +517,13 @@
|
|
|
581
517
|
ressert(res).status.toBe(200);
|
|
582
518
|
await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
583
519
|
}),
|
|
584
|
-
...['string', 0, false, ['array']].map((invalid, index) => audit(`
|
|
520
|
+
...['string', 0, false, ['array']].map((invalid, index) => audit(`58B${index}`,
|
|
585
521
|
// TODO: convert to MUST after watershed
|
|
586
|
-
`
|
|
522
|
+
`MAY use 400 status code on ${extendedTypeof(invalid)} {extensions} parameter`, async () => {
|
|
587
523
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
588
524
|
method: 'POST',
|
|
589
525
|
headers: {
|
|
590
526
|
'content-type': 'application/json',
|
|
591
|
-
accept: 'application/graphql-response+json',
|
|
592
527
|
},
|
|
593
528
|
body: JSON.stringify({
|
|
594
529
|
query: '{ __typename }',
|
|
@@ -597,21 +532,6 @@
|
|
|
597
532
|
});
|
|
598
533
|
ressert(res).status.toBe(400);
|
|
599
534
|
})),
|
|
600
|
-
...['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 () => {
|
|
601
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
602
|
-
method: 'POST',
|
|
603
|
-
headers: {
|
|
604
|
-
'content-type': 'application/json',
|
|
605
|
-
accept: 'application/json',
|
|
606
|
-
},
|
|
607
|
-
body: JSON.stringify({
|
|
608
|
-
query: '{ __typename }',
|
|
609
|
-
extensions: invalid,
|
|
610
|
-
}),
|
|
611
|
-
});
|
|
612
|
-
ressert(res).status.toBe(200);
|
|
613
|
-
await ressert(res).bodyAsExecutionResult.toHaveProperty('errors');
|
|
614
|
-
})),
|
|
615
535
|
audit(
|
|
616
536
|
// TODO: convert to MUST after watershed
|
|
617
537
|
'428F', 'SHOULD allow map {extensions} parameter when accepting application/graphql-response+json', async () => {
|
|
@@ -643,122 +563,91 @@
|
|
|
643
563
|
ressert(res).status.toBe(200);
|
|
644
564
|
await ressert(res).bodyAsExecutionResult.notToHaveProperty('errors');
|
|
645
565
|
}),
|
|
646
|
-
|
|
647
|
-
// Response application/json
|
|
648
|
-
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 () => {
|
|
649
567
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
650
568
|
method: 'POST',
|
|
651
569
|
headers: {
|
|
652
570
|
'content-type': 'application/json',
|
|
653
|
-
accept: 'application/json',
|
|
654
571
|
},
|
|
655
572
|
body: '{ "not a JSON',
|
|
656
573
|
});
|
|
657
|
-
ressert(res).status.
|
|
658
|
-
}),
|
|
659
|
-
audit('F5AF', 'SHOULD use 200 status code if parameters are invalid when accepting application/json', async () => {
|
|
660
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
661
|
-
method: 'POST',
|
|
662
|
-
headers: {
|
|
663
|
-
'content-type': 'application/json',
|
|
664
|
-
accept: 'application/json',
|
|
665
|
-
},
|
|
666
|
-
body: JSON.stringify({
|
|
667
|
-
qeury: /* typo */ '{ __typename }',
|
|
668
|
-
}),
|
|
669
|
-
});
|
|
670
|
-
ressert(res).status.toBe(200);
|
|
574
|
+
ressert(res).status.toBeBetween(400, 499);
|
|
671
575
|
}),
|
|
672
|
-
audit('
|
|
576
|
+
audit('BCF8', 'MAY use 400 status code on JSON parsing failure', async () => {
|
|
673
577
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
674
578
|
method: 'POST',
|
|
675
579
|
headers: {
|
|
676
580
|
'content-type': 'application/json',
|
|
677
|
-
accept: 'application/json',
|
|
678
581
|
},
|
|
679
|
-
body:
|
|
582
|
+
body: '{ "not a JSON',
|
|
680
583
|
});
|
|
681
|
-
ressert(res).status.toBe(
|
|
584
|
+
ressert(res).status.toBe(400);
|
|
682
585
|
}),
|
|
683
|
-
audit('
|
|
586
|
+
audit('8764', 'MAY use 4xx or 5xx status codes if parameters are invalid', async () => {
|
|
684
587
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
685
588
|
method: 'POST',
|
|
686
589
|
headers: {
|
|
687
590
|
'content-type': 'application/json',
|
|
688
|
-
accept: 'application/json',
|
|
689
591
|
},
|
|
690
592
|
body: JSON.stringify({
|
|
691
|
-
|
|
593
|
+
qeury /* typo */: '{ __typename }',
|
|
692
594
|
}),
|
|
693
595
|
});
|
|
694
|
-
ressert(res).status.
|
|
695
|
-
}),
|
|
696
|
-
// Response application/graphql-response+json
|
|
697
|
-
audit(
|
|
698
|
-
// TODO: convert to MUST after watershed
|
|
699
|
-
'60AA', 'SHOULD use 4xx or 5xx status codes on JSON parsing failure when accepting application/graphql-response+json', async () => {
|
|
700
|
-
const res = await fetchFn(await getUrl(opts.url), {
|
|
701
|
-
method: 'POST',
|
|
702
|
-
headers: {
|
|
703
|
-
'content-type': 'application/json',
|
|
704
|
-
accept: 'application/graphql-response+json',
|
|
705
|
-
},
|
|
706
|
-
body: '{ "not a JSON',
|
|
707
|
-
});
|
|
708
|
-
ressert(res).status.toBeBetween(400, 499);
|
|
596
|
+
ressert(res).status.toBeBetween(400, 599);
|
|
709
597
|
}),
|
|
710
|
-
audit('
|
|
598
|
+
audit('3E3A', 'MAY use 400 status code if parameters are invalid', async () => {
|
|
711
599
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
712
600
|
method: 'POST',
|
|
713
601
|
headers: {
|
|
714
602
|
'content-type': 'application/json',
|
|
715
|
-
accept: 'application/graphql-response+json',
|
|
716
603
|
},
|
|
717
|
-
body:
|
|
604
|
+
body: JSON.stringify({
|
|
605
|
+
qeury: /* typo */ '{ __typename }',
|
|
606
|
+
}),
|
|
718
607
|
});
|
|
719
608
|
ressert(res).status.toBe(400);
|
|
720
609
|
}),
|
|
721
|
-
audit(
|
|
722
|
-
//
|
|
723
|
-
'
|
|
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 () => {
|
|
724
613
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
725
614
|
method: 'POST',
|
|
726
615
|
headers: {
|
|
727
616
|
'content-type': 'application/json',
|
|
728
|
-
accept: 'application/
|
|
617
|
+
accept: 'application/json',
|
|
729
618
|
},
|
|
730
|
-
body: JSON.stringify({
|
|
731
|
-
qeury /* typo */: '{ __typename }',
|
|
732
|
-
}),
|
|
619
|
+
body: JSON.stringify({ query: '{' }),
|
|
733
620
|
});
|
|
734
|
-
ressert(res).status.
|
|
621
|
+
ressert(res).status.toBe(200);
|
|
735
622
|
}),
|
|
736
|
-
audit('
|
|
623
|
+
audit('FDE2', 'SHOULD use 200 status code on document validation failure when accepting application/json', async () => {
|
|
737
624
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
738
625
|
method: 'POST',
|
|
739
626
|
headers: {
|
|
740
627
|
'content-type': 'application/json',
|
|
741
|
-
accept: 'application/
|
|
628
|
+
accept: 'application/json',
|
|
742
629
|
},
|
|
743
630
|
body: JSON.stringify({
|
|
744
|
-
|
|
631
|
+
query: '{ 8f31403dfe404bccbb0e835f2629c6a7 }', // making sure the field doesnt exist
|
|
745
632
|
}),
|
|
746
633
|
});
|
|
747
|
-
ressert(res).status.toBe(
|
|
634
|
+
ressert(res).status.toBe(200);
|
|
748
635
|
}),
|
|
749
|
-
audit('
|
|
636
|
+
audit('7B9B', 'SHOULD use a status code of 200 on variable coercion failure when accepting application/json', async () => {
|
|
750
637
|
const res = await fetchFn(await getUrl(opts.url), {
|
|
751
638
|
method: 'POST',
|
|
752
639
|
headers: {
|
|
753
640
|
'content-type': 'application/json',
|
|
754
|
-
accept: 'application/
|
|
641
|
+
accept: 'application/json',
|
|
755
642
|
},
|
|
756
643
|
body: JSON.stringify({
|
|
757
|
-
|
|
644
|
+
query: 'query CoerceFailure($id: ID!){ __typename }',
|
|
645
|
+
variables: { id: null },
|
|
758
646
|
}),
|
|
759
647
|
});
|
|
760
|
-
|
|
648
|
+
ressert(res).status.toBe(200);
|
|
761
649
|
}),
|
|
650
|
+
// Response application/graphql-response+json
|
|
762
651
|
audit(
|
|
763
652
|
// TODO: convert to MUST after watershed
|
|
764
653
|
'865D', 'SHOULD use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json', async () => {
|
|
@@ -841,6 +730,20 @@
|
|
|
841
730
|
});
|
|
842
731
|
await ressert(res).bodyAsExecutionResult.data.toBe(undefined);
|
|
843
732
|
}),
|
|
733
|
+
audit('86EE', 'SHOULD use a status code of 400 on variable coercion failure when accepting application/graphql-response+json', async () => {
|
|
734
|
+
const res = await fetchFn(await getUrl(opts.url), {
|
|
735
|
+
method: 'POST',
|
|
736
|
+
headers: {
|
|
737
|
+
'content-type': 'application/json',
|
|
738
|
+
accept: 'application/graphql-response+json',
|
|
739
|
+
},
|
|
740
|
+
body: JSON.stringify({
|
|
741
|
+
query: 'query CoerceFailure($id: ID!){ __typename }',
|
|
742
|
+
variables: { id: null },
|
|
743
|
+
}),
|
|
744
|
+
});
|
|
745
|
+
ressert(res).status.toBe(400);
|
|
746
|
+
}),
|
|
844
747
|
// TODO: how to fail and have the data entry?
|
|
845
748
|
// audit('EE52', 'MUST use 2xx status code if response contains the data entry and it is not null when accepting application/graphql-response+json'),
|
|
846
749
|
// TODO: how to make an unauthorized request?
|
|
@@ -878,6 +781,7 @@
|
|
|
878
781
|
const grouped = {
|
|
879
782
|
total: 0,
|
|
880
783
|
ok: [],
|
|
784
|
+
notice: [],
|
|
881
785
|
warn: [],
|
|
882
786
|
error: [],
|
|
883
787
|
};
|
|
@@ -900,6 +804,9 @@
|
|
|
900
804
|
if (grouped.ok.length) {
|
|
901
805
|
report += `<li><span style="font-family: monospace">✅</span> <b>${grouped.ok.length}</b> pass</li>\n`;
|
|
902
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
|
+
}
|
|
903
810
|
if (grouped.warn.length) {
|
|
904
811
|
// TODO: warning sign is rendered as "⚠️" in markdown instead of the emoji
|
|
905
812
|
report += `<li><span style="font-family: monospace">⚠️</span> <b>${grouped.warn.length}</b> warnings (optional)</li>\n`;
|
|
@@ -918,6 +825,17 @@
|
|
|
918
825
|
report += '</ol>\n';
|
|
919
826
|
report += '\n';
|
|
920
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
|
+
}
|
|
921
839
|
if (grouped.warn.length) {
|
|
922
840
|
report += `<h2>Warnings</h2>\n`;
|
|
923
841
|
report += 'The server <i>SHOULD</i> support these, but is not required.\n';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("graphql")):"function"==typeof define&&define.amd?define(["exports","graphql"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).graphqlHttpAudits={})}(this,(function(t){"use strict";function e(t){return null===t?"null":Array.isArray(t)?"array":typeof t}function a(t,e,a){return{id:t,name:e,fn:async()=>{try{return await a(),{id:t,name:e,status:"ok"}}catch(a){if(!(a instanceof n))throw a;return{id:t,name:e,status:e.startsWith("MUST")?"error":"warn",reason:a.reason,response:a.response}}}}}class n{constructor(t,e){this.response=t,this.reason=e}}function o(t){return{status:{toBe(e){if(t.status!==e)throw new n(t,`Response status code is not ${e}`)},toBeBetween:(e,a)=>{if(!(e<=t.status&&t.status<=a))throw new n(t,`Response status is not between ${e} and ${a}`)}},header:e=>({toContain(a){var o;if(!(null===(o=t.headers.get(e))||void 0===o?void 0:o.includes(a)))throw new n(t,`Response header ${e} does not contain ${a}`)},notToContain(a){var o;if(null===(o=t.headers.get(e))||void 0===o?void 0:o.includes(a))throw new n(t,`Response header ${e} contains ${a}`)}}),bodyAsExecutionResult:{data:{async toBe(e){const a=t.clone();if((await s(t)).data!==e)throw new n(a,`Response body execution result data is not "${e}"`)}},async toHaveProperty(e){const a=t.clone();if(!(e in await s(t)))throw new n(a,`Response body execution result does not have a property "${e}"`)},async notToHaveProperty(e){const a=t.clone();if(e in await s(t))throw new n(a,`Response body execution result has a property "${e}"`)}}}}async function s(t){let e,a;try{const a=new TextDecoder("utf-8"),n=await t.arrayBuffer();e=a.decode(n)}catch(e){throw new n(t,"Response body is not UTF-8 encoded")}try{a=JSON.parse(e)}catch(e){throw new n(t,"Response body is not valid JSON")}return a}function i(t){const s=t.fetchFn||fetch;return[a("22EB","SHOULD accept application/graphql-response+json and match the content-type",(async()=>{const e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }"})});o(e).status.toBe(200),o(e).header("content-type").toContain("application/graphql-response+json")})),a("4655","MUST accept application/json and match the content-type",(async()=>{const e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(e).status.toBe(200),o(e).header("content-type").toContain("application/json")})),a("47DE","SHOULD accept */* and use application/json for the content-type",(async()=>{const e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"*/*"},body:JSON.stringify({query:"{ __typename }"})});o(e).status.toBe(200),o(e).header("content-type").toContain("application/json")})),a("80D8","SHOULD assume application/json content-type when accept is missing",(async()=>{const e=new URL(await r(t.url));e.searchParams.set("query","{ __typename }");const a=await s(e.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 e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(e).status.toBe(200);try{new TextDecoder("utf-8").decode(await e.arrayBuffer())}catch(t){throw new n(e,"Response body is not UTF-8 encoded")}})),a("BF61","MUST accept utf-8 encoded request",(async()=>{o(await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json; charset=utf-8"},body:JSON.stringify({query:"{ __typename }"})})).status.toBe(200)})),a("78D5","MUST assume utf-8 in request if encoding is unspecified",(async()=>{o(await s(await r(t.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(t.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 e=new URL(await r(t.url));e.searchParams.set("query","{ __typename }");o(await s(e.toString())).status.toBe(200)})),a("9C48","MAY NOT allow executing mutations on GET requests",(async()=>{const e=new URL(await r(t.url));e.searchParams.set("query","mutation { __typename }");o(await s(e.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(t.url),{method:"POST"})).status.toBeBetween(400,499)})),a("03D4","MUST accept application/json POST requests",(async()=>{o(await s(await r(t.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 e;const a=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json"}});(null===(e=a.headers.get("content-type"))||void 0===e?void 0:e.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(t.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 e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({notquery:"{ __typename }"})});o(e).status.toBe(200),await o(e).bodyAsExecutionResult.toHaveProperty("errors")})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`4F5${i}`,`SHOULD use 400 status code on ${e(n)} {query} parameter when accepting application/graphql-response+json`,(async()=>{o(await s(await r(t.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 ${e(n)} {query} parameter when accepting application/json`,(async()=>{const e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:n})});o(e).status.toBe(200),await o(e).bodyAsExecutionResult.toHaveProperty("errors")})))),a("34A2","SHOULD allow string {query} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(t.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 e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }"})});o(e).status.toBe(200),await o(e).bodyAsExecutionResult.notToHaveProperty("errors")})),...[{obj:"ect"},0,!1,["array"]].map(((n,i)=>a(`E3E${i}`,`SHOULD use 400 status code on ${e(n)} {operationName} parameter when accepting application/graphql-response+json`,(async()=>{o(await s(await r(t.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 ${e(n)} {operationName} parameter when accepting application/json`,(async()=>{const e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({operationName:n,query:"{ __typename }"})});o(e).status.toBe(200),await o(e).bodyAsExecutionResult.toHaveProperty("errors")})))),a("8161","SHOULD allow string {operationName} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(t.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 e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({operationName:"Query",query:"query Query { __typename }"})});o(e).status.toBe(200),await o(e).bodyAsExecutionResult.notToHaveProperty("errors")})),...["variables","operationName","extensions"].flatMap(((e,n)=>[a(`94B${n}`,`SHOULD allow null {${e}} parameter when accepting application/graphql-response+json`,(async()=>{const a=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ __typename }",[e]:null})});o(a).status.toBe(200),await o(a).bodyAsExecutionResult.notToHaveProperty("errors")})),a(`022${n}`,`MUST allow null {${e}} parameter when accepting application/json`,(async()=>{const a=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",[e]: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 ${e(n)} {variables} parameter when accepting application/graphql-response+json`,(async()=>{o(await s(await r(t.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 ${e(n)} {variables} parameter when accepting application/json`,(async()=>{const e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",variables:n})});o(e).status.toBe(200),await o(e).bodyAsExecutionResult.toHaveProperty("errors")})))),a("2EA1","SHOULD allow map {variables} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(t.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 e=await s(await r(t.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(e).status.toBe(200),await o(e).bodyAsExecutionResult.notToHaveProperty("errors")})),a("D6D5","MAY allow URL-encoded JSON string {variables} parameter in GETs when accepting application/graphql-response+json",(async()=>{const e=new URL(await r(t.url));e.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),e.searchParams.set("variables",JSON.stringify({name:"sometype"}));o(await s(e.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 e=new URL(await r(t.url));e.searchParams.set("query","query Type($name: String!) { __type(name: $name) { name } }"),e.searchParams.set("variables",JSON.stringify({name:"sometype"}));const a=await s(e.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 ${e(n)} {extensions} parameter when accepting application/graphql-response+json`,(async()=>{o(await s(await r(t.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 ${e(n)} {extensions} parameter when accepting application/json`,(async()=>{const e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:n})});o(e).status.toBe(200),await o(e).bodyAsExecutionResult.toHaveProperty("errors")})))),a("428F","SHOULD allow map {extensions} parameter when accepting application/graphql-response+json",(async()=>{o(await s(await r(t.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 e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ __typename }",extensions:{some:"value"}})});o(e).status.toBe(200),await o(e).bodyAsExecutionResult.notToHaveProperty("errors")})),a("D477","SHOULD use 200 status code on JSON parsing failure when accepting application/json",(async()=>{o(await s(await r(t.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(t.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(t.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(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})})).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(t.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(t.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(t.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(t.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 e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({qeury:"{ __typename }"})});await o(e).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(t.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(t.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 e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{"})});await o(e).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(t.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(t.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 e=await s(await r(t.url),{method:"POST",headers:{"content-type":"application/json",accept:"application/graphql-response+json"},body:JSON.stringify({query:"{ 8f31403dfe404bccbb0e835f2629c6a7 }"})});await o(e).bodyAsExecutionResult.data.toBe(void 0)}))]}async function r(t){return"function"==typeof t?await t():t}async function p(t){let e="";e+=`<li><code>${t.id}</code> ${t.name}\n`,e+="<details>\n",e+=`<summary>${function(t,e=1024){if(t.length>e)return t.substring(0,e)+"...";return t}(t.reason)}</summary>\n`,e+='<pre><code class="lang-json">';const a=t.response,n={};for(const[t,e]of a.headers.entries())"date"===t?n[t]="<timestamp>":["cf-ray","server-timing"].includes(t)?n[t]="<omitted>":n[t]=e;let o,s="";try{s=await a.text(),o=JSON.parse(s)}catch(t){}return e+=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},((t,e)=>{if(null!=e&&"object"==typeof e&&!Array.isArray(e)){const t={};return Object.keys(e).sort().reverse().reduce(((t,a)=>(t[a]=e[a],t)),t)}return e}),2)+"\n",e+="</code></pre>\n",e+="</details>\n",e+="</li>\n",e}t.auditServer=async function(t){const e=i(t);return await Promise.all(e.map((({fn:t})=>t())))},t.renderAuditResultsToHTML=async function(t){const e={total:0,ok:[],warn:[],error:[]};for(const a of t)e.total++,a.status,e[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>${e.total}</b> audits in total</li>\n`,e.ok.length&&(a+=`<li><span style="font-family: monospace">✅</span> <b>${e.ok.length}</b> pass</li>\n`),e.warn.length&&(a+=`<li><span style="font-family: monospace">⚠️</span> <b>${e.warn.length}</b> warnings (optional)</li>\n`),e.error.length&&(a+=`<li><span style="font-family: monospace">❌</span> <b>${e.error.length}</b> errors (required)</li>\n`),a+="</ul>\n",a+="\n",e.ok.length){a+="<h2>Passing</h2>\n",a+="<ol>\n";for(const[,t]of e.ok.entries())a+=`<li><code>${t.id}</code> ${t.name}</li>\n`;a+="</ol>\n",a+="\n"}if(e.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[,t]of e.warn.entries())a+=await p(t);a+="</ol>\n",a+="\n"}if(e.error.length){a+="<h2>Errors</h2>\n",a+="The server <b>MUST</b> support these.\n",a+="<ol>\n";for(const[,t]of e.error.entries())a+=await p(t);a+="</ol>\n"}return a},t.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
|