mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create MetadataHandler
This commit is contained in:
parent
bb28af937b
commit
7dcb3eaa84
2
index.ts
2
index.ts
@ -109,7 +109,7 @@ export * from './src/util/errors/UnsupportedHttpError';
|
|||||||
export * from './src/util/errors/UnsupportedMediaTypeHttpError';
|
export * from './src/util/errors/UnsupportedMediaTypeHttpError';
|
||||||
|
|
||||||
// Util
|
// Util
|
||||||
export * from './src/util/AcceptParser';
|
export * from './src/util/HeaderUtil';
|
||||||
export * from './src/util/AsyncHandler';
|
export * from './src/util/AsyncHandler';
|
||||||
export * from './src/util/CompositeAsyncHandler';
|
export * from './src/util/CompositeAsyncHandler';
|
||||||
export * from './src/util/InteractionController';
|
export * from './src/util/InteractionController';
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import type { HttpRequest } from '../../server/HttpRequest';
|
import type { HttpRequest } from '../../server/HttpRequest';
|
||||||
import type { AcceptHeader } from '../../util/AcceptParser';
|
import type { AcceptHeader } from '../../util/HeaderUtil';
|
||||||
import {
|
import {
|
||||||
parseAccept,
|
parseAccept,
|
||||||
parseAcceptCharset,
|
parseAcceptCharset,
|
||||||
parseAcceptEncoding,
|
parseAcceptEncoding,
|
||||||
parseAcceptLanguage,
|
parseAcceptLanguage,
|
||||||
} from '../../util/AcceptParser';
|
} from '../../util/HeaderUtil';
|
||||||
import type { RepresentationPreference } from '../representation/RepresentationPreference';
|
import type { RepresentationPreference } from '../representation/RepresentationPreference';
|
||||||
import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
|
import type { RepresentationPreferences } from '../representation/RepresentationPreferences';
|
||||||
import { PreferenceParser } from './PreferenceParser';
|
import { PreferenceParser } from './PreferenceParser';
|
||||||
|
29
src/ldp/http/metadata/BasicMetadataExtractor.ts
Normal file
29
src/ldp/http/metadata/BasicMetadataExtractor.ts
Normal file
@ -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<void> {
|
||||||
|
// Can handle all requests
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle(request: HttpRequest):
|
||||||
|
Promise<RepresentationMetadata> {
|
||||||
|
const metadata = new RepresentationMetadata();
|
||||||
|
for (const parser of this.parsers) {
|
||||||
|
await parser.parse(request, metadata);
|
||||||
|
}
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
17
src/ldp/http/metadata/ContentTypeParser.ts
Normal file
17
src/ldp/http/metadata/ContentTypeParser.ts
Normal file
@ -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<void> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
src/ldp/http/metadata/LinkTypeParser.ts
Normal file
38
src/ldp/http/metadata/LinkTypeParser.ts
Normal file
@ -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<void> {
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/ldp/http/metadata/MetadataExtractor.ts
Normal file
9
src/ldp/http/metadata/MetadataExtractor.ts
Normal file
@ -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<HttpRequest, RepresentationMetadata> {}
|
15
src/ldp/http/metadata/MetadataParser.ts
Normal file
15
src/ldp/http/metadata/MetadataParser.ts
Normal file
@ -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<void>;
|
||||||
|
}
|
20
src/ldp/http/metadata/SlugParser.ts
Normal file
20
src/ldp/http/metadata/SlugParser.ts
Normal file
@ -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<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
* @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;
|
let idx = 0;
|
||||||
const replacements: { [id: string]: string } = {};
|
const replacements: { [id: string]: string } = {};
|
||||||
const result = input.replace(/"(?:[^"\\]|\\.)*"/gu, (match): string => {
|
const result = input.replace(/"(?:[^"\\]|\\.)*"/gu, (match): string => {
|
||||||
// Not all characters allowed in quoted strings, see BNF above
|
// 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)) {
|
if (!/^"(?:[\t !\u0023-\u005B\u005D-\u007E\u0080-\u00FF]|(?:\\[\t\u0020-\u007E\u0080-\u00FF]))*"$/u.test(match)) {
|
||||||
throw new UnsupportedHttpError(
|
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}"`;
|
const replacement = `"${idx}"`;
|
||||||
replacements[replacement] = match;
|
replacements[replacement] = match.slice(1, -1);
|
||||||
idx += 1;
|
idx += 1;
|
||||||
return replacement;
|
return replacement;
|
||||||
});
|
});
|
||||||
@ -122,7 +122,7 @@ const transformQuotedStrings = (input: string): { result: string; replacements:
|
|||||||
*
|
*
|
||||||
* @param input - Input header string.
|
* @param input - Input header string.
|
||||||
*/
|
*/
|
||||||
const splitAndClean = (input: string): string[] =>
|
export const splitAndClean = (input: string): string[] =>
|
||||||
input.split(',')
|
input.split(',')
|
||||||
.map((part): string => part.trim())
|
.map((part): string => part.trim())
|
||||||
.filter((part): boolean => part.length > 0);
|
.filter((part): boolean => part.length > 0);
|
||||||
@ -136,13 +136,47 @@ const splitAndClean = (input: string): string[] =>
|
|||||||
* Thrown on invalid syntax.
|
* Thrown on invalid syntax.
|
||||||
*/
|
*/
|
||||||
const testQValue = (qvalue: string): void => {
|
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(
|
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.
|
* Parses a single media range with corresponding parameters from an Accept header.
|
||||||
* For every parameter value that is a double quoted string,
|
* 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
|
// No reason to test differently for * since we don't check if the type exists
|
||||||
const [ type, subtype ] = range.split('/');
|
const [ type, subtype ] = range.split('/');
|
||||||
if (!type || !subtype || !token.test(type) || !token.test(subtype)) {
|
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 ) )`,
|
`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 mediaTypeParams: { [key: string]: string } = {};
|
||||||
const extensionParams: { [key: string]: string } = {};
|
const extensionParams: { [key: string]: string } = {};
|
||||||
let map = mediaTypeParams;
|
let map = mediaTypeParams;
|
||||||
parameters.forEach((param): void => {
|
const parsedParams = parseParameters(parameters, replacements);
|
||||||
const [ name, value ] = param.split('=');
|
parsedParams.forEach(({ name, value }): void => {
|
||||||
|
|
||||||
if (name === 'q') {
|
if (name === 'q') {
|
||||||
// Extension parameters appear after the q value
|
// Extension parameters appear after the q value
|
||||||
map = extensionParams;
|
map = extensionParams;
|
||||||
testQValue(param);
|
testQValue(value);
|
||||||
weight = Number.parseFloat(value);
|
weight = Number.parseFloat(value);
|
||||||
} else {
|
} else {
|
||||||
// Test replaced string for easier check
|
if (!value && map !== extensionParams) {
|
||||||
// parameter = token "=" ( token / quoted-string )
|
throw new UnsupportedHttpError(`Invalid Accept parameter ${name}: ` +
|
||||||
// second part is optional for extension parameters
|
`Accept parameter values are not optional when preceding the q value.`);
|
||||||
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.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
map[name] = 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 || '';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -228,8 +248,12 @@ const parseNoParameters = (input: string): { range: string; weight: number }[] =
|
|||||||
const [ range, qvalue ] = part.split(';').map((param): string => param.trim());
|
const [ range, qvalue ] = part.split(';').map((param): string => param.trim());
|
||||||
const result = { range, weight: 1 };
|
const result = { range, weight: 1 };
|
||||||
if (qvalue) {
|
if (qvalue) {
|
||||||
testQValue(qvalue);
|
if (!qvalue.startsWith('q=')) {
|
||||||
result.weight = Number.parseFloat(qvalue.split('=')[1]);
|
throw new UnsupportedHttpError(`Only q parameters are allowed in ${input}.`);
|
||||||
|
}
|
||||||
|
const val = qvalue.slice(2);
|
||||||
|
testQValue(val);
|
||||||
|
result.weight = Number.parseFloat(val);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}).sort((left, right): number => right.weight - left.weight);
|
}).sort((left, right): number => right.weight - left.weight);
|
38
test/unit/ldp/http/metadata/BasicMetadataExtractor.test.ts
Normal file
38
test/unit/ldp/http/metadata/BasicMetadataExtractor.test.ts
Normal file
@ -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<void> {
|
||||||
|
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<void> => {
|
||||||
|
await expect(handler.canHandle()).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will add metadata from the parsers.', async(): Promise<void> => {
|
||||||
|
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' ]);
|
||||||
|
});
|
||||||
|
});
|
26
test/unit/ldp/http/metadata/ContentTypeParser.test.ts
Normal file
26
test/unit/ldp/http/metadata/ContentTypeParser.test.ts
Normal file
@ -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<void> => {
|
||||||
|
request = { headers: {}} as HttpRequest;
|
||||||
|
metadata = new RepresentationMetadata();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if there is no content-type header.', async(): Promise<void> => {
|
||||||
|
await expect(parser.parse(request, metadata)).resolves.toBeUndefined();
|
||||||
|
expect(metadata.quads()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the given content-type as metadata.', async(): Promise<void> => {
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
61
test/unit/ldp/http/metadata/LinkTypeParser.test.ts
Normal file
61
test/unit/ldp/http/metadata/LinkTypeParser.test.ts
Normal file
@ -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<void> => {
|
||||||
|
setGlobalLoggerFactory(new VoidLoggerFactory());
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
request = { headers: {}} as HttpRequest;
|
||||||
|
metadata = new RepresentationMetadata();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if there are no type headers.', async(): Promise<void> => {
|
||||||
|
await parser.parse(request, metadata);
|
||||||
|
expect(metadata.quads()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores link headers with rel = type as metadata.', async(): Promise<void> => {
|
||||||
|
request.headers.link = '<http://test.com/type>;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<void> => {
|
||||||
|
request.headers.link = [ '<http://test.com/typeA>;rel="type"', '<http://test.com/typeB>;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<void> => {
|
||||||
|
request.headers.link = '<http://test.com/typeA>;rel="type" , <http://test.com/typeB>;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<void> => {
|
||||||
|
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<void> => {
|
||||||
|
request.headers.link = '<http://test.com/typeA>;rel="notype" , <http://test.com/typeB>';
|
||||||
|
await expect(parser.parse(request, metadata)).resolves.toBeUndefined();
|
||||||
|
expect(metadata.quads()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
34
test/unit/ldp/http/metadata/SlugParser.test.ts
Normal file
34
test/unit/ldp/http/metadata/SlugParser.test.ts
Normal file
@ -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<void> => {
|
||||||
|
request = { headers: {}} as HttpRequest;
|
||||||
|
metadata = new RepresentationMetadata();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if there is no slug header.', async(): Promise<void> => {
|
||||||
|
await expect(parser.parse(request, metadata)).resolves.toBeUndefined();
|
||||||
|
expect(metadata.quads()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if there are multiple slug headers.', async(): Promise<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
@ -3,9 +3,9 @@ import {
|
|||||||
parseAcceptCharset,
|
parseAcceptCharset,
|
||||||
parseAcceptEncoding,
|
parseAcceptEncoding,
|
||||||
parseAcceptLanguage,
|
parseAcceptLanguage,
|
||||||
} from '../../../src/util/AcceptParser';
|
} from '../../../src/util/HeaderUtil';
|
||||||
|
|
||||||
describe('AcceptParser', (): void => {
|
describe('HeaderUtil', (): void => {
|
||||||
describe('parseAccept function', (): void => {
|
describe('parseAccept function', (): void => {
|
||||||
it('parses empty Accept headers.', async(): Promise<void> => {
|
it('parses empty Accept headers.', async(): Promise<void> => {
|
||||||
expect(parseAccept('')).toEqual([]);
|
expect(parseAccept('')).toEqual([]);
|
||||||
@ -39,7 +39,7 @@ describe('AcceptParser', (): void => {
|
|||||||
expect(parseAccept('audio/basic; param1="val" ; q=0.5 ;param2="\\\\\\"valid"')).toEqual([
|
expect(parseAccept('audio/basic; param1="val" ; q=0.5 ;param2="\\\\\\"valid"')).toEqual([
|
||||||
{ range: 'audio/basic',
|
{ range: 'audio/basic',
|
||||||
weight: 0.5,
|
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<void> => {
|
it('rejects Accept Headers with invalid parameters.', async(): Promise<void> => {
|
||||||
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; 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 Accept parameter:');
|
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:');
|
expect((): any => parseAccept('a/b; q=1 ; a')).not.toThrow('Invalid Accept parameter');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects Accept Headers with quoted parameters.', async(): Promise<void> => {
|
it('rejects Accept Headers with quoted parameters.', async(): Promise<void> => {
|
||||||
expect((): any => parseAccept('a/b; a="\\""')).not.toThrow();
|
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<void> => {
|
it('rejects invalid Accept-Charset Headers.', async(): Promise<void> => {
|
||||||
expect((): any => parseAcceptCharset('a/b')).toThrow('Invalid Accept-Charset range:');
|
expect((): any => parseAcceptCharset('a/b')).toThrow('Invalid Accept-Charset range:');
|
||||||
expect((): any => parseAcceptCharset('a; q=text')).toThrow('Invalid q value:');
|
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<void> => {
|
it('rejects invalid Accept-Encoding Headers.', async(): Promise<void> => {
|
||||||
expect((): any => parseAcceptEncoding('a/b')).toThrow('Invalid Accept-Encoding range:');
|
expect((): any => parseAcceptEncoding('a/b')).toThrow('Invalid Accept-Encoding range:');
|
||||||
expect((): any => parseAcceptEncoding('a; q=text')).toThrow('Invalid q value:');
|
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-b-c-d')).not.toThrow('Invalid Accept-Language range:');
|
||||||
|
|
||||||
expect((): any => parseAcceptLanguage('a; q=text')).toThrow('Invalid q value:');
|
expect((): any => parseAcceptLanguage('a; q=text')).toThrow('Invalid q value:');
|
||||||
|
expect((): any => parseAcceptCharset('a; c=d')).toThrow('Only q parameters are allowed');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
Loading…
x
Reference in New Issue
Block a user