diff --git a/index.ts b/index.ts index 9413f7026..88a2a911d 100644 --- a/index.ts +++ b/index.ts @@ -109,7 +109,7 @@ export * from './src/util/errors/UnsupportedHttpError'; export * from './src/util/errors/UnsupportedMediaTypeHttpError'; // Util -export * from './src/util/AcceptParser'; +export * from './src/util/HeaderUtil'; export * from './src/util/AsyncHandler'; export * from './src/util/CompositeAsyncHandler'; export * from './src/util/InteractionController'; diff --git a/src/ldp/http/AcceptPreferenceParser.ts b/src/ldp/http/AcceptPreferenceParser.ts index 469968b90..958ad4bed 100644 --- a/src/ldp/http/AcceptPreferenceParser.ts +++ b/src/ldp/http/AcceptPreferenceParser.ts @@ -1,11 +1,11 @@ import type { HttpRequest } from '../../server/HttpRequest'; -import type { AcceptHeader } from '../../util/AcceptParser'; +import type { AcceptHeader } from '../../util/HeaderUtil'; import { parseAccept, parseAcceptCharset, parseAcceptEncoding, parseAcceptLanguage, -} from '../../util/AcceptParser'; +} from '../../util/HeaderUtil'; import type { RepresentationPreference } from '../representation/RepresentationPreference'; import type { RepresentationPreferences } from '../representation/RepresentationPreferences'; import { PreferenceParser } from './PreferenceParser'; diff --git a/src/ldp/http/metadata/BasicMetadataExtractor.ts b/src/ldp/http/metadata/BasicMetadataExtractor.ts new file mode 100644 index 000000000..be5d0b24b --- /dev/null +++ b/src/ldp/http/metadata/BasicMetadataExtractor.ts @@ -0,0 +1,29 @@ +import type { HttpRequest } from '../../../server/HttpRequest'; +import { RepresentationMetadata } from '../../representation/RepresentationMetadata'; +import { MetadataExtractor } from './MetadataExtractor'; +import type { MetadataParser } from './MetadataParser'; + +/** + * MetadataExtractor that lets each of its MetadataParsers add metadata based on the HttpRequest. + */ +export class BasicMetadataExtractor extends MetadataExtractor { + private readonly parsers: MetadataParser[]; + + public constructor(parsers: MetadataParser[]) { + super(); + this.parsers = parsers; + } + + public async canHandle(): Promise { + // Can handle all requests + } + + public async handle(request: HttpRequest): + Promise { + const metadata = new RepresentationMetadata(); + for (const parser of this.parsers) { + await parser.parse(request, metadata); + } + return metadata; + } +} diff --git a/src/ldp/http/metadata/ContentTypeParser.ts b/src/ldp/http/metadata/ContentTypeParser.ts new file mode 100644 index 000000000..01c06bbfc --- /dev/null +++ b/src/ldp/http/metadata/ContentTypeParser.ts @@ -0,0 +1,17 @@ +import type { HttpRequest } from '../../../server/HttpRequest'; +import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; +import type { MetadataParser } from './MetadataParser'; + +/** + * Parser for the `content-type` header. + * Currently only stores the media type and ignores other parameters such as charset. + */ +export class ContentTypeParser implements MetadataParser { + public async parse(request: HttpRequest, metadata: RepresentationMetadata): Promise { + const contentType = request.headers['content-type']; + if (contentType) { + // Will need to use HeaderUtil once parameters need to be parsed + metadata.contentType = /^[^;]*/u.exec(contentType)![0].trim(); + } + } +} diff --git a/src/ldp/http/metadata/LinkTypeParser.ts b/src/ldp/http/metadata/LinkTypeParser.ts new file mode 100644 index 000000000..99c68108f --- /dev/null +++ b/src/ldp/http/metadata/LinkTypeParser.ts @@ -0,0 +1,38 @@ +import { DataFactory } from 'n3'; +import { getLoggerFor } from '../../../logging/LogUtil'; +import type { HttpRequest } from '../../../server/HttpRequest'; +import { parseParameters, splitAndClean, transformQuotedStrings } from '../../../util/HeaderUtil'; +import { RDF } from '../../../util/UriConstants'; +import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; +import type { MetadataParser } from './MetadataParser'; + +/** + * Parses Link headers with "rel=type" parameters and adds them as RDF.type metadata. + */ +export class LinkTypeParser implements MetadataParser { + protected readonly logger = getLoggerFor(this); + + public async parse(request: HttpRequest, metadata: RepresentationMetadata): Promise { + const link = request.headers.link ?? []; + const entries: string[] = Array.isArray(link) ? link : [ link ]; + for (const entry of entries) { + this.parseLink(entry, metadata); + } + } + + protected parseLink(linkEntry: string, metadata: RepresentationMetadata): void { + const { result, replacements } = transformQuotedStrings(linkEntry); + for (const part of splitAndClean(result)) { + const [ link, ...parameters ] = part.split(/\s*;\s*/u); + if (/^[^<]|[^>]$/u.test(link)) { + this.logger.warn(`Invalid link header ${part}.`); + continue; + } + for (const { name, value } of parseParameters(parameters, replacements)) { + if (name === 'rel' && value === 'type') { + metadata.add(RDF.type, DataFactory.namedNode(link.slice(1, -1))); + } + } + } + } +} diff --git a/src/ldp/http/metadata/MetadataExtractor.ts b/src/ldp/http/metadata/MetadataExtractor.ts new file mode 100644 index 000000000..1251de244 --- /dev/null +++ b/src/ldp/http/metadata/MetadataExtractor.ts @@ -0,0 +1,9 @@ +import type { HttpRequest } from '../../../server/HttpRequest'; +import { AsyncHandler } from '../../../util/AsyncHandler'; +import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; + +/** + * Parses the metadata of a {@link HttpRequest} into a {@link RepresentationMetadata}. + */ +export abstract class MetadataExtractor extends + AsyncHandler {} diff --git a/src/ldp/http/metadata/MetadataParser.ts b/src/ldp/http/metadata/MetadataParser.ts new file mode 100644 index 000000000..3bf7bd4df --- /dev/null +++ b/src/ldp/http/metadata/MetadataParser.ts @@ -0,0 +1,15 @@ +import type { HttpRequest } from '../../../server/HttpRequest'; +import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; + +/** + * A parser that takes a specific part of an HttpRequest and converts it to medata, + * such as the value of a header entry. + */ +export interface MetadataParser { + /** + * Potentially adds metadata to the RepresentationMetadata based on the HttpRequest contents. + * @param request - Request with potential metadata. + * @param metadata - Metadata objects that should be updated. + */ + parse: (request: HttpRequest, metadata: RepresentationMetadata) => Promise; +} diff --git a/src/ldp/http/metadata/SlugParser.ts b/src/ldp/http/metadata/SlugParser.ts new file mode 100644 index 000000000..b0e5748bc --- /dev/null +++ b/src/ldp/http/metadata/SlugParser.ts @@ -0,0 +1,20 @@ +import type { HttpRequest } from '../../../server/HttpRequest'; +import { UnsupportedHttpError } from '../../../util/errors/UnsupportedHttpError'; +import { HTTP } from '../../../util/UriConstants'; +import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; +import type { MetadataParser } from './MetadataParser'; + +/** + * Converts the contents of the slug header to metadata. + */ +export class SlugParser implements MetadataParser { + public async parse(request: HttpRequest, metadata: RepresentationMetadata): Promise { + const { slug } = request.headers; + if (slug) { + if (Array.isArray(slug)) { + throw new UnsupportedHttpError('At most 1 slug header is allowed.'); + } + metadata.set(HTTP.slug, slug); + } + } +} diff --git a/src/util/AcceptParser.ts b/src/util/HeaderUtil.ts similarity index 79% rename from src/util/AcceptParser.ts rename to src/util/HeaderUtil.ts index 8febdb1ac..513561d28 100644 --- a/src/util/AcceptParser.ts +++ b/src/util/HeaderUtil.ts @@ -99,18 +99,18 @@ const token = /^[a-zA-Z0-9!#$%&'*+-.^_`|~]+$/u; * * @returns The transformed string and a map with keys `"0"`, etc. and values the original string that was there. */ -const transformQuotedStrings = (input: string): { result: string; replacements: { [id: string]: string } } => { +export const transformQuotedStrings = (input: string): { result: string; replacements: { [id: string]: string } } => { let idx = 0; const replacements: { [id: string]: string } = {}; const result = input.replace(/"(?:[^"\\]|\\.)*"/gu, (match): string => { // Not all characters allowed in quoted strings, see BNF above if (!/^"(?:[\t !\u0023-\u005B\u005D-\u007E\u0080-\u00FF]|(?:\\[\t\u0020-\u007E\u0080-\u00FF]))*"$/u.test(match)) { throw new UnsupportedHttpError( - `Invalid quoted string in Accept header: ${match}. Check which characters are allowed`, + `Invalid quoted string in header: ${match}. Check which characters are allowed`, ); } const replacement = `"${idx}"`; - replacements[replacement] = match; + replacements[replacement] = match.slice(1, -1); idx += 1; return replacement; }); @@ -122,7 +122,7 @@ const transformQuotedStrings = (input: string): { result: string; replacements: * * @param input - Input header string. */ -const splitAndClean = (input: string): string[] => +export const splitAndClean = (input: string): string[] => input.split(',') .map((part): string => part.trim()) .filter((part): boolean => part.length > 0); @@ -136,13 +136,47 @@ const splitAndClean = (input: string): string[] => * Thrown on invalid syntax. */ const testQValue = (qvalue: string): void => { - if (!/^q=(?:(?:0(?:\.\d{0,3})?)|(?:1(?:\.0{0,3})?))$/u.test(qvalue)) { + if (!/^(?:(?:0(?:\.\d{0,3})?)|(?:1(?:\.0{0,3})?))$/u.test(qvalue)) { throw new UnsupportedHttpError( - `Invalid q value: ${qvalue} does not match ("q=" ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] )).`, + `Invalid q value: ${qvalue} does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`, ); } }; +/** + * Parses a list of split parameters and checks their validity. + * + * @param parameters - A list of split parameters (token [ "=" ( token / quoted-string ) ]) + * @param replacements - The double quoted strings that need to be replaced. + * + * + * @throws {@link UnsupportedHttpError} + * Thrown on invalid parameter syntax. + * + * @returns An array of name/value objects corresponding to the parameters. + */ +export const parseParameters = (parameters: string[], replacements: { [id: string]: string }): +{ name: string; value: string }[] => parameters.map((param): { name: string; value: string } => { + const [ name, rawValue ] = param.split('=').map((str): string => str.trim()); + + // Test replaced string for easier check + // parameter = token "=" ( token / quoted-string ) + // second part is optional for certain parameters + if (!(token.test(name) && (!rawValue || /^"\d+"$/u.test(rawValue) || token.test(rawValue)))) { + throw new UnsupportedHttpError( + `Invalid parameter value: ${name}=${replacements[rawValue] || rawValue} ` + + `does not match (token ( "=" ( token / quoted-string ))?). `, + ); + } + + let value = rawValue; + if (value in replacements) { + value = replacements[rawValue]; + } + + return { name, value }; +}); + /** * Parses a single media range with corresponding parameters from an Accept header. * For every parameter value that is a double quoted string, @@ -163,7 +197,7 @@ const parseAcceptPart = (part: string, replacements: { [id: string]: string }): // No reason to test differently for * since we don't check if the type exists const [ type, subtype ] = range.split('/'); if (!type || !subtype || !token.test(type) || !token.test(subtype)) { - throw new Error( + throw new UnsupportedHttpError( `Invalid Accept range: ${range} does not match ( "*/*" / ( token "/" "*" ) / ( token "/" token ) )`, ); } @@ -172,33 +206,19 @@ const parseAcceptPart = (part: string, replacements: { [id: string]: string }): const mediaTypeParams: { [key: string]: string } = {}; const extensionParams: { [key: string]: string } = {}; let map = mediaTypeParams; - parameters.forEach((param): void => { - const [ name, value ] = param.split('='); - + const parsedParams = parseParameters(parameters, replacements); + parsedParams.forEach(({ name, value }): void => { if (name === 'q') { // Extension parameters appear after the q value map = extensionParams; - testQValue(param); + testQValue(value); weight = Number.parseFloat(value); } else { - // Test replaced string for easier check - // parameter = token "=" ( token / quoted-string ) - // second part is optional for extension parameters - if (!token.test(name) || - !((map === extensionParams && !value) || (value && (/^"\d+"$/u.test(value) || token.test(value))))) { - throw new UnsupportedHttpError( - `Invalid Accept parameter: ${param} does not match (token "=" ( token / quoted-string )). ` + - `Second part is optional for extension parameters.`, - ); + if (!value && map !== extensionParams) { + throw new UnsupportedHttpError(`Invalid Accept parameter ${name}: ` + + `Accept parameter values are not optional when preceding the q value.`); } - - let actualValue = value; - if (value && value.length > 0 && value.startsWith('"') && replacements[value]) { - actualValue = replacements[value]; - } - - // Value is optional for extension parameters - map[name] = actualValue || ''; + map[name] = value || ''; } }); @@ -228,8 +248,12 @@ const parseNoParameters = (input: string): { range: string; weight: number }[] = const [ range, qvalue ] = part.split(';').map((param): string => param.trim()); const result = { range, weight: 1 }; if (qvalue) { - testQValue(qvalue); - result.weight = Number.parseFloat(qvalue.split('=')[1]); + if (!qvalue.startsWith('q=')) { + throw new UnsupportedHttpError(`Only q parameters are allowed in ${input}.`); + } + const val = qvalue.slice(2); + testQValue(val); + result.weight = Number.parseFloat(val); } return result; }).sort((left, right): number => right.weight - left.weight); diff --git a/test/unit/ldp/http/metadata/BasicMetadataExtractor.test.ts b/test/unit/ldp/http/metadata/BasicMetadataExtractor.test.ts new file mode 100644 index 000000000..51c303e9f --- /dev/null +++ b/test/unit/ldp/http/metadata/BasicMetadataExtractor.test.ts @@ -0,0 +1,38 @@ +import { BasicMetadataExtractor } from '../../../../../src/ldp/http/metadata/BasicMetadataExtractor'; +import type { MetadataParser } from '../../../../../src/ldp/http/metadata/MetadataParser'; +import type { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata'; +import type { HttpRequest } from '../../../../../src/server/HttpRequest'; +import { RDF } from '../../../../../src/util/UriConstants'; + +class BasicParser implements MetadataParser { + private readonly header: string; + + public constructor(header: string) { + this.header = header; + } + + public async parse(input: HttpRequest, metadata: RepresentationMetadata): Promise { + const header = input.headers[this.header]; + if (header) { + if (typeof header === 'string') { + metadata.add(RDF.type, header); + } + } + } +} + +describe(' A BasicMetadataExtractor', (): void => { + const handler = new BasicMetadataExtractor([ + new BasicParser('aa'), + new BasicParser('bb'), + ]); + + it('can handle all requests.', async(): Promise => { + await expect(handler.canHandle()).resolves.toBeUndefined(); + }); + + it('will add metadata from the parsers.', async(): Promise => { + const metadata = await handler.handle({ headers: { aa: 'valA', bb: 'valB' } as any } as HttpRequest); + expect(metadata.getAll(RDF.type).map((term): any => term.value)).toEqual([ 'valA', 'valB' ]); + }); +}); diff --git a/test/unit/ldp/http/metadata/ContentTypeParser.test.ts b/test/unit/ldp/http/metadata/ContentTypeParser.test.ts new file mode 100644 index 000000000..b9731a0ec --- /dev/null +++ b/test/unit/ldp/http/metadata/ContentTypeParser.test.ts @@ -0,0 +1,26 @@ +import { ContentTypeParser } from '../../../../../src/ldp/http/metadata/ContentTypeParser'; +import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata'; +import type { HttpRequest } from '../../../../../src/server/HttpRequest'; + +describe('A ContentTypeParser', (): void => { + const parser = new ContentTypeParser(); + let request: HttpRequest; + let metadata: RepresentationMetadata; + + beforeEach(async(): Promise => { + request = { headers: {}} as HttpRequest; + metadata = new RepresentationMetadata(); + }); + + it('does nothing if there is no content-type header.', async(): Promise => { + await expect(parser.parse(request, metadata)).resolves.toBeUndefined(); + expect(metadata.quads()).toHaveLength(0); + }); + + it('sets the given content-type as metadata.', async(): Promise => { + request.headers['content-type'] = 'text/plain;charset=UTF-8'; + await expect(parser.parse(request, metadata)).resolves.toBeUndefined(); + expect(metadata.quads()).toHaveLength(1); + expect(metadata.contentType).toBe('text/plain'); + }); +}); diff --git a/test/unit/ldp/http/metadata/LinkTypeParser.test.ts b/test/unit/ldp/http/metadata/LinkTypeParser.test.ts new file mode 100644 index 000000000..d986d6af5 --- /dev/null +++ b/test/unit/ldp/http/metadata/LinkTypeParser.test.ts @@ -0,0 +1,61 @@ +import { LinkTypeParser } from '../../../../../src/ldp/http/metadata/LinkTypeParser'; +import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata'; +import { setGlobalLoggerFactory } from '../../../../../src/logging/LogUtil'; +import { VoidLoggerFactory } from '../../../../../src/logging/VoidLoggerFactory'; +import type { HttpRequest } from '../../../../../src/server/HttpRequest'; +import { RDF } from '../../../../../src/util/UriConstants'; + +describe('A LinkTypeParser', (): void => { + const parser = new LinkTypeParser(); + let request: HttpRequest; + let metadata: RepresentationMetadata; + + beforeAll(async(): Promise => { + setGlobalLoggerFactory(new VoidLoggerFactory()); + }); + + beforeEach(async(): Promise => { + request = { headers: {}} as HttpRequest; + metadata = new RepresentationMetadata(); + }); + + it('does nothing if there are no type headers.', async(): Promise => { + await parser.parse(request, metadata); + expect(metadata.quads()).toHaveLength(0); + }); + + it('stores link headers with rel = type as metadata.', async(): Promise => { + request.headers.link = ';rel="type"'; + await expect(parser.parse(request, metadata)).resolves.toBeUndefined(); + expect(metadata.quads()).toHaveLength(1); + expect(metadata.get(RDF.type)?.value).toBe('http://test.com/type'); + }); + + it('supports multiple link headers.', async(): Promise => { + request.headers.link = [ ';rel="type"', ';rel=type' ]; + await expect(parser.parse(request, metadata)).resolves.toBeUndefined(); + expect(metadata.quads()).toHaveLength(2); + expect(metadata.getAll(RDF.type).map((term): any => term.value)) + .toEqual([ 'http://test.com/typeA', 'http://test.com/typeB' ]); + }); + + it('supports multiple link header values in the same entry.', async(): Promise => { + request.headers.link = ';rel="type" , ;rel=type'; + await expect(parser.parse(request, metadata)).resolves.toBeUndefined(); + expect(metadata.quads()).toHaveLength(2); + expect(metadata.getAll(RDF.type).map((term): any => term.value)) + .toEqual([ 'http://test.com/typeA', 'http://test.com/typeB' ]); + }); + + it('ignores invalid link headers.', async(): Promise => { + request.headers.link = 'http://test.com/type;rel="type"'; + await parser.parse(request, metadata); + expect(metadata.quads()).toHaveLength(0); + }); + + it('ignores non-type link headers.', async(): Promise => { + request.headers.link = ';rel="notype" , '; + await expect(parser.parse(request, metadata)).resolves.toBeUndefined(); + expect(metadata.quads()).toHaveLength(0); + }); +}); diff --git a/test/unit/ldp/http/metadata/SlugParser.test.ts b/test/unit/ldp/http/metadata/SlugParser.test.ts new file mode 100644 index 000000000..96a837da9 --- /dev/null +++ b/test/unit/ldp/http/metadata/SlugParser.test.ts @@ -0,0 +1,34 @@ +import { SlugParser } from '../../../../../src/ldp/http/metadata/SlugParser'; +import { RepresentationMetadata } from '../../../../../src/ldp/representation/RepresentationMetadata'; +import type { HttpRequest } from '../../../../../src/server/HttpRequest'; +import { UnsupportedHttpError } from '../../../../../src/util/errors/UnsupportedHttpError'; +import { HTTP } from '../../../../../src/util/UriConstants'; + +describe('A SlugParser', (): void => { + const parser = new SlugParser(); + let request: HttpRequest; + let metadata: RepresentationMetadata; + + beforeEach(async(): Promise => { + request = { headers: {}} as HttpRequest; + metadata = new RepresentationMetadata(); + }); + + it('does nothing if there is no slug header.', async(): Promise => { + await expect(parser.parse(request, metadata)).resolves.toBeUndefined(); + expect(metadata.quads()).toHaveLength(0); + }); + + it('errors if there are multiple slug headers.', async(): Promise => { + request.headers.slug = [ 'slugA', 'slugB' ]; + await expect(parser.parse(request, metadata)) + .rejects.toThrow(new UnsupportedHttpError('At most 1 slug header is allowed.')); + }); + + it('stores the slug metadata.', async(): Promise => { + request.headers.slug = 'slugA'; + await expect(parser.parse(request, metadata)).resolves.toBeUndefined(); + expect(metadata.quads()).toHaveLength(1); + expect(metadata.get(HTTP.slug)?.value).toBe('slugA'); + }); +}); diff --git a/test/unit/util/AcceptParser.test.ts b/test/unit/util/HeaderUtil.test.ts similarity index 90% rename from test/unit/util/AcceptParser.test.ts rename to test/unit/util/HeaderUtil.test.ts index 23a268294..fb7e0fe14 100644 --- a/test/unit/util/AcceptParser.test.ts +++ b/test/unit/util/HeaderUtil.test.ts @@ -3,9 +3,9 @@ import { parseAcceptCharset, parseAcceptEncoding, parseAcceptLanguage, -} from '../../../src/util/AcceptParser'; +} from '../../../src/util/HeaderUtil'; -describe('AcceptParser', (): void => { +describe('HeaderUtil', (): void => { describe('parseAccept function', (): void => { it('parses empty Accept headers.', async(): Promise => { expect(parseAccept('')).toEqual([]); @@ -39,7 +39,7 @@ describe('AcceptParser', (): void => { expect(parseAccept('audio/basic; param1="val" ; q=0.5 ;param2="\\\\\\"valid"')).toEqual([ { range: 'audio/basic', weight: 0.5, - parameters: { mediaType: { param1: '"val"' }, extension: { param2: '"\\\\\\"valid"' }}}, + parameters: { mediaType: { param1: 'val' }, extension: { param2: '\\\\\\"valid' }}}, ]); }); @@ -59,15 +59,15 @@ describe('AcceptParser', (): void => { }); it('rejects Accept Headers with invalid parameters.', async(): Promise => { - expect((): any => parseAccept('a/b; a')).toThrow('Invalid Accept parameter:'); - expect((): any => parseAccept('a/b; a=\\')).toThrow('Invalid Accept parameter:'); - expect((): any => parseAccept('a/b; q=1 ; a=\\')).toThrow('Invalid Accept parameter:'); - expect((): any => parseAccept('a/b; q=1 ; a')).not.toThrow('Invalid Accept parameter:'); + expect((): any => parseAccept('a/b; a')).toThrow('Invalid Accept parameter'); + expect((): any => parseAccept('a/b; a=\\')).toThrow('Invalid parameter value'); + expect((): any => parseAccept('a/b; q=1 ; a=\\')).toThrow('Invalid parameter value'); + expect((): any => parseAccept('a/b; q=1 ; a')).not.toThrow('Invalid Accept parameter'); }); it('rejects Accept Headers with quoted parameters.', async(): Promise => { expect((): any => parseAccept('a/b; a="\\""')).not.toThrow(); - expect((): any => parseAccept('a/b; a="\\\u007F"')).toThrow('Invalid quoted string in Accept header:'); + expect((): any => parseAccept('a/b; a="\\\u007F"')).toThrow('Invalid quoted string in header:'); }); }); @@ -82,6 +82,7 @@ describe('AcceptParser', (): void => { it('rejects invalid Accept-Charset Headers.', async(): Promise => { expect((): any => parseAcceptCharset('a/b')).toThrow('Invalid Accept-Charset range:'); expect((): any => parseAcceptCharset('a; q=text')).toThrow('Invalid q value:'); + expect((): any => parseAcceptCharset('a; c=d')).toThrow('Only q parameters are allowed'); }); }); @@ -101,6 +102,7 @@ describe('AcceptParser', (): void => { it('rejects invalid Accept-Encoding Headers.', async(): Promise => { expect((): any => parseAcceptEncoding('a/b')).toThrow('Invalid Accept-Encoding range:'); expect((): any => parseAcceptEncoding('a; q=text')).toThrow('Invalid q value:'); + expect((): any => parseAcceptCharset('a; c=d')).toThrow('Only q parameters are allowed'); }); }); @@ -122,6 +124,7 @@ describe('AcceptParser', (): void => { expect((): any => parseAcceptLanguage('a-b-c-d')).not.toThrow('Invalid Accept-Language range:'); expect((): any => parseAcceptLanguage('a; q=text')).toThrow('Invalid q value:'); + expect((): any => parseAcceptCharset('a; c=d')).toThrow('Only q parameters are allowed'); }); }); });