graphql-http 1.2.0 → 1.5.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 CHANGED
@@ -68,11 +68,12 @@ const server = http.createServer(async (req, res) => {
68
68
  url: req.url,
69
69
  method: req.method,
70
70
  headers: req.headers,
71
- body: await new Promise((resolve) => {
72
- let body = '';
73
- req.on('data', (chunk) => (body += chunk));
74
- req.on('end', () => resolve(body));
75
- }),
71
+ body: () =>
72
+ new Promise((resolve) => {
73
+ let body = '';
74
+ req.on('data', (chunk) => (body += chunk));
75
+ req.on('end', () => resolve(body));
76
+ }),
76
77
  raw: req,
77
78
  });
78
79
  res.writeHead(init.status, init.statusText, init.headers).end(body);
@@ -119,11 +120,12 @@ const server = http2.createSecureServer(
119
120
  url: req.url,
120
121
  method: req.method,
121
122
  headers: req.headers,
122
- body: await new Promise((resolve) => {
123
- let body = '';
124
- req.on('data', (chunk) => (body += chunk));
125
- req.on('end', () => resolve(body));
126
- }),
123
+ body: () =>
124
+ new Promise((resolve) => {
125
+ let body = '';
126
+ req.on('data', (chunk) => (body += chunk));
127
+ req.on('end', () => resolve(body));
128
+ }),
127
129
  raw: req,
128
130
  });
129
131
  res.writeHead(init.status, init.statusText, init.headers).end(body);
@@ -155,11 +157,12 @@ app.use('/graphql', async (req, res) => {
155
157
  url: req.url,
156
158
  method: req.method,
157
159
  headers: req.headers,
158
- body: await new Promise((resolve) => {
159
- let body = '';
160
- req.on('data', (chunk) => (body += chunk));
161
- req.on('end', () => resolve(body));
162
- }),
160
+ body: () =>
161
+ new Promise((resolve) => {
162
+ let body = '';
163
+ req.on('data', (chunk) => (body += chunk));
164
+ req.on('end', () => resolve(body));
165
+ }),
163
166
  raw: req,
164
167
  });
165
168
  res.writeHead(init.status, init.statusText, init.headers).end(body);
@@ -190,11 +193,7 @@ fastify.all('/graphql', async (req, res) => {
190
193
  url: req.url,
191
194
  method: req.method,
192
195
  headers: req.headers,
193
- body: await new Promise((resolve) => {
194
- let body = '';
195
- req.on('data', (chunk) => (body += chunk));
196
- req.on('end', () => resolve(body));
197
- }),
196
+ body: req.body, // fastify reads the body for you
198
197
  raw: req,
199
198
  });
200
199
  res.writeHead(init.status, init.statusText, init.headers).end(body);
@@ -231,7 +230,7 @@ await serve(
231
230
  url: req.url,
232
231
  method: req.method,
233
232
  headers,
234
- body: await req.text(),
233
+ body: () => req.text(),
235
234
  raw: req,
236
235
  });
237
236
  return new Response(body, init);
@@ -690,11 +689,12 @@ const server = http.createServer(async (req, res) => {
690
689
  url: req.url,
691
690
  method: req.method,
692
691
  headers: req.headers,
693
- body: await new Promise((resolve) => {
694
- let body = '';
695
- req.on('data', (chunk) => (body += chunk));
696
- req.on('end', () => resolve(body));
697
- }),
692
+ body: () =>
693
+ new Promise((resolve) => {
694
+ let body = '';
695
+ req.on('data', (chunk) => (body += chunk));
696
+ req.on('end', () => resolve(body));
697
+ }),
698
698
  raw: req,
699
699
  });
700
700
  res.writeHead(init.status, init.statusText, init.headers).end(body);
@@ -709,6 +709,32 @@ console.log('Listening to port 4000');
709
709
 
710
710
  </details>
711
711
 
712
+ <details id="audit-jest">
713
+ <summary><a href="#audit-jest">🔗</a> Audit for servers usage in <a href="https://jestjs.io">Jest</a> environment</summary>
714
+
715
+ ```js
716
+ import { fetch } from '@whatwg-node/fetch';
717
+ import { serverAudits } from 'graphql-http';
718
+
719
+ for (const audit of serverAudits({
720
+ url: 'http://localhost:4000/graphql',
721
+ fetchFn: fetch,
722
+ })) {
723
+ test(audit.name, async () => {
724
+ const result = await audit.fn();
725
+ if (result.status === 'error') {
726
+ throw result.reason;
727
+ }
728
+ if (result.status === 'warn') {
729
+ console.warn(result.reason); // or throw if you want full compliance (warnings are not requirements)
730
+ }
731
+ // result.status === 'ok'
732
+ });
733
+ }
734
+ ```
735
+
736
+ </details>
737
+
712
738
  ## [Documentation](docs/)
713
739
 
714
740
  Check the [docs folder](docs/) out for [TypeDoc](https://typedoc.org) generated documentation.
@@ -0,0 +1,58 @@
1
+ /**
2
+ *
3
+ * audit/common
4
+ *
5
+ */
6
+ /**
7
+ * Audit requirement levels as per [RFC2119](https://www.rfc-editor.org/rfc/rfc2119).
8
+ *
9
+ * @category Audits
10
+ */
11
+ export declare type AuditRequirement = 'MUST' | 'SHOULD' | 'MAY';
12
+ /**
13
+ * Audit name starting with the audit requirement level.
14
+ *
15
+ * @category Audits
16
+ */
17
+ export declare type AuditName = `${AuditRequirement} ${string}`;
18
+ /**
19
+ * Actual audit test returning an result.
20
+ *
21
+ * The test function will throw only if the error is fatal.
22
+ *
23
+ * @category Audits
24
+ */
25
+ export interface Audit {
26
+ name: AuditName;
27
+ fn: () => Promise<AuditResult>;
28
+ }
29
+ /**
30
+ * Indicates that the audit was successful.
31
+ *
32
+ * @category Audits
33
+ */
34
+ export interface AuditOk {
35
+ name: AuditName;
36
+ status: 'ok';
37
+ }
38
+ /**
39
+ * Indicates that the audit failed.
40
+ *
41
+ * If the status is `warn`, the audit is not a requirement but rather a recommendation.
42
+ *
43
+ * On the other hand, if the status is `error`, the audit is a requirement and the source
44
+ * is therefore not compliant.
45
+ *
46
+ * @category Audits
47
+ */
48
+ export interface AuditFail {
49
+ name: AuditName;
50
+ status: 'warn' | 'error';
51
+ reason: string;
52
+ }
53
+ /**
54
+ * Result of the performed audit. See `AuditOk` and `AuditFail` for more information.
55
+ *
56
+ * @category Audits
57
+ */
58
+ export declare type AuditResult = AuditOk | AuditFail;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /**
3
+ *
4
+ * audit/common
5
+ *
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ /**
2
+ *
3
+ * audit/common
4
+ *
5
+ */
6
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from './common';
2
+ export * from './server';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./common"), exports);
18
+ __exportStar(require("./server"), exports);
@@ -0,0 +1,2 @@
1
+ export * from './common.mjs';
2
+ export * from './server.mjs';
@@ -0,0 +1,39 @@
1
+ /**
2
+ *
3
+ * audit/server
4
+ *
5
+ */
6
+ import { Audit, AuditResult } from './common';
7
+ /**
8
+ * Options for server audits required to check GraphQL over HTTP spec conformance.
9
+ *
10
+ * @category Audits
11
+ */
12
+ export interface ServerAuditOptions {
13
+ /**
14
+ * The URL of the GraphQL server for the audit.
15
+ */
16
+ url: string;
17
+ /**
18
+ * The Fetch function to use.
19
+ *
20
+ * For NodeJS environments consider using [`@whatwg-node/fetch`](https://github.com/ardatan/whatwg-node/tree/master/packages/fetch).
21
+ *
22
+ * @default global.fetch
23
+ */
24
+ fetchFn?: unknown;
25
+ }
26
+ /**
27
+ * List of server audits required to check GraphQL over HTTP spec conformance.
28
+ *
29
+ * @category Audits
30
+ */
31
+ export declare function serverAudits(opts: ServerAuditOptions): Audit[];
32
+ /**
33
+ * Performs the full list of server audits required for GraphQL over HTTP spec conformance.
34
+ *
35
+ * Please consult the `AuditResult` for more information.
36
+ *
37
+ * @category Audits
38
+ */
39
+ export declare function auditServer(opts: ServerAuditOptions): Promise<AuditResult[]>;
@@ -0,0 +1,400 @@
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)('MUST accept application/graphql-response+json and match the content-type', async () => {
20
+ const url = new URL(opts.url);
21
+ url.searchParams.set('query', '{ __typename }');
22
+ const res = await fetchFn(url.toString(), {
23
+ headers: {
24
+ accept: 'application/graphql-response+json',
25
+ },
26
+ });
27
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
28
+ (0, utils_1.assert)('Content-Type header', res.headers.get('content-type')).toContain('application/graphql-response+json');
29
+ }),
30
+ (0, utils_1.audit)('MUST accept application/json and match the content-type', async () => {
31
+ const url = new URL(opts.url);
32
+ url.searchParams.set('query', '{ __typename }');
33
+ const res = await fetchFn(url.toString(), {
34
+ headers: {
35
+ accept: 'application/json',
36
+ },
37
+ });
38
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
39
+ (0, utils_1.assert)('Content-Type header', res.headers.get('content-type')).toContain('application/json');
40
+ }),
41
+ (0, utils_1.audit)('MUST accept */* and use application/graphql-response+json for the content-type', async () => {
42
+ const url = new URL(opts.url);
43
+ url.searchParams.set('query', '{ __typename }');
44
+ const res = await fetchFn(url.toString(), {
45
+ headers: {
46
+ accept: '*/*',
47
+ },
48
+ });
49
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
50
+ (0, utils_1.assert)('Content-Type header', res.headers.get('content-type')).toContain('application/graphql-response+json');
51
+ }),
52
+ (0, utils_1.audit)('MUST assume application/graphql-response+json content-type when accept is missing', async () => {
53
+ const url = new URL(opts.url);
54
+ url.searchParams.set('query', '{ __typename }');
55
+ const res = await fetchFn(url.toString());
56
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
57
+ (0, utils_1.assert)('Content-Type header', res.headers.get('content-type')).toContain('application/graphql-response+json');
58
+ }),
59
+ (0, utils_1.audit)('MUST use utf-8 charset in response', async () => {
60
+ const url = new URL(opts.url);
61
+ url.searchParams.set('query', '{ __typename }');
62
+ const res = await fetchFn(url.toString());
63
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
64
+ (0, utils_1.assert)('Content-Type header', res.headers.get('content-type')).toContain('charset=utf-8');
65
+ }),
66
+ (0, utils_1.audit)('MUST accept only utf-8 charset', async () => {
67
+ const url = new URL(opts.url);
68
+ url.searchParams.set('query', '{ __typename }');
69
+ const res = await fetchFn(url.toString(), {
70
+ headers: {
71
+ accept: 'application/graphql-response+json; charset=iso-8859-1',
72
+ },
73
+ });
74
+ (0, utils_1.assert)('Status code', res.status).toBe(406);
75
+ (0, utils_1.assert)('Accept header', res.headers.get('accept')).toContain('charset=utf-8');
76
+ }),
77
+ // Request
78
+ (0, utils_1.audit)('MUST accept POST requests', async () => {
79
+ const res = await fetchFn(opts.url, {
80
+ method: 'POST',
81
+ headers: { 'content-type': 'application/json' },
82
+ body: JSON.stringify({ query: '{ __typename }' }),
83
+ });
84
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
85
+ }),
86
+ (0, utils_1.audit)('MAY accept application/x-www-form-urlencoded formatted GET requests', async () => {
87
+ const url = new URL(opts.url);
88
+ url.searchParams.set('query', '{ __typename }');
89
+ const res = await fetchFn(url.toString());
90
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
91
+ }),
92
+ // Request GET
93
+ (0, utils_1.audit)('MUST NOT allow executing mutations on GET requests', async () => {
94
+ const url = new URL(opts.url);
95
+ url.searchParams.set('query', 'mutation { __typename }');
96
+ const res = await fetchFn(url.toString(), {
97
+ headers: {
98
+ accept: 'application/graphql-response+json',
99
+ },
100
+ });
101
+ (0, utils_1.assert)('Status code', res.status).toBe(405);
102
+ }),
103
+ // Request POST
104
+ (0, utils_1.audit)('SHOULD respond with 4xx status code if content-type is not supplied on POST requests', async () => {
105
+ const res = await fetchFn(opts.url, {
106
+ method: 'POST',
107
+ });
108
+ (0, utils_1.assert)('Status code', res.status).toBeGreaterThanOrEqual(400);
109
+ (0, utils_1.assert)('Status code', res.status).toBeLessThanOrEqual(599);
110
+ }),
111
+ (0, utils_1.audit)('MUST accept application/json POST requests', async () => {
112
+ const res = await fetchFn(opts.url, {
113
+ method: 'POST',
114
+ headers: { 'content-type': 'application/json' },
115
+ body: JSON.stringify({ query: '{ __typename }' }),
116
+ });
117
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
118
+ }),
119
+ (0, utils_1.audit)('MUST require a request body on POST', async () => {
120
+ const res = await fetchFn(opts.url, {
121
+ method: 'POST',
122
+ headers: { 'content-type': 'application/json' },
123
+ });
124
+ (0, utils_1.assert)('Status code', res.status).toBe(400);
125
+ }),
126
+ // Request Parameters
127
+ (0, utils_1.audit)('MUST require the {query} parameter', async () => {
128
+ const res = await fetchFn(opts.url, {
129
+ method: 'POST',
130
+ headers: {
131
+ 'content-type': 'application/json',
132
+ accept: 'application/graphql-response+json',
133
+ },
134
+ body: JSON.stringify({ notquery: '{ __typename }' }),
135
+ });
136
+ (0, utils_1.assert)('Status code', res.status).toBe(400);
137
+ }),
138
+ ...[{ obj: 'ect' }, 0, false, ['array']].map((invalid) => (0, utils_1.audit)(`MUST NOT allow ${(0, utils_1.extendedTypeof)(invalid)} for the {query} parameter`, async () => {
139
+ const res = await fetchFn(opts.url, {
140
+ method: 'POST',
141
+ headers: {
142
+ 'content-type': 'application/json',
143
+ accept: 'application/graphql-response+json',
144
+ },
145
+ body: JSON.stringify({
146
+ query: invalid,
147
+ }),
148
+ });
149
+ (0, utils_1.assert)('Status code', res.status).toBe(400);
150
+ })),
151
+ (0, utils_1.audit)('MUST accept a string for the {query} parameter', async () => {
152
+ const res = await fetchFn(opts.url, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'content-type': 'application/json',
156
+ accept: 'application/graphql-response+json',
157
+ },
158
+ body: JSON.stringify({
159
+ query: '{ __typename }',
160
+ }),
161
+ });
162
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
163
+ }),
164
+ ...[{ obj: 'ect' }, 0, false, ['array']].map((invalid) => (0, utils_1.audit)(`MUST NOT allow ${(0, utils_1.extendedTypeof)(invalid)} for the {operationName} parameter`, async () => {
165
+ const res = await fetchFn(opts.url, {
166
+ method: 'POST',
167
+ headers: {
168
+ 'content-type': 'application/json',
169
+ accept: 'application/graphql-response+json',
170
+ },
171
+ body: JSON.stringify({
172
+ operationName: invalid,
173
+ query: '{ __typename }',
174
+ }),
175
+ });
176
+ (0, utils_1.assert)('Status code', res.status).toBe(400);
177
+ })),
178
+ (0, utils_1.audit)('MUST accept a string for the {operationName} parameter', async () => {
179
+ const res = await fetchFn(opts.url, {
180
+ method: 'POST',
181
+ headers: {
182
+ 'content-type': 'application/json',
183
+ accept: 'application/graphql-response+json',
184
+ },
185
+ body: JSON.stringify({
186
+ operationName: 'Query',
187
+ query: 'query Query { __typename }',
188
+ }),
189
+ });
190
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
191
+ }),
192
+ ...['string', 0, false, ['array']].map((invalid) => (0, utils_1.audit)(`MUST NOT allow ${(0, utils_1.extendedTypeof)(invalid)} for the {variables} parameter`, async () => {
193
+ const res = await fetchFn(opts.url, {
194
+ method: 'POST',
195
+ headers: {
196
+ 'content-type': 'application/json',
197
+ accept: 'application/graphql-response+json',
198
+ },
199
+ body: JSON.stringify({
200
+ query: '{ __typename }',
201
+ variables: invalid,
202
+ }),
203
+ });
204
+ (0, utils_1.assert)('Status code', res.status).toBe(400);
205
+ })),
206
+ (0, utils_1.audit)('MUST accept a map for the {variables} parameter', async () => {
207
+ const res = await fetchFn(opts.url, {
208
+ method: 'POST',
209
+ headers: {
210
+ 'content-type': 'application/json',
211
+ accept: 'application/graphql-response+json',
212
+ },
213
+ body: JSON.stringify({
214
+ query: 'query Type($name: String!) { __type(name: $name) { name } }',
215
+ variables: { name: 'sometype' },
216
+ }),
217
+ });
218
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
219
+ const result = (await res.json());
220
+ (0, utils_1.assert)('Execution result', result).notToHaveProperty('errors');
221
+ }),
222
+ (0, utils_1.audit)('MUST accept a URL-encoded JSON string for the {variables} parameter in GETs', async () => {
223
+ const url = new URL(opts.url);
224
+ url.searchParams.set('query', 'query Type($name: String!) { __type(name: $name) { name } }');
225
+ url.searchParams.set('variables', JSON.stringify({ name: 'sometype' }));
226
+ const res = await fetchFn(url.toString(), {
227
+ method: 'GET',
228
+ });
229
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
230
+ const result = (await res.json());
231
+ (0, utils_1.assert)('Execution result', result).notToHaveProperty('errors');
232
+ }),
233
+ ...['string', 0, false, ['array']].map((invalid) => (0, utils_1.audit)(`MUST NOT allow ${(0, utils_1.extendedTypeof)(invalid)} for the {extensions} parameter`, async () => {
234
+ const res = await fetchFn(opts.url, {
235
+ method: 'POST',
236
+ headers: {
237
+ 'content-type': 'application/json',
238
+ accept: 'application/graphql-response+json',
239
+ },
240
+ body: JSON.stringify({
241
+ query: '{ __typename }',
242
+ extensions: invalid,
243
+ }),
244
+ });
245
+ (0, utils_1.assert)('Status code', res.status).toBe(400);
246
+ })),
247
+ (0, utils_1.audit)('MUST accept a map for the {extensions} parameter', async () => {
248
+ const res = await fetchFn(opts.url, {
249
+ method: 'POST',
250
+ headers: {
251
+ 'content-type': 'application/json',
252
+ accept: 'application/graphql-response+json',
253
+ },
254
+ body: JSON.stringify({
255
+ query: '{ __typename }',
256
+ extensions: { some: 'value' },
257
+ }),
258
+ });
259
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
260
+ }),
261
+ // TODO: audit('MUST accept a map for the {extensions} parameter'),
262
+ // Response application/json
263
+ (0, utils_1.audit)('SHOULD use 200 status code on JSON parsing failure when accepting application/json', async () => {
264
+ const res = await fetchFn(opts.url, {
265
+ method: 'POST',
266
+ headers: {
267
+ 'content-type': 'application/json',
268
+ accept: 'application/json',
269
+ },
270
+ body: '{ "not a JSON',
271
+ });
272
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
273
+ }),
274
+ (0, utils_1.audit)('SHOULD use 200 status code if parameters are invalid when accepting application/json', async () => {
275
+ const url = new URL(opts.url);
276
+ url.searchParams.set('qeury' /* typo */, '{ __typename }');
277
+ const res = await fetchFn(url.toString(), {
278
+ method: 'GET',
279
+ headers: { accept: 'application/json' },
280
+ });
281
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
282
+ }),
283
+ (0, utils_1.audit)('SHOULD use 200 status code on document parsing failure when accepting application/json', async () => {
284
+ const url = new URL(opts.url);
285
+ url.searchParams.set('query', '{');
286
+ const res = await fetchFn(url.toString(), {
287
+ method: 'GET',
288
+ headers: { accept: 'application/json' },
289
+ });
290
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
291
+ }),
292
+ (0, utils_1.audit)('SHOULD use 200 status code on document validation failure when accepting application/json', async () => {
293
+ const url = new URL(opts.url);
294
+ url.searchParams.set('query', '{ 8f31403dfe404bccbb0e835f2629c6a7 }'); // making sure the field doesnt exist
295
+ const res = await fetchFn(url.toString(), {
296
+ method: 'GET',
297
+ headers: { accept: 'application/json' },
298
+ });
299
+ (0, utils_1.assert)('Status code', res.status).toBe(200);
300
+ }),
301
+ // Response application/graphql-response+json
302
+ (0, utils_1.audit)('MUST use 4xx or 5xx status codes on JSON parsing failure when accepting application/graphql-response+json', async () => {
303
+ const res = await fetchFn(opts.url, {
304
+ method: 'POST',
305
+ headers: {
306
+ 'content-type': 'application/json',
307
+ accept: 'application/graphql-response+json',
308
+ },
309
+ body: '{ "not a JSON',
310
+ });
311
+ (0, utils_1.assert)('Status code', res.status).toBeGreaterThanOrEqual(400);
312
+ (0, utils_1.assert)('Status code', res.status).toBeLessThanOrEqual(599);
313
+ }),
314
+ (0, utils_1.audit)('SHOULD use 400 status code on JSON parsing failure when accepting application/graphql-response+json', async () => {
315
+ const res = await fetchFn(opts.url, {
316
+ method: 'POST',
317
+ headers: {
318
+ 'content-type': 'application/json',
319
+ accept: 'application/graphql-response+json',
320
+ },
321
+ body: '{ "not a JSON',
322
+ });
323
+ (0, utils_1.assert)('Status code', res.status).toBe(400);
324
+ }),
325
+ (0, utils_1.audit)('MUST use 4xx or 5xx status codes if parameters are invalid when accepting application/graphql-response+json', async () => {
326
+ const url = new URL(opts.url);
327
+ url.searchParams.set('qeury' /* typo */, '{ __typename }');
328
+ const res = await fetchFn(url.toString(), {
329
+ method: 'GET',
330
+ headers: { accept: 'application/graphql-response+json' },
331
+ });
332
+ (0, utils_1.assert)('Status code', res.status).toBeGreaterThanOrEqual(400);
333
+ (0, utils_1.assert)('Status code', res.status).toBeLessThanOrEqual(599);
334
+ }),
335
+ (0, utils_1.audit)('SHOULD use 400 status code if parameters are invalid when accepting application/graphql-response+json', async () => {
336
+ const url = new URL(opts.url);
337
+ url.searchParams.set('qeury' /* typo */, '{ __typename }');
338
+ const res = await fetchFn(url.toString(), {
339
+ method: 'GET',
340
+ headers: { accept: 'application/graphql-response+json' },
341
+ });
342
+ (0, utils_1.assert)('Status code', res.status).toBe(400);
343
+ }),
344
+ (0, utils_1.audit)('MUST use 4xx or 5xx status codes on document parsing failure when accepting application/graphql-response+json', async () => {
345
+ const url = new URL(opts.url);
346
+ url.searchParams.set('query', '{');
347
+ const res = await fetchFn(url.toString(), {
348
+ method: 'GET',
349
+ headers: { accept: 'application/graphql-response+json' },
350
+ });
351
+ (0, utils_1.assert)('Status code', res.status).toBeGreaterThanOrEqual(400);
352
+ (0, utils_1.assert)('Status code', res.status).toBeLessThanOrEqual(599);
353
+ }),
354
+ (0, utils_1.audit)('SHOULD use 400 status code on document parsing failure when accepting application/graphql-response+json', async () => {
355
+ const url = new URL(opts.url);
356
+ url.searchParams.set('query', '{');
357
+ const res = await fetchFn(url.toString(), {
358
+ method: 'GET',
359
+ headers: { accept: 'application/graphql-response+json' },
360
+ });
361
+ (0, utils_1.assert)('Status code', res.status).toBe(400);
362
+ }),
363
+ (0, utils_1.audit)('MUST use 4xx or 5xx status codes on document validation failure when accepting application/graphql-response+json', async () => {
364
+ const url = new URL(opts.url);
365
+ url.searchParams.set('query', '{ 8f31403dfe404bccbb0e835f2629c6a7 }'); // making sure the field doesnt exist
366
+ const res = await fetchFn(url.toString(), {
367
+ method: 'GET',
368
+ headers: { accept: 'application/graphql-response+json' },
369
+ });
370
+ (0, utils_1.assert)('Status code', res.status).toBeGreaterThanOrEqual(400);
371
+ (0, utils_1.assert)('Status code', res.status).toBeLessThanOrEqual(599);
372
+ }),
373
+ (0, utils_1.audit)('SHOULD use 400 status code on document validation failure when accepting application/graphql-response+json', async () => {
374
+ const url = new URL(opts.url);
375
+ url.searchParams.set('query', '{ 8f31403dfe404bccbb0e835f2629c6a7 }'); // making sure the field doesnt exist
376
+ const res = await fetchFn(url.toString(), {
377
+ method: 'GET',
378
+ headers: { accept: 'application/graphql-response+json' },
379
+ });
380
+ (0, utils_1.assert)('Status code', res.status).toBe(400);
381
+ }),
382
+ // TODO: how to make an unauthorized request?
383
+ // https://graphql.github.io/graphql-over-http/draft/#sel-EANNNDTAAEVBAAqqc
384
+ // audit('SHOULD use 401 or 403 status codes when the request is not permitted')
385
+ ];
386
+ }
387
+ exports.serverAudits = serverAudits;
388
+ /**
389
+ * Performs the full list of server audits required for GraphQL over HTTP spec conformance.
390
+ *
391
+ * Please consult the `AuditResult` for more information.
392
+ *
393
+ * @category Audits
394
+ */
395
+ async function auditServer(opts) {
396
+ const audits = serverAudits(opts);
397
+ // audit tests will throw only on fatal errors, tests are contained within the AuditResult
398
+ return await Promise.all(audits.map(({ fn }) => fn()));
399
+ }
400
+ exports.auditServer = auditServer;