Skip to content

Commit 2b21b08

Browse files
committed
fix(server): Adjust audits following the spec
1 parent 0bccfa9 commit 2b21b08

File tree

2 files changed

+71
-11
lines changed

2 files changed

+71
-11
lines changed

src/audits/server.ts

+55-11
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export function serverAudits(opts: ServerAuditOptions): Audit[] {
3838
return [
3939
// Media Types
4040
audit(
41-
'MUST accept application/graphql-response+json and match the content-type',
41+
// TODO: convert to MUST after watershed
42+
'SHOULD accept application/graphql-response+json and match the content-type',
4243
async () => {
4344
const url = new URL(opts.url);
4445
url.searchParams.set('query', '{ __typename }');
@@ -74,7 +75,8 @@ export function serverAudits(opts: ServerAuditOptions): Audit[] {
7475
},
7576
),
7677
audit(
77-
'MUST accept */* and use application/graphql-response+json for the content-type',
78+
// TODO: convert to MUST after watershed
79+
'SHOULD accept */* and use application/graphql-response+json for the content-type',
7880
async () => {
7981
const url = new URL(opts.url);
8082
url.searchParams.set('query', '{ __typename }');
@@ -92,7 +94,8 @@ export function serverAudits(opts: ServerAuditOptions): Audit[] {
9294
},
9395
),
9496
audit(
95-
'MUST assume application/graphql-response+json content-type when accept is missing',
97+
// TODO: convert to MUST after watershed
98+
'SHOULD assume application/graphql-response+json content-type when accept is missing',
9699
async () => {
97100
const url = new URL(opts.url);
98101
url.searchParams.set('query', '{ __typename }');
@@ -105,15 +108,40 @@ export function serverAudits(opts: ServerAuditOptions): Audit[] {
105108
).toContain('application/graphql-response+json');
106109
},
107110
),
108-
audit('MUST use utf-8 charset in response', async () => {
111+
audit('MUST use utf-8 encoding when responding', async () => {
109112
const url = new URL(opts.url);
110113
url.searchParams.set('query', '{ __typename }');
111114

112115
const res = await fetchFn(url.toString());
113116
assert('Status code', res.status).toBe(200);
114-
assert('Content-Type header', res.headers.get('content-type')).toContain(
115-
'charset=utf-8',
116-
);
117+
118+
// has charset set to utf-8
119+
try {
120+
assert(
121+
'Content-Type header',
122+
res.headers.get('content-type'),
123+
).toContain('charset=utf-8');
124+
return;
125+
} catch {
126+
// noop, continue
127+
}
128+
129+
// has no charset specified
130+
assert(
131+
'Content-Type header',
132+
res.headers.get('content-type'),
133+
).notToContain('charset');
134+
135+
// and the content is utf-8 encoded
136+
try {
137+
const decoder = new TextDecoder('utf-8');
138+
const decoded = decoder.decode(await res.arrayBuffer());
139+
assert('UTF-8 decoded body', decoded).toBe(
140+
'{"data":{"__typename":"Query"}}',
141+
);
142+
} catch {
143+
throw 'Body is not UTF-8 encoded';
144+
}
117145
}),
118146
audit('MUST accept only utf-8 charset', async () => {
119147
const url = new URL(opts.url);
@@ -124,10 +152,26 @@ export function serverAudits(opts: ServerAuditOptions): Audit[] {
124152
accept: 'application/graphql-response+json; charset=iso-8859-1',
125153
},
126154
});
127-
assert('Status code', res.status).toBe(406);
128-
assert('Accept header', res.headers.get('accept')).toContain(
129-
'charset=utf-8',
130-
);
155+
156+
// application/json is 200 + errors
157+
const contentType = res.headers.get('content-type') || '';
158+
if (contentType.includes('application/json')) {
159+
assert(`Content-Type ${contentType} status code`, res.status).toBe(200);
160+
161+
const body = await res.json();
162+
assert('Body data errors', body.errors).toBeDefined();
163+
return;
164+
}
165+
166+
// other content-types must be 4xx
167+
assert(
168+
`Content-Type ${contentType} status code`,
169+
res.status,
170+
).toBeGreaterThanOrEqual(400);
171+
assert(
172+
`Content-Type ${contentType} status code`,
173+
res.status,
174+
).toBeLessThanOrEqual(499);
131175
}),
132176
// Request
133177
audit('MUST accept POST requests', async () => {

src/audits/utils.ts

+16
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export function assert<T = unknown>(name: string, actual: T) {
5656
throw `${name} ${actual} is not ${expected}`;
5757
}
5858
},
59+
toBeDefined: () => {
60+
if (actual === undefined) {
61+
throw `${name} is not defined`;
62+
}
63+
},
5964
toBeLessThanOrEqual: (expected: T extends number ? T : never) => {
6065
if (!(actual <= expected)) {
6166
throw `${name} ${actual} is not less than or equal to ${expected}`;
@@ -77,6 +82,17 @@ export function assert<T = unknown>(name: string, actual: T) {
7782
)} does not contain ${JSON.stringify(expected)}`;
7883
}
7984
},
85+
notToContain: (
86+
expected: T extends Array<infer U> ? U : T extends string ? T : never,
87+
) => {
88+
// @ts-expect-error types will match, otherwise never
89+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
90+
if (actual.includes(expected as any)) {
91+
throw `${name} ${JSON.stringify(actual)} contains ${JSON.stringify(
92+
expected,
93+
)}`;
94+
}
95+
},
8096
notToHaveProperty: (
8197
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8298
prop: T extends Record<any, any> ? PropertyKey : never,

0 commit comments

Comments
 (0)