graphql-http 1.4.0 → 1.6.1

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