refactor: Restructure source code folder

This way the location of certain classes should make more sense
This commit is contained in:
Joachim Van Herwegen
2021-10-08 10:58:35 +02:00
parent 012d9e0864
commit b3da9c9fcf
280 changed files with 684 additions and 673 deletions

View File

@@ -0,0 +1,201 @@
import { EventEmitter } from 'events';
import { UnsecureWebSocketsProtocol } from '../../../src/http/UnsecureWebSocketsProtocol';
import type { HttpRequest } from '../../../src/server/HttpRequest';
class DummySocket extends EventEmitter {
public readonly messages = new Array<string>();
public readonly close = jest.fn();
public send(message: string): void {
this.messages.push(message);
}
}
describe('An UnsecureWebSocketsProtocol', (): void => {
const source = new EventEmitter();
const protocol = new UnsecureWebSocketsProtocol(source);
describe('after registering a socket', (): void => {
const webSocket = new DummySocket();
beforeAll(async(): Promise<void> => {
const upgradeRequest = {
headers: {
host: 'mypod.example',
'sec-websocket-protocol': 'solid-0.1, other/1.0.0',
},
socket: {
secure: true,
},
} as any as HttpRequest;
await protocol.handle({ webSocket, upgradeRequest } as any);
});
afterEach((): void => {
webSocket.messages.length = 0;
});
it('sends a protocol message.', (): void => {
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('protocol solid-0.1');
});
it('warns when receiving an unexpected message.', (): void => {
webSocket.emit('message', 'unexpected');
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('warning Unrecognized message format: unexpected');
});
it('warns when receiving an unexpected message type.', (): void => {
webSocket.emit('message', 'unknown 1 2 3');
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('warning Unrecognized message type: unknown');
});
describe('before subscribing to resources', (): void => {
it('does not emit pub messages.', (): void => {
source.emit('changed', { path: 'https://mypod.example/foo/bar' });
expect(webSocket.messages).toHaveLength(0);
});
});
describe('after subscribing to a resource', (): void => {
beforeAll((): void => {
webSocket.emit('message', 'sub https://mypod.example/foo/bar');
});
it('sends an ack message.', (): void => {
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('ack https://mypod.example/foo/bar');
});
it('emits pub messages for that resource.', (): void => {
source.emit('changed', { path: 'https://mypod.example/foo/bar' });
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('pub https://mypod.example/foo/bar');
});
});
describe('after subscribing to a resource via a relative URL', (): void => {
beforeAll((): void => {
webSocket.emit('message', 'sub /relative/foo');
});
it('sends an ack message.', (): void => {
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('ack https://mypod.example/relative/foo');
});
it('emits pub messages for that resource.', (): void => {
source.emit('changed', { path: 'https://mypod.example/relative/foo' });
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift()).toBe('pub https://mypod.example/relative/foo');
});
});
describe('after subscribing to a resource with the wrong host name', (): void => {
beforeAll((): void => {
webSocket.emit('message', 'sub https://wrong.example/host/foo');
});
it('send an error message.', (): void => {
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift())
.toBe('error Mismatched host: wrong.example instead of mypod.example');
});
});
describe('after subscribing to a resource with the wrong protocol', (): void => {
beforeAll((): void => {
webSocket.emit('message', 'sub http://mypod.example/protocol/foo');
});
it('send an error message.', (): void => {
expect(webSocket.messages).toHaveLength(1);
expect(webSocket.messages.shift())
.toBe('error Mismatched protocol: http: instead of https:');
});
});
});
it('unsubscribes when a socket closes.', async(): Promise<void> => {
const webSocket = new DummySocket();
await protocol.handle({ webSocket, upgradeRequest: { headers: {}, socket: {}}} as any);
expect(webSocket.listenerCount('message')).toBe(1);
webSocket.emit('close');
expect(webSocket.listenerCount('message')).toBe(0);
expect(webSocket.listenerCount('close')).toBe(0);
expect(webSocket.listenerCount('error')).toBe(0);
});
it('unsubscribes when a socket errors.', async(): Promise<void> => {
const webSocket = new DummySocket();
await protocol.handle({ webSocket, upgradeRequest: { headers: {}, socket: {}}} as any);
expect(webSocket.listenerCount('message')).toBe(1);
webSocket.emit('error');
expect(webSocket.listenerCount('message')).toBe(0);
expect(webSocket.listenerCount('close')).toBe(0);
expect(webSocket.listenerCount('error')).toBe(0);
});
it('emits a warning when no Sec-WebSocket-Protocol is supplied.', async(): Promise<void> => {
const webSocket = new DummySocket();
const upgradeRequest = {
headers: {},
socket: {},
} as any as HttpRequest;
await protocol.handle({ webSocket, upgradeRequest } as any);
expect(webSocket.messages).toHaveLength(2);
expect(webSocket.messages.pop())
.toBe('warning Missing Sec-WebSocket-Protocol header, expected value \'solid-0.1\'');
expect(webSocket.close).toHaveBeenCalledTimes(0);
});
it('emits an error and closes the connection with the wrong Sec-WebSocket-Protocol.', async(): Promise<void> => {
const webSocket = new DummySocket();
const upgradeRequest = {
headers: {
'sec-websocket-protocol': 'solid/1.0.0, other',
},
socket: {},
} as any as HttpRequest;
await protocol.handle({ webSocket, upgradeRequest } as any);
expect(webSocket.messages).toHaveLength(2);
expect(webSocket.messages.pop()).toBe('error Client does not support protocol solid-0.1');
expect(webSocket.close).toHaveBeenCalledTimes(1);
expect(webSocket.listenerCount('message')).toBe(0);
expect(webSocket.listenerCount('close')).toBe(0);
expect(webSocket.listenerCount('error')).toBe(0);
});
it('respects the Forwarded header.', async(): Promise<void> => {
const webSocket = new DummySocket();
const upgradeRequest = {
headers: {
forwarded: 'proto=https;host=other.example',
'sec-websocket-protocol': 'solid-0.1',
},
socket: {},
} as any as HttpRequest;
await protocol.handle({ webSocket, upgradeRequest } as any);
webSocket.emit('message', 'sub https://other.example/protocol/foo');
expect(webSocket.messages).toHaveLength(2);
expect(webSocket.messages.pop()).toBe('ack https://other.example/protocol/foo');
});
it('respects the X-Forwarded-* headers if Forwarded header is not present.', async(): Promise<void> => {
const webSocket = new DummySocket();
const upgradeRequest = {
headers: {
'x-forwarded-host': 'other.example',
'x-forwarded-proto': 'https',
'sec-websocket-protocol': 'solid-0.1',
},
socket: {},
} as any as HttpRequest;
await protocol.handle({ webSocket, upgradeRequest } as any);
webSocket.emit('message', 'sub https://other.example/protocol/foo');
expect(webSocket.messages).toHaveLength(2);
expect(webSocket.messages.pop()).toBe('ack https://other.example/protocol/foo');
});
});

View File

@@ -0,0 +1,84 @@
import type { AuxiliaryIdentifierStrategy } from '../../../../src/http/auxiliary/AuxiliaryIdentifierStrategy';
import { ComposedAuxiliaryStrategy } from '../../../../src/http/auxiliary/ComposedAuxiliaryStrategy';
import type { MetadataGenerator } from '../../../../src/http/auxiliary/MetadataGenerator';
import type { Validator } from '../../../../src/http/auxiliary/Validator';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
describe('A ComposedAuxiliaryStrategy', (): void => {
const identifier = { path: 'http://test.com/foo' };
let identifierStrategy: AuxiliaryIdentifierStrategy;
let metadataGenerator: MetadataGenerator;
let validator: Validator;
let strategy: ComposedAuxiliaryStrategy;
beforeEach(async(): Promise<void> => {
identifierStrategy = {
getAuxiliaryIdentifier: jest.fn(),
getAuxiliaryIdentifiers: jest.fn(),
getSubjectIdentifier: jest.fn(),
isAuxiliaryIdentifier: jest.fn(),
};
metadataGenerator = {
handleSafe: jest.fn(),
} as any;
validator = {
handleSafe: jest.fn(),
} as any;
strategy = new ComposedAuxiliaryStrategy(identifierStrategy, metadataGenerator, validator, false, true);
});
it('calls the AuxiliaryIdentifierStrategy for related calls.', async(): Promise<void> => {
strategy.getAuxiliaryIdentifier(identifier);
expect(identifierStrategy.getAuxiliaryIdentifier).toHaveBeenCalledTimes(1);
expect(identifierStrategy.getAuxiliaryIdentifier).toHaveBeenLastCalledWith(identifier);
strategy.getAuxiliaryIdentifiers(identifier);
expect(identifierStrategy.getAuxiliaryIdentifiers).toHaveBeenCalledTimes(1);
expect(identifierStrategy.getAuxiliaryIdentifiers).toHaveBeenLastCalledWith(identifier);
strategy.getSubjectIdentifier(identifier);
expect(identifierStrategy.getSubjectIdentifier).toHaveBeenCalledTimes(1);
expect(identifierStrategy.getSubjectIdentifier).toHaveBeenLastCalledWith(identifier);
strategy.isAuxiliaryIdentifier(identifier);
expect(identifierStrategy.isAuxiliaryIdentifier).toHaveBeenCalledTimes(1);
expect(identifierStrategy.isAuxiliaryIdentifier).toHaveBeenLastCalledWith(identifier);
});
it('returns the injected value for usesOwnAuthorization.', async(): Promise<void> => {
expect(strategy.usesOwnAuthorization()).toBe(false);
});
it('returns the injected value for isRequiredInRoot.', async(): Promise<void> => {
expect(strategy.isRequiredInRoot()).toBe(true);
});
it('adds metadata through the MetadataGenerator.', async(): Promise<void> => {
const metadata = new RepresentationMetadata();
await expect(strategy.addMetadata(metadata)).resolves.toBeUndefined();
expect(metadataGenerator.handleSafe).toHaveBeenCalledTimes(1);
expect(metadataGenerator.handleSafe).toHaveBeenLastCalledWith(metadata);
});
it('validates data through the Validator.', async(): Promise<void> => {
const representation = { data: 'data!' } as any;
await expect(strategy.validate(representation)).resolves.toBeUndefined();
expect(validator.handleSafe).toHaveBeenCalledTimes(1);
expect(validator.handleSafe).toHaveBeenLastCalledWith(representation);
});
it('defaults isRequiredInRoot to false.', async(): Promise<void> => {
strategy = new ComposedAuxiliaryStrategy(identifierStrategy, metadataGenerator, validator);
expect(strategy.isRequiredInRoot()).toBe(false);
});
it('does not add metadata or validate if the corresponding classes are not injected.', async(): Promise<void> => {
strategy = new ComposedAuxiliaryStrategy(identifierStrategy);
const metadata = new RepresentationMetadata();
await expect(strategy.addMetadata(metadata)).resolves.toBeUndefined();
const representation = { data: 'data!' } as any;
await expect(strategy.validate(representation)).resolves.toBeUndefined();
});
});

View File

@@ -0,0 +1,41 @@
import type { AuxiliaryIdentifierStrategy } from '../../../../src/http/auxiliary/AuxiliaryIdentifierStrategy';
import { LinkMetadataGenerator } from '../../../../src/http/auxiliary/LinkMetadataGenerator';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
import { SOLID_META } from '../../../../src/util/Vocabularies';
describe('A LinkMetadataGenerator', (): void => {
const link = 'link';
const subjectId: ResourceIdentifier = { path: 'http://test.com/foo' };
const auxiliaryId: ResourceIdentifier = { path: 'http://test.com/foo.dummy' };
let generator: LinkMetadataGenerator;
beforeEach(async(): Promise<void> => {
const strategy = {
getAuxiliaryIdentifier: (identifier: ResourceIdentifier): ResourceIdentifier =>
({ path: `${identifier.path}.dummy` }),
isAuxiliaryIdentifier: (identifier: ResourceIdentifier): boolean => identifier.path.endsWith('.dummy'),
getSubjectIdentifier: (identifier: ResourceIdentifier): ResourceIdentifier =>
({ path: identifier.path.slice(0, -'.dummy'.length) }),
} as AuxiliaryIdentifierStrategy;
generator = new LinkMetadataGenerator(link, strategy);
});
it('can handle all metadata.', async(): Promise<void> => {
await expect(generator.canHandle(null as any)).resolves.toBeUndefined();
});
it('stores no metadata if the input is a subject resource.', async(): Promise<void> => {
const metadata = new RepresentationMetadata(auxiliaryId);
await expect(generator.handle(metadata)).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
});
it('uses the stored link to add metadata for subject resources.', async(): Promise<void> => {
const metadata = new RepresentationMetadata(subjectId);
await expect(generator.handle(metadata)).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(1);
expect(metadata.get(link)?.value).toBe(auxiliaryId.path);
expect(metadata.getAll(link, SOLID_META.terms.ResponseMetadata)).toHaveLength(1);
});
});

View File

@@ -0,0 +1,44 @@
import { RdfValidator } from '../../../../src/http/auxiliary/RdfValidator';
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import type { RepresentationConverter } from '../../../../src/storage/conversion/RepresentationConverter';
import { readableToString } from '../../../../src/util/StreamUtil';
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
import 'jest-rdf';
describe('An RdfValidator', (): void => {
let converter: RepresentationConverter;
let validator: RdfValidator;
beforeEach(async(): Promise<void> => {
converter = new StaticAsyncHandler<any>(true, null);
validator = new RdfValidator(converter);
});
it('can handle all representations.', async(): Promise<void> => {
await expect(validator.canHandle(null as any)).resolves.toBeUndefined();
});
it('always accepts content-type internal/quads.', async(): Promise<void> => {
const representation = new BasicRepresentation('data', 'internal/quads');
await expect(validator.handle(representation)).resolves.toBeUndefined();
});
it('validates data by running it through a converter.', async(): Promise<void> => {
converter.handleSafe = jest.fn().mockResolvedValue(new BasicRepresentation('transformedData', 'wrongType'));
const representation = new BasicRepresentation('data', 'content-type');
const quads = representation.metadata.quads();
await expect(validator.handle(representation)).resolves.toBeUndefined();
// Make sure the data can still be streamed
await expect(readableToString(representation.data)).resolves.toBe('data');
// Make sure the metadata was not changed
expect(quads).toBeRdfIsomorphic(representation.metadata.quads());
});
it('throws an error when validating invalid data.', async(): Promise<void> => {
converter.handleSafe = jest.fn().mockRejectedValue(new Error('bad data!'));
const representation = new BasicRepresentation('data', 'content-type');
await expect(validator.handle(representation)).rejects.toThrow('bad data!');
// Make sure the data on the readable has not been reset
expect(representation.data.destroyed).toBe(true);
});
});

View File

@@ -0,0 +1,66 @@
import type { AuxiliaryIdentifierStrategy } from '../../../../src/http/auxiliary/AuxiliaryIdentifierStrategy';
import { RoutingAuxiliaryIdentifierStrategy } from '../../../../src/http/auxiliary/RoutingAuxiliaryIdentifierStrategy';
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
class SimpleSuffixStrategy implements AuxiliaryIdentifierStrategy {
private readonly suffix: string;
public constructor(suffix: string) {
this.suffix = suffix;
}
public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return { path: `${identifier.path}${this.suffix}` };
}
public getAuxiliaryIdentifiers(identifier: ResourceIdentifier): ResourceIdentifier[] {
return [ this.getAuxiliaryIdentifier(identifier) ];
}
public isAuxiliaryIdentifier(identifier: ResourceIdentifier): boolean {
return identifier.path.endsWith(this.suffix);
}
public getSubjectIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return { path: identifier.path.slice(0, -this.suffix.length) };
}
}
describe('A RoutingAuxiliaryIdentifierStrategy', (): void => {
let sources: SimpleSuffixStrategy[];
let strategy: RoutingAuxiliaryIdentifierStrategy;
const baseId = { path: 'http://test.com/foo' };
const dummy1Id = { path: 'http://test.com/foo.dummy1' };
const dummy2Id = { path: 'http://test.com/foo.dummy2' };
const dummy3Id = { path: 'http://test.com/foo.dummy3' };
beforeEach(async(): Promise<void> => {
sources = [
new SimpleSuffixStrategy('.dummy1'),
new SimpleSuffixStrategy('.dummy2'),
];
strategy = new RoutingAuxiliaryIdentifierStrategy(sources);
});
it('#getAuxiliaryIdentifier always errors.', async(): Promise<void> => {
expect((): any => strategy.getAuxiliaryIdentifier()).toThrow(InternalServerError);
});
it('#getAuxiliaryIdentifiers returns results of all sources.', async(): Promise<void> => {
expect(strategy.getAuxiliaryIdentifiers(baseId)).toEqual([ dummy1Id, dummy2Id ]);
});
it('#isAuxiliaryIdentifier returns true if there is at least 1 match.', async(): Promise<void> => {
expect(strategy.isAuxiliaryIdentifier(dummy1Id)).toBe(true);
expect(strategy.isAuxiliaryIdentifier(dummy2Id)).toBe(true);
expect(strategy.isAuxiliaryIdentifier(dummy3Id)).toBe(false);
});
it('#getSubjectIdentifier returns the base id if a match is found.', async(): Promise<void> => {
expect(strategy.getSubjectIdentifier(dummy1Id)).toEqual(baseId);
expect(strategy.getSubjectIdentifier(dummy2Id)).toEqual(baseId);
expect((): any => strategy.getSubjectIdentifier(dummy3Id)).toThrow(NotImplementedHttpError);
});
});

View File

@@ -0,0 +1,119 @@
import type { AuxiliaryStrategy } from '../../../../src/http/auxiliary/AuxiliaryStrategy';
import { RoutingAuxiliaryStrategy } from '../../../../src/http/auxiliary/RoutingAuxiliaryStrategy';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
class SimpleSuffixStrategy implements AuxiliaryStrategy {
private readonly suffix: string;
public constructor(suffix: string) {
this.suffix = suffix;
}
public usesOwnAuthorization(): boolean {
return true;
}
public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return { path: `${identifier.path}${this.suffix}` };
}
public getAuxiliaryIdentifiers(identifier: ResourceIdentifier): ResourceIdentifier[] {
return [ this.getAuxiliaryIdentifier(identifier) ];
}
public isAuxiliaryIdentifier(identifier: ResourceIdentifier): boolean {
return identifier.path.endsWith(this.suffix);
}
public getSubjectIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return { path: identifier.path.slice(0, -this.suffix.length) };
}
public isRequiredInRoot(): boolean {
return true;
}
public async addMetadata(): Promise<void> {
// Empty fn
}
public async validate(): Promise<void> {
// Always validates
}
}
describe('A RoutingAuxiliaryStrategy', (): void => {
let sources: SimpleSuffixStrategy[];
let strategy: RoutingAuxiliaryStrategy;
const baseId = { path: 'http://test.com/foo' };
const dummy1Id = { path: 'http://test.com/foo.dummy1' };
const dummy2Id = { path: 'http://test.com/foo.dummy2' };
const dummy3Id = { path: 'http://test.com/foo.dummy3' };
beforeEach(async(): Promise<void> => {
sources = [
new SimpleSuffixStrategy('.dummy1'),
new SimpleSuffixStrategy('.dummy2'),
];
strategy = new RoutingAuxiliaryStrategy(sources);
});
it('#addMetadata adds the metadata of all sources for the base identifier.', async(): Promise<void> => {
sources[0].addMetadata = jest.fn();
sources[1].addMetadata = jest.fn();
const metadata = new RepresentationMetadata(baseId);
await expect(strategy.addMetadata(metadata)).resolves.toBeUndefined();
expect(sources[0].addMetadata).toHaveBeenCalledTimes(1);
expect(sources[0].addMetadata).toHaveBeenLastCalledWith(metadata);
expect(sources[1].addMetadata).toHaveBeenCalledTimes(1);
expect(sources[1].addMetadata).toHaveBeenLastCalledWith(metadata);
});
it('#addMetadata adds the metadata of the correct source for auxiliary identifiers.', async(): Promise<void> => {
sources[0].addMetadata = jest.fn();
sources[1].addMetadata = jest.fn();
const metadata = new RepresentationMetadata(dummy2Id);
await expect(strategy.addMetadata(metadata)).resolves.toBeUndefined();
expect(sources[0].addMetadata).toHaveBeenCalledTimes(0);
expect(sources[1].addMetadata).toHaveBeenCalledTimes(1);
expect(sources[1].addMetadata).toHaveBeenLastCalledWith(metadata);
});
it('#usesOwnAuthorization returns the result of the correct source.', async(): Promise<void> => {
sources[0].usesOwnAuthorization = jest.fn();
sources[1].usesOwnAuthorization = jest.fn();
strategy.usesOwnAuthorization(dummy2Id);
expect(sources[0].usesOwnAuthorization).toHaveBeenCalledTimes(0);
expect(sources[1].usesOwnAuthorization).toHaveBeenCalledTimes(1);
expect(sources[1].usesOwnAuthorization).toHaveBeenLastCalledWith(dummy2Id);
});
it('#isRequiredInRoot returns the result of the correct source.', async(): Promise<void> => {
sources[0].isRequiredInRoot = jest.fn();
sources[1].isRequiredInRoot = jest.fn();
strategy.isRequiredInRoot(dummy2Id);
expect(sources[0].isRequiredInRoot).toHaveBeenCalledTimes(0);
expect(sources[1].isRequiredInRoot).toHaveBeenCalledTimes(1);
expect(sources[1].isRequiredInRoot).toHaveBeenLastCalledWith(dummy2Id);
});
it('#validates using the correct validator.', async(): Promise<void> => {
sources[0].validate = jest.fn();
sources[1].validate = jest.fn();
let metadata = new RepresentationMetadata(dummy1Id);
await expect(strategy.validate({ metadata } as any)).resolves.toBeUndefined();
expect(sources[0].validate).toHaveBeenCalledTimes(1);
expect(sources[1].validate).toHaveBeenCalledTimes(0);
metadata = new RepresentationMetadata(dummy2Id);
await expect(strategy.validate({ metadata } as any)).resolves.toBeUndefined();
expect(sources[0].validate).toHaveBeenCalledTimes(1);
expect(sources[1].validate).toHaveBeenCalledTimes(1);
metadata = new RepresentationMetadata(dummy3Id);
await expect(strategy.validate({ metadata } as any)).rejects.toThrow(NotImplementedHttpError);
});
});

View File

@@ -0,0 +1,41 @@
import { SuffixAuxiliaryIdentifierStrategy } from '../../../../src/http/auxiliary/SuffixAuxiliaryIdentifierStrategy';
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
import 'jest-rdf';
const suffix = '.dummy';
describe('A SuffixAuxiliaryManager', (): void => {
let strategy: SuffixAuxiliaryIdentifierStrategy;
const subjectId: ResourceIdentifier = { path: 'http://test.com/foo' };
const auxiliaryId: ResourceIdentifier = { path: 'http://test.com/foo.dummy' };
beforeEach(async(): Promise<void> => {
strategy = new SuffixAuxiliaryIdentifierStrategy(suffix);
});
it('errors if the suffix is empty.', async(): Promise<void> => {
expect((): any => new SuffixAuxiliaryIdentifierStrategy('')).toThrow('Suffix length should be non-zero.');
});
it('creates new identifiers by appending the suffix.', async(): Promise<void> => {
expect(strategy.getAuxiliaryIdentifier(subjectId)).toEqual(auxiliaryId);
});
it('returns the same single identifier when requesting all of them.', async(): Promise<void> => {
expect(strategy.getAuxiliaryIdentifiers(subjectId)).toEqual([ auxiliaryId ]);
});
it('checks the suffix to determine if an identifier is auxiliary.', async(): Promise<void> => {
expect(strategy.isAuxiliaryIdentifier(subjectId)).toBe(false);
expect(strategy.isAuxiliaryIdentifier(auxiliaryId)).toBe(true);
});
it('errors when trying to get the subject id from a non-auxiliary identifier.', async(): Promise<void> => {
expect((): any => strategy.getSubjectIdentifier(subjectId)).toThrow(InternalServerError);
});
it('removes the suffix to create the subject identifier.', async(): Promise<void> => {
expect(strategy.getSubjectIdentifier(auxiliaryId)).toEqual(subjectId);
});
});

View File

@@ -0,0 +1,48 @@
import { BasicRequestParser } from '../../../../src/http/input/BasicRequestParser';
import type { BodyParser } from '../../../../src/http/input/body/BodyParser';
import type { ConditionsParser } from '../../../../src/http/input/conditions/ConditionsParser';
import type { TargetExtractor } from '../../../../src/http/input/identifier/TargetExtractor';
import type { MetadataParser } from '../../../../src/http/input/metadata/MetadataParser';
import type { PreferenceParser } from '../../../../src/http/input/preferences/PreferenceParser';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
describe('A BasicRequestParser', (): void => {
let targetExtractor: TargetExtractor;
let preferenceParser: PreferenceParser;
let metadataParser: MetadataParser;
let conditionsParser: ConditionsParser;
let bodyParser: BodyParser;
let requestParser: BasicRequestParser;
beforeEach(async(): Promise<void> => {
targetExtractor = new StaticAsyncHandler(true, 'target' as any);
preferenceParser = new StaticAsyncHandler(true, 'preference' as any);
metadataParser = new StaticAsyncHandler(true, undefined);
conditionsParser = new StaticAsyncHandler(true, 'conditions' as any);
bodyParser = new StaticAsyncHandler(true, 'body' as any);
requestParser = new BasicRequestParser(
{ targetExtractor, preferenceParser, metadataParser, conditionsParser, bodyParser },
);
});
it('can handle any input.', async(): Promise<void> => {
await expect(requestParser.canHandle({} as any)).resolves.toBeUndefined();
});
it('errors if there is no input.', async(): Promise<void> => {
await expect(requestParser.handle({ url: 'url' } as any))
.rejects.toThrow('No method specified on the HTTP request');
});
it('returns the output of all input parsers after calling handle.', async(): Promise<void> => {
bodyParser.handle = ({ metadata }): any => ({ data: 'body', metadata });
await expect(requestParser.handle({ url: 'url', method: 'GET' } as any)).resolves.toEqual({
method: 'GET',
target: 'target',
preferences: 'preference',
conditions: 'conditions',
body: { data: 'body', metadata: new RepresentationMetadata('target') },
});
});
});

View File

@@ -0,0 +1,73 @@
import 'jest-rdf';
import arrayifyStream from 'arrayify-stream';
import type { BodyParserArgs } from '../../../../../src/http/input/body/BodyParser';
import { RawBodyParser } from '../../../../../src/http/input/body/RawBodyParser';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
import { guardedStreamFrom } from '../../../../../src/util/StreamUtil';
describe('A RawBodyparser', (): void => {
const bodyParser = new RawBodyParser();
let input: BodyParserArgs;
beforeEach(async(): Promise<void> => {
input = { request: { headers: {}} as HttpRequest, metadata: new RepresentationMetadata() };
});
it('accepts all input.', async(): Promise<void> => {
await expect(bodyParser.canHandle({} as any)).resolves.toBeUndefined();
});
it('returns empty output if there is no content length or transfer encoding.', async(): Promise<void> => {
input.request = guardedStreamFrom([ '' ]) as HttpRequest;
input.request.headers = {};
await expect(bodyParser.handle(input)).resolves.toBeUndefined();
});
// https://github.com/solid/community-server/issues/498
it('returns empty output if the content length is 0 and there is no content type.', async(): Promise<void> => {
input.request = guardedStreamFrom([ '' ]) as HttpRequest;
input.request.headers = { 'content-length': '0' };
await expect(bodyParser.handle(input)).resolves.toBeUndefined();
});
it('errors when a content length is specified without content type.', async(): Promise<void> => {
input.request = guardedStreamFrom([ 'abc' ]) as HttpRequest;
input.request.headers = { 'content-length': '1' };
await expect(bodyParser.handle(input)).rejects
.toThrow('HTTP request body was passed without a Content-Type header');
});
it('errors when a transfer encoding is specified without content type.', async(): Promise<void> => {
input.request = guardedStreamFrom([ 'abc' ]) as HttpRequest;
input.request.headers = { 'transfer-encoding': 'chunked' };
await expect(bodyParser.handle(input)).rejects
.toThrow('HTTP request body was passed without a Content-Type header');
});
it('returns a Representation if there is empty data.', async(): Promise<void> => {
input.request = guardedStreamFrom([]) as HttpRequest;
input.request.headers = { 'content-length': '0', 'content-type': 'text/turtle' };
const result = (await bodyParser.handle(input))!;
expect(result).toEqual({
binary: true,
data: input.request,
metadata: input.metadata,
});
await expect(arrayifyStream(result.data)).resolves.toEqual([]);
});
it('returns a Representation if there is non-empty data.', async(): Promise<void> => {
input.request = guardedStreamFrom([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;
input.request.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle' };
const result = (await bodyParser.handle(input))!;
expect(result).toEqual({
binary: true,
data: input.request,
metadata: input.metadata,
});
await expect(arrayifyStream(result.data)).resolves.toEqual(
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],
);
});
});

View File

@@ -0,0 +1,88 @@
import 'jest-rdf';
import { namedNode, quad } from '@rdfjs/data-model';
import arrayifyStream from 'arrayify-stream';
import { Algebra } from 'sparqlalgebrajs';
import * as algebra from 'sparqlalgebrajs';
import type { BodyParserArgs } from '../../../../../src/http/input/body/BodyParser';
import { SparqlUpdateBodyParser } from '../../../../../src/http/input/body/SparqlUpdateBodyParser';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError';
import { UnsupportedMediaTypeHttpError } from '../../../../../src/util/errors/UnsupportedMediaTypeHttpError';
import { guardedStreamFrom } from '../../../../../src/util/StreamUtil';
describe('A SparqlUpdateBodyParser', (): void => {
const bodyParser = new SparqlUpdateBodyParser();
let input: BodyParserArgs;
beforeEach(async(): Promise<void> => {
input = { request: { headers: {}} as HttpRequest, metadata: new RepresentationMetadata() };
});
it('only accepts application/sparql-update content.', async(): Promise<void> => {
await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
input.metadata.contentType = 'text/plain';
await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
input.metadata.contentType = 'application/sparql-update;charset=utf-8';
await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
input.metadata.contentType = 'application/sparql-update ; foo=bar';
await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
input.metadata.contentType = 'application/sparql-update';
await expect(bodyParser.canHandle(input)).resolves.toBeUndefined();
});
it('errors when handling invalid SPARQL updates.', async(): Promise<void> => {
input.request = guardedStreamFrom([ 'VERY INVALID UPDATE' ]) as HttpRequest;
await expect(bodyParser.handle(input)).rejects.toThrow(BadRequestHttpError);
});
it('errors when receiving an unexpected error.', async(): Promise<void> => {
const mock = jest.spyOn(algebra, 'translate').mockImplementationOnce((): any => {
throw 'apple';
});
input.request = guardedStreamFrom(
[ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o> }' ],
) as HttpRequest;
await expect(bodyParser.handle(input)).rejects.toThrow(BadRequestHttpError);
mock.mockRestore();
});
it('converts SPARQL updates to algebra.', async(): Promise<void> => {
input.request = guardedStreamFrom(
[ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o> }' ],
) as HttpRequest;
const result = await bodyParser.handle(input);
expect(result.algebra.type).toBe(Algebra.types.DELETE_INSERT);
expect((result.algebra as Algebra.DeleteInsert).delete).toBeRdfIsomorphic([ quad(
namedNode('http://test.com/s'),
namedNode('http://test.com/p'),
namedNode('http://test.com/o'),
) ]);
expect(result.binary).toBe(true);
expect(result.metadata).toBe(input.metadata);
expect(await arrayifyStream(result.data)).toEqual(
[ 'DELETE DATA { <http://test.com/s> <http://test.com/p> <http://test.com/o> }' ],
);
});
it('accepts relative references.', async(): Promise<void> => {
input.request = guardedStreamFrom(
[ 'INSERT DATA { <#it> <http://test.com/p> <http://test.com/o> }' ],
) as HttpRequest;
input.metadata.identifier = namedNode('http://test.com/my-document.ttl');
const result = await bodyParser.handle(input);
expect(result.algebra.type).toBe(Algebra.types.DELETE_INSERT);
expect((result.algebra as Algebra.DeleteInsert).insert).toBeRdfIsomorphic([ quad(
namedNode('http://test.com/my-document.ttl#it'),
namedNode('http://test.com/p'),
namedNode('http://test.com/o'),
) ]);
expect(result.binary).toBe(true);
expect(result.metadata).toBe(input.metadata);
expect(await arrayifyStream(result.data)).toEqual(
[ 'INSERT DATA { <#it> <http://test.com/p> <http://test.com/o> }' ],
);
});
});

View File

@@ -0,0 +1,60 @@
import { BasicConditionsParser } from '../../../../../src/http/input/conditions/BasicConditionsParser';
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
describe('A BasicConditionsParser', (): void => {
const dateString = 'Wed, 21 Oct 2015 07:28:00 UTC';
const date = new Date('2015-10-21T07:28:00.000Z');
let request: HttpRequest;
const parser = new BasicConditionsParser();
beforeEach(async(): Promise<void> => {
request = { headers: {}, method: 'GET' } as HttpRequest;
});
it('returns undefined if there are no relevant headers.', async(): Promise<void> => {
await expect(parser.handleSafe(request)).resolves.toBeUndefined();
});
it('parses the if-modified-since header.', async(): Promise<void> => {
request.headers['if-modified-since'] = dateString;
await expect(parser.handleSafe(request)).resolves.toEqual({ modifiedSince: date });
});
it('parses the if-unmodified-since header.', async(): Promise<void> => {
request.headers['if-unmodified-since'] = dateString;
await expect(parser.handleSafe(request)).resolves.toEqual({ unmodifiedSince: date });
});
it('parses the if-match header.', async(): Promise<void> => {
request.headers['if-match'] = '"1234567", "abcdefg"';
await expect(parser.handleSafe(request)).resolves.toEqual({ matchesETag: [ '"1234567"', '"abcdefg"' ]});
});
it('parses the if-none-match header.', async(): Promise<void> => {
request.headers['if-none-match'] = '*';
await expect(parser.handleSafe(request)).resolves.toEqual({ notMatchesETag: [ '*' ]});
});
it('does not parse the if-modified-since header if there is an if-none-match header.', async(): Promise<void> => {
request.headers['if-modified-since'] = dateString;
request.headers['if-none-match'] = '*';
await expect(parser.handleSafe(request)).resolves.toEqual({ notMatchesETag: [ '*' ]});
});
it('only parses the if-modified-since header for GET and HEAD requests.', async(): Promise<void> => {
request.headers['if-modified-since'] = dateString;
request.method = 'PUT';
await expect(parser.handleSafe(request)).resolves.toBeUndefined();
});
it('does not parse the if-unmodified-since header if there is an if-match header.', async(): Promise<void> => {
request.headers['if-unmodified-since'] = dateString;
request.headers['if-match'] = '*';
await expect(parser.handleSafe(request)).resolves.toEqual({ matchesETag: [ '*' ]});
});
it('ignores invalid dates.', async(): Promise<void> => {
request.headers['if-modified-since'] = 'notADate';
await expect(parser.handleSafe(request)).resolves.toBeUndefined();
});
});

View File

@@ -0,0 +1,133 @@
import { OriginalUrlExtractor } from '../../../../../src/http/input/identifier/OriginalUrlExtractor';
describe('A OriginalUrlExtractor', (): void => {
const extractor = new OriginalUrlExtractor();
it('can handle any input.', async(): Promise<void> => {
await expect(extractor.canHandle({} as any)).resolves.toBeUndefined();
});
it('errors if there is no URL.', async(): Promise<void> => {
await expect(extractor.handle({ request: { headers: { host: 'test.com' }} as any })).rejects.toThrow('Missing URL');
});
it('errors if there is no host.', async(): Promise<void> => {
await expect(extractor.handle({ request: { url: 'url', headers: {}} as any }))
.rejects.toThrow('Missing Host header');
});
it('errors if the host is invalid.', async(): Promise<void> => {
await expect(extractor.handle({ request: { url: 'url', headers: { host: 'test.com/forbidden' }} as any }))
.rejects.toThrow('The request has an invalid Host header: test.com/forbidden');
});
it('returns the input URL.', async(): Promise<void> => {
await expect(extractor.handle({ request: { url: 'url', headers: { host: 'test.com' }} as any }))
.resolves.toEqual({ path: 'http://test.com/url' });
});
it('returns an input URL with query string.', async(): Promise<void> => {
const noQuery = new OriginalUrlExtractor({ includeQueryString: false });
await expect(noQuery.handle({ request: { url: '/url?abc=def&xyz', headers: { host: 'test.com' }} as any }))
.resolves.toEqual({ path: 'http://test.com/url' });
});
it('drops the query string when includeQueryString is set to false.', async(): Promise<void> => {
await expect(extractor.handle({ request: { url: '/url?abc=def&xyz', headers: { host: 'test.com' }} as any }))
.resolves.toEqual({ path: 'http://test.com/url?abc=def&xyz' });
});
it('supports host:port combinations.', async(): Promise<void> => {
await expect(extractor.handle({ request: { url: 'url', headers: { host: 'localhost:3000' }} as any }))
.resolves.toEqual({ path: 'http://localhost:3000/url' });
});
it('uses https protocol if the connection is secure.', async(): Promise<void> => {
await expect(extractor.handle(
{ request: { url: 'url', headers: { host: 'test.com' }, connection: { encrypted: true } as any } as any },
)).resolves.toEqual({ path: 'https://test.com/url' });
});
it('encodes paths.', async(): Promise<void> => {
await expect(extractor.handle({ request: { url: '/a%20path%26/name', headers: { host: 'test.com' }} as any }))
.resolves.toEqual({ path: 'http://test.com/a%20path%26/name' });
await expect(extractor.handle({ request: { url: '/a path%26/name', headers: { host: 'test.com' }} as any }))
.resolves.toEqual({ path: 'http://test.com/a%20path%26/name' });
await expect(extractor.handle({ request: { url: '/path&%26/name', headers: { host: 'test.com' }} as any }))
.resolves.toEqual({ path: 'http://test.com/path%26%26/name' });
});
it('encodes hosts.', async(): Promise<void> => {
await expect(extractor.handle({ request: { url: '/', headers: { host: '點看' }} as any }))
.resolves.toEqual({ path: 'http://xn--c1yn36f/' });
});
it('ignores an irrelevant Forwarded header.', async(): Promise<void> => {
const headers = {
host: 'test.com',
forwarded: 'by=203.0.113.60',
};
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
.resolves.toEqual({ path: 'http://test.com/foo/bar' });
});
it('takes the Forwarded header into account.', async(): Promise<void> => {
const headers = {
host: 'test.com',
forwarded: 'proto=https;host=pod.example',
};
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
.resolves.toEqual({ path: 'https://pod.example/foo/bar' });
});
it('should fallback to x-fowarded-* headers.', async(): Promise<void> => {
const headers = {
host: 'test.com',
'x-forwarded-host': 'pod.example',
'x-forwarded-proto': 'https',
};
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
.resolves.toEqual({ path: 'https://pod.example/foo/bar' });
});
it('should just take x-forwarded-host if provided.', async(): Promise<void> => {
const headers = {
host: 'test.com',
'x-forwarded-host': 'pod.example',
};
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
.resolves.toEqual({ path: 'http://pod.example/foo/bar' });
});
it('should just take x-forwarded-protocol if provided.', async(): Promise<void> => {
const headers = {
host: 'test.com',
'x-forwarded-proto': 'https',
};
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
.resolves.toEqual({ path: 'https://test.com/foo/bar' });
});
it('should prefer forwarded header to x-forwarded-* headers.', async(): Promise<void> => {
const headers = {
host: 'test.com',
forwarded: 'proto=http;host=pod.example',
'x-forwarded-proto': 'https',
'x-forwarded-host': 'anotherpod.example',
};
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
.resolves.toEqual({ path: 'http://pod.example/foo/bar' });
});
it('should just take the first x-forwarded-* value.', async(): Promise<void> => {
const headers = {
host: 'test.com',
'x-forwarded-host': 'pod.example, another.domain',
'x-forwarded-proto': 'http,https',
};
await expect(extractor.handle({ request: { url: '/foo/bar', headers } as any }))
.resolves.toEqual({ path: 'http://pod.example/foo/bar' });
});
});

View File

@@ -0,0 +1,26 @@
import { ContentTypeParser } from '../../../../../src/http/input/metadata/ContentTypeParser';
import { RepresentationMetadata } from '../../../../../src/http/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.handle({ 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.handle({ request, metadata })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(1);
expect(metadata.contentType).toBe('text/plain');
});
});

View File

@@ -0,0 +1,55 @@
import { LinkRelParser } from '../../../../../src/http/input/metadata/LinkRelParser';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
import { RDF } from '../../../../../src/util/Vocabularies';
describe('A LinkParser', (): void => {
const parser = new LinkRelParser({ type: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' });
let request: HttpRequest;
let metadata: RepresentationMetadata;
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.handle({ request, metadata });
expect(metadata.quads()).toHaveLength(0);
});
it('stores link headers with rel matching the given value as metadata.', async(): Promise<void> => {
request.headers.link = '<http://test.com/type>;rel="type"';
await expect(parser.handle({ 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.handle({ 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.handle({ 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.handle({ 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.handle({ request, metadata })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
});
});

View File

@@ -0,0 +1,35 @@
import { SlugParser } from '../../../../../src/http/input/metadata/SlugParser';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError';
import { SOLID_HTTP } from '../../../../../src/util/Vocabularies';
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.handle({ 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' ];
const result = parser.handle({ request, metadata });
await expect(result).rejects.toThrow(BadRequestHttpError);
await expect(result).rejects.toThrow('Request has multiple Slug headers');
});
it('stores the slug metadata.', async(): Promise<void> => {
request.headers.slug = 'slugA';
await expect(parser.handle({ request, metadata })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(1);
expect(metadata.get(SOLID_HTTP.slug)?.value).toBe('slugA');
});
});

View File

@@ -0,0 +1,52 @@
import { AcceptPreferenceParser } from '../../../../../src/http/input/preferences/AcceptPreferenceParser';
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
describe('An AcceptPreferenceParser', (): void => {
const preferenceParser = new AcceptPreferenceParser();
let request: HttpRequest;
beforeEach(async(): Promise<void> => {
request = { headers: {}} as HttpRequest;
});
it('can handle all input.', async(): Promise<void> => {
await expect(preferenceParser.canHandle({ request })).resolves.toBeUndefined();
});
it('returns an empty result if there is no relevant input.', async(): Promise<void> => {
await expect(preferenceParser.handle({ request })).resolves.toEqual({});
request.headers = { accept: '' };
await expect(preferenceParser.handle({ request })).resolves.toEqual({});
});
it('parses accept headers.', async(): Promise<void> => {
request.headers = { accept: 'audio/*; q=0.2, audio/basic' };
await expect(preferenceParser.handle({ request }))
.resolves.toEqual({ type: { 'audio/basic': 1, 'audio/*': 0.2 }});
});
it('parses accept-charset headers.', async(): Promise<void> => {
request.headers = { 'accept-charset': 'iso-8859-5, unicode-1-1;q=0.8' };
await expect(preferenceParser.handle({ request }))
.resolves.toEqual({ charset: { 'iso-8859-5': 1, 'unicode-1-1': 0.8 }});
});
it('parses accept-datetime headers.', async(): Promise<void> => {
request.headers = { 'accept-datetime': 'Tue, 20 Mar 2001 20:35:00 GMT' };
await expect(preferenceParser.handle({ request }))
// eslint-disable-next-line @typescript-eslint/naming-convention
.resolves.toEqual({ datetime: { 'Tue, 20 Mar 2001 20:35:00 GMT': 1 }});
});
it('parses accept-encoding headers.', async(): Promise<void> => {
request.headers = { 'accept-encoding': 'gzip;q=1.0, identity; q=0.5, *;q=0' };
await expect(preferenceParser.handle({ request }))
.resolves.toEqual({ encoding: { gzip: 1, identity: 0.5, '*': 0 }});
});
it('parses accept-language headers.', async(): Promise<void> => {
request.headers = { 'accept-language': 'da, en-gb;q=0.8, en;q=0.7' };
await expect(preferenceParser.handle({ request }))
.resolves.toEqual({ language: { da: 1, 'en-gb': 0.8, en: 0.7 }});
});
});

View File

@@ -0,0 +1,31 @@
import { DeleteOperationHandler } from '../../../../src/http/ldp/DeleteOperationHandler';
import type { Operation } from '../../../../src/http/Operation';
import { BasicConditions } from '../../../../src/storage/BasicConditions';
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
describe('A DeleteOperationHandler', (): void => {
let operation: Operation;
const conditions = new BasicConditions({});
const store = {} as unknown as ResourceStore;
const handler = new DeleteOperationHandler(store);
beforeEach(async(): Promise<void> => {
operation = { method: 'DELETE', target: { path: 'http://test.com/foo' }, preferences: {}, conditions };
store.deleteResource = jest.fn(async(): Promise<any> => undefined);
});
it('only supports DELETE operations.', async(): Promise<void> => {
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
operation.method = 'GET';
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
});
it('deletes the resource from the store and returns the correct response.', async(): Promise<void> => {
const result = await handler.handle({ operation });
expect(store.deleteResource).toHaveBeenCalledTimes(1);
expect(store.deleteResource).toHaveBeenLastCalledWith(operation.target, conditions);
expect(result.statusCode).toBe(205);
expect(result.metadata).toBeUndefined();
expect(result.data).toBeUndefined();
});
});

View File

@@ -0,0 +1,39 @@
import { GetOperationHandler } from '../../../../src/http/ldp/GetOperationHandler';
import type { Operation } from '../../../../src/http/Operation';
import type { Representation } from '../../../../src/http/representation/Representation';
import { BasicConditions } from '../../../../src/storage/BasicConditions';
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
describe('A GetOperationHandler', (): void => {
let operation: Operation;
const conditions = new BasicConditions({});
const preferences = {};
let store: ResourceStore;
let handler: GetOperationHandler;
beforeEach(async(): Promise<void> => {
operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences, conditions };
store = {
getRepresentation: jest.fn(async(): Promise<Representation> =>
({ binary: false, data: 'data', metadata: 'metadata' } as any)),
} as unknown as ResourceStore;
handler = new GetOperationHandler(store);
});
it('only supports GET operations.', async(): Promise<void> => {
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
operation.method = 'POST';
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
});
it('returns the representation from the store with the correct response.', async(): Promise<void> => {
const result = await handler.handle({ operation });
expect(result.statusCode).toBe(200);
expect(result.metadata).toBe('metadata');
expect(result.data).toBe('data');
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
expect(store.getRepresentation).toHaveBeenLastCalledWith(operation.target, preferences, conditions);
});
});

View File

@@ -0,0 +1,45 @@
import type { Readable } from 'stream';
import { HeadOperationHandler } from '../../../../src/http/ldp/HeadOperationHandler';
import type { Operation } from '../../../../src/http/Operation';
import type { Representation } from '../../../../src/http/representation/Representation';
import { BasicConditions } from '../../../../src/storage/BasicConditions';
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
describe('A HeadOperationHandler', (): void => {
let operation: Operation;
const conditions = new BasicConditions({});
const preferences = {};
let store: ResourceStore;
let handler: HeadOperationHandler;
let data: Readable;
beforeEach(async(): Promise<void> => {
operation = { method: 'HEAD', target: { path: 'http://test.com/foo' }, preferences, conditions };
data = { destroy: jest.fn() } as any;
store = {
getRepresentation: jest.fn(async(): Promise<Representation> =>
({ binary: false, data, metadata: 'metadata' } as any)),
} as any;
handler = new HeadOperationHandler(store);
});
it('only supports HEAD operations.', async(): Promise<void> => {
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
operation.method = 'GET';
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
operation.method = 'POST';
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
});
it('returns the representation from the store with the correct response.', async(): Promise<void> => {
const result = await handler.handle({ operation });
expect(result.statusCode).toBe(200);
expect(result.metadata).toBe('metadata');
expect(result.data).toBeUndefined();
expect(data.destroy).toHaveBeenCalledTimes(1);
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
expect(store.getRepresentation).toHaveBeenLastCalledWith(operation.target, preferences, conditions);
});
});

View File

@@ -0,0 +1,43 @@
import { PatchOperationHandler } from '../../../../src/http/ldp/PatchOperationHandler';
import type { Operation } from '../../../../src/http/Operation';
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import type { Representation } from '../../../../src/http/representation/Representation';
import { BasicConditions } from '../../../../src/storage/BasicConditions';
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
describe('A PatchOperationHandler', (): void => {
let operation: Operation;
let body: Representation;
const conditions = new BasicConditions({});
const store = {} as unknown as ResourceStore;
const handler = new PatchOperationHandler(store);
beforeEach(async(): Promise<void> => {
body = new BasicRepresentation('', 'text/turtle');
operation = { method: 'PATCH', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}};
store.modifyResource = jest.fn(async(): Promise<any> => undefined);
});
it('only supports PATCH operations.', async(): Promise<void> => {
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
operation.method = 'GET';
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
});
it('errors if there is no body or content-type.', async(): Promise<void> => {
operation.body!.metadata.contentType = undefined;
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
delete operation.body;
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
});
it('deletes the resource from the store and returns the correct response.', async(): Promise<void> => {
const result = await handler.handle({ operation });
expect(store.modifyResource).toHaveBeenCalledTimes(1);
expect(store.modifyResource).toHaveBeenLastCalledWith(operation.target, body, conditions);
expect(result.statusCode).toBe(205);
expect(result.metadata).toBeUndefined();
expect(result.data).toBeUndefined();
});
});

View File

@@ -0,0 +1,51 @@
import { PostOperationHandler } from '../../../../src/http/ldp/PostOperationHandler';
import type { Operation } from '../../../../src/http/Operation';
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import type { Representation } from '../../../../src/http/representation/Representation';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
import { BasicConditions } from '../../../../src/storage/BasicConditions';
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
import { SOLID_HTTP } from '../../../../src/util/Vocabularies';
describe('A PostOperationHandler', (): void => {
let operation: Operation;
let body: Representation;
const conditions = new BasicConditions({});
let store: ResourceStore;
let handler: PostOperationHandler;
beforeEach(async(): Promise<void> => {
body = new BasicRepresentation('', 'text/turtle');
operation = { method: 'POST', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}};
store = {
addResource: jest.fn(async(): Promise<ResourceIdentifier> => ({ path: 'newPath' } as ResourceIdentifier)),
} as unknown as ResourceStore;
handler = new PostOperationHandler(store);
});
it('only supports POST operations.', async(): Promise<void> => {
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
operation.method = 'GET';
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
});
it('errors if there is no body or content-type.', async(): Promise<void> => {
operation.body!.metadata.contentType = undefined;
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
delete operation.body;
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
});
it('adds the given representation to the store and returns the correct response.', async(): Promise<void> => {
const result = await handler.handle({ operation });
expect(result.statusCode).toBe(201);
expect(result.metadata).toBeInstanceOf(RepresentationMetadata);
expect(result.metadata?.get(SOLID_HTTP.location)?.value).toBe('newPath');
expect(result.data).toBeUndefined();
expect(store.addResource).toHaveBeenCalledTimes(1);
expect(store.addResource).toHaveBeenLastCalledWith(operation.target, body, conditions);
});
});

View File

@@ -0,0 +1,44 @@
import { PutOperationHandler } from '../../../../src/http/ldp/PutOperationHandler';
import type { Operation } from '../../../../src/http/Operation';
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import type { Representation } from '../../../../src/http/representation/Representation';
import { BasicConditions } from '../../../../src/storage/BasicConditions';
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
describe('A PutOperationHandler', (): void => {
let operation: Operation;
let body: Representation;
const conditions = new BasicConditions({});
const store = {} as unknown as ResourceStore;
const handler = new PutOperationHandler(store);
beforeEach(async(): Promise<void> => {
body = new BasicRepresentation('', 'text/turtle');
operation = { method: 'PUT', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}};
// eslint-disable-next-line @typescript-eslint/no-empty-function
store.setRepresentation = jest.fn(async(): Promise<any> => {});
});
it('only supports PUT operations.', async(): Promise<void> => {
await expect(handler.canHandle({ operation })).resolves.toBeUndefined();
operation.method = 'GET';
await expect(handler.canHandle({ operation })).rejects.toThrow(NotImplementedHttpError);
});
it('errors if there is no body or content-type.', async(): Promise<void> => {
operation.body!.metadata.contentType = undefined;
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
delete operation.body;
await expect(handler.handle({ operation })).rejects.toThrow(BadRequestHttpError);
});
it('sets the representation in the store and returns the correct response.', async(): Promise<void> => {
const result = await handler.handle({ operation });
expect(store.setRepresentation).toHaveBeenCalledTimes(1);
expect(store.setRepresentation).toHaveBeenLastCalledWith(operation.target, body, conditions);
expect(result.statusCode).toBe(205);
expect(result.metadata).toBeUndefined();
expect(result.data).toBeUndefined();
});
});

View File

@@ -0,0 +1,61 @@
import 'jest-rdf';
import { CredentialGroup } from '../../../../../src/authentication/Credentials';
import type { AclPermission } from '../../../../../src/authorization/permissions/AclPermission';
import { WebAclMetadataCollector } from '../../../../../src/http/ldp/metadata/WebAclMetadataCollector';
import type { Operation } from '../../../../../src/http/Operation';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import { ACL, AUTH } from '../../../../../src/util/Vocabularies';
describe('A WebAclMetadataCollector', (): void => {
let operation: Operation;
let metadata: RepresentationMetadata;
const writer = new WebAclMetadataCollector();
beforeEach(async(): Promise<void> => {
operation = {
method: 'GET',
target: { path: 'http://test.com/foo' },
preferences: {},
};
metadata = new RepresentationMetadata();
});
it('adds no metadata if there are no permissions.', async(): Promise<void> => {
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
operation.permissionSet = {};
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
});
it('adds no metadata if the method is wrong.', async(): Promise<void> => {
operation.permissionSet = { [CredentialGroup.public]: { read: true, write: false }};
operation.method = 'DELETE';
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
});
it('adds corresponding metadata for all permissions present.', async(): Promise<void> => {
operation.permissionSet = {
[CredentialGroup.agent]: { read: true, write: true, control: false } as AclPermission,
[CredentialGroup.public]: { read: true, write: false },
};
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(3);
expect(metadata.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read, ACL.terms.Write ]);
expect(metadata.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read);
});
it('ignores unknown modes.', async(): Promise<void> => {
operation.permissionSet = {
[CredentialGroup.agent]: { read: true, create: true },
[CredentialGroup.public]: { read: true },
};
await expect(writer.handle({ metadata, operation })).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(2);
expect(metadata.getAll(AUTH.terms.userMode)).toEqualRdfTermArray([ ACL.terms.Read ]);
expect(metadata.get(AUTH.terms.publicMode)).toEqualRdfTerm(ACL.terms.Read);
});
});

View File

@@ -0,0 +1,92 @@
import { EventEmitter } from 'events';
import { PassThrough } from 'stream';
import type { MockResponse } from 'node-mocks-http';
import { createResponse } from 'node-mocks-http';
import { BasicResponseWriter } from '../../../../src/http/output/BasicResponseWriter';
import type { MetadataWriter } from '../../../../src/http/output/metadata/MetadataWriter';
import type { ResponseDescription } from '../../../../src/http/output/response/ResponseDescription';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
describe('A BasicResponseWriter', (): void => {
let metadataWriter: MetadataWriter;
let writer: BasicResponseWriter;
let response: MockResponse<any>;
let result: ResponseDescription;
beforeEach(async(): Promise<void> => {
metadataWriter = new StaticAsyncHandler(true, undefined);
writer = new BasicResponseWriter(metadataWriter);
response = createResponse({ eventEmitter: EventEmitter });
result = { statusCode: 201 };
});
it('requires the input to be a binary ResponseDescription.', async(): Promise<void> => {
const metadata = new RepresentationMetadata(INTERNAL_QUADS);
await expect(writer.canHandle({ response, result: { statusCode: 201, metadata }}))
.rejects.toThrow(NotImplementedHttpError);
metadata.contentType = 'text/turtle';
await expect(writer.canHandle({ response, result: { statusCode: 201, metadata }}))
.resolves.toBeUndefined();
await expect(writer.canHandle({ response, result: { statusCode: 201 }}))
.resolves.toBeUndefined();
});
it('responds with the status code of the ResponseDescription.', async(): Promise<void> => {
await writer.handle({ response, result });
expect(response._isEndCalled()).toBeTruthy();
expect(response._getStatusCode()).toBe(201);
});
it('responds with a body if the description has a body.', async(): Promise<void> => {
const data = guardedStreamFrom([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]);
result = { statusCode: 201, data };
const end = new Promise<void>((resolve): void => {
response.on('end', (): void => {
expect(response._isEndCalled()).toBeTruthy();
expect(response._getStatusCode()).toBe(201);
expect(response._getData()).toEqual('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
resolve();
});
});
await writer.handle({ response, result });
await end;
});
it('serializes metadata if there is metadata.', async(): Promise<void> => {
result = { statusCode: 201, metadata: new RepresentationMetadata() };
metadataWriter.handle = jest.fn();
await writer.handle({ response, result });
expect(metadataWriter.handle).toHaveBeenCalledTimes(1);
expect(metadataWriter.handle).toHaveBeenLastCalledWith({ response, metadata: result.metadata });
expect(response._isEndCalled()).toBeTruthy();
expect(response._getStatusCode()).toBe(201);
});
it('can handle the data stream erroring.', async(): Promise<void> => {
const data = guardedStreamFrom([]);
data.read = (): any => {
data.emit('error', new Error('bad data!'));
return null;
};
result = { statusCode: 201, data };
response = new PassThrough();
response.writeHead = jest.fn();
const end = new Promise<void>((resolve): void => {
response.on('error', (error: Error): void => {
expect(error).toEqual(new Error('bad data!'));
resolve();
});
});
await expect(writer.handle({ response, result })).resolves.toBeUndefined();
await end;
});
});

View File

@@ -0,0 +1,95 @@
import 'jest-rdf';
import arrayifyStream from 'arrayify-stream';
import { DataFactory } from 'n3';
import { ConvertingErrorHandler } from '../../../../../src/http/output/error/ConvertingErrorHandler';
import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation';
import type { Representation } from '../../../../../src/http/representation/Representation';
import type { RepresentationPreferences } from '../../../../../src/http/representation/RepresentationPreferences';
import type {
RepresentationConverter,
RepresentationConverterArgs,
} from '../../../../../src/storage/conversion/RepresentationConverter';
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
import { HTTP, XSD } from '../../../../../src/util/Vocabularies';
import literal = DataFactory.literal;
const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
async function expectValidArgs(args: RepresentationConverterArgs, stack?: string): Promise<void> {
expect(args.preferences).toBe(preferences);
expect(args.representation.metadata.get(HTTP.terms.statusCodeNumber))
.toEqualRdfTerm(literal(404, XSD.terms.integer));
expect(args.representation.metadata.contentType).toBe('internal/error');
// Error contents
const errorArray = await arrayifyStream(args.representation.data);
expect(errorArray).toHaveLength(1);
const resultError = errorArray[0];
expect(resultError).toMatchObject({ name: 'NotFoundHttpError', message: 'not here' });
expect(resultError.stack).toBe(stack);
}
describe('A ConvertingErrorHandler', (): void => {
// The error object can get modified by the handler
let error: Error;
let stack: string | undefined;
let converter: RepresentationConverter;
let handler: ConvertingErrorHandler;
beforeEach(async(): Promise<void> => {
error = new NotFoundHttpError('not here');
({ stack } = error);
converter = {
canHandle: jest.fn(),
handle: jest.fn((): Representation => new BasicRepresentation('serialization', 'text/turtle', true)),
handleSafe: jest.fn((): Representation => new BasicRepresentation('serialization', 'text/turtle', true)),
} as any;
handler = new ConvertingErrorHandler(converter, true);
});
it('rejects input not supported by the converter.', async(): Promise<void> => {
(converter.canHandle as jest.Mock).mockRejectedValueOnce(new Error('rejected'));
await expect(handler.canHandle({ error, preferences })).rejects.toThrow('rejected');
expect(converter.canHandle).toHaveBeenCalledTimes(1);
const args = (converter.canHandle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
expect(args.preferences).toBe(preferences);
expect(args.representation.metadata.contentType).toBe('internal/error');
});
it('accepts input supported by the converter.', async(): Promise<void> => {
await expect(handler.canHandle({ error, preferences })).resolves.toBeUndefined();
expect(converter.canHandle).toHaveBeenCalledTimes(1);
const args = (converter.canHandle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
expect(args.preferences).toBe(preferences);
expect(args.representation.metadata.contentType).toBe('internal/error');
});
it('returns the converted error response.', async(): Promise<void> => {
const prom = handler.handle({ error, preferences });
await expect(prom).resolves.toMatchObject({ statusCode: 404 });
expect((await prom).metadata?.contentType).toBe('text/turtle');
expect(converter.handle).toHaveBeenCalledTimes(1);
const args = (converter.handle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
await expectValidArgs(args, stack);
});
it('uses the handleSafe function of the converter during its own handleSafe call.', async(): Promise<void> => {
const prom = handler.handleSafe({ error, preferences });
await expect(prom).resolves.toMatchObject({ statusCode: 404 });
expect((await prom).metadata?.contentType).toBe('text/turtle');
expect(converter.handleSafe).toHaveBeenCalledTimes(1);
const args = (converter.handleSafe as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
await expectValidArgs(args, stack);
});
it('hides the stack trace if the option is disabled.', async(): Promise<void> => {
handler = new ConvertingErrorHandler(converter);
const prom = handler.handle({ error, preferences });
await expect(prom).resolves.toMatchObject({ statusCode: 404 });
expect((await prom).metadata?.contentType).toBe('text/turtle');
expect(converter.handle).toHaveBeenCalledTimes(1);
const args = (converter.handle as jest.Mock).mock.calls[0][0] as RepresentationConverterArgs;
await expectValidArgs(args);
});
});

View File

@@ -0,0 +1,77 @@
import 'jest-rdf';
import { DataFactory } from 'n3';
import type { ErrorHandler } from '../../../../../src/http/output/error/ErrorHandler';
import { SafeErrorHandler } from '../../../../../src/http/output/error/SafeErrorHandler';
import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation';
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
import { readableToString } from '../../../../../src/util/StreamUtil';
import { HTTP, XSD } from '../../../../../src/util/Vocabularies';
import literal = DataFactory.literal;
describe('A SafeErrorHandler', (): void => {
let error: Error;
let stack: string | undefined;
let errorHandler: jest.Mocked<ErrorHandler>;
let handler: SafeErrorHandler;
beforeEach(async(): Promise<void> => {
error = new NotFoundHttpError('not here');
({ stack } = error);
errorHandler = {
handleSafe: jest.fn().mockResolvedValue(new BasicRepresentation('<html>fancy error</html>', 'text/html')),
} as any;
handler = new SafeErrorHandler(errorHandler, true);
});
it('can handle everything.', async(): Promise<void> => {
await expect(handler.canHandle({} as any)).resolves.toBeUndefined();
});
it('sends the request to the stored error handler.', async(): Promise<void> => {
const prom = handler.handle({ error } as any);
await expect(prom).resolves.toBeDefined();
const result = await prom;
expect(result.metadata?.contentType).toBe('text/html');
await expect(readableToString(result.data!)).resolves.toBe('<html>fancy error</html>');
});
describe('where the wrapped error handler fails', (): void => {
beforeEach(async(): Promise<void> => {
errorHandler.handleSafe.mockRejectedValue(new Error('handler failed'));
});
it('creates a text representation of the error.', async(): Promise<void> => {
const prom = handler.handle({ error } as any);
await expect(prom).resolves.toBeDefined();
const result = await prom;
expect(result.statusCode).toBe(404);
expect(result.metadata?.get(HTTP.terms.statusCodeNumber)).toEqualRdfTerm(literal(404, XSD.terms.integer));
expect(result.metadata?.contentType).toBe('text/plain');
await expect(readableToString(result.data!)).resolves.toBe(`${stack}\n`);
});
it('concatenates name and message if there is no stack.', async(): Promise<void> => {
delete error.stack;
const prom = handler.handle({ error } as any);
await expect(prom).resolves.toBeDefined();
const result = await prom;
expect(result.statusCode).toBe(404);
expect(result.metadata?.get(HTTP.terms.statusCodeNumber)).toEqualRdfTerm(literal(404, XSD.terms.integer));
expect(result.metadata?.contentType).toBe('text/plain');
await expect(readableToString(result.data!)).resolves.toBe(`NotFoundHttpError: not here\n`);
});
it('hides the stack trace if the option is disabled.', async(): Promise<void> => {
handler = new SafeErrorHandler(errorHandler);
const prom = handler.handle({ error } as any);
await expect(prom).resolves.toBeDefined();
const result = await prom;
expect(result.statusCode).toBe(404);
expect(result.metadata?.get(HTTP.terms.statusCodeNumber)).toEqualRdfTerm(literal(404, XSD.terms.integer));
expect(result.metadata?.contentType).toBe('text/plain');
await expect(readableToString(result.data!)).resolves.toBe(`NotFoundHttpError: not here\n`);
});
});
});

View File

@@ -0,0 +1,24 @@
import { createResponse } from 'node-mocks-http';
import { ConstantMetadataWriter } from '../../../../../src/http/output/metadata/ConstantMetadataWriter';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
describe('A ConstantMetadataWriter', (): void => {
const writer = new ConstantMetadataWriter({ 'custom-Header': 'X', other: 'Y' });
it('adds new headers.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
await expect(writer.handle({ response })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ 'custom-header': 'X', other: 'Y' });
});
it('extends existing headers.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
response.setHeader('Other', 'A');
await expect(writer.handle({ response })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ 'custom-header': 'X', other: [ 'A', 'Y' ]});
});
});

View File

@@ -0,0 +1,16 @@
import { createResponse } from 'node-mocks-http';
import { LinkRelMetadataWriter } from '../../../../../src/http/output/metadata/LinkRelMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { LDP, RDF } from '../../../../../src/util/Vocabularies';
describe('A LinkRelMetadataWriter', (): void => {
const writer = new LinkRelMetadataWriter({ [RDF.type]: 'type', dummy: 'dummy' });
it('adds the correct link headers.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
const metadata = new RepresentationMetadata({ [RDF.type]: LDP.terms.Resource, unused: 'text' });
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ link: `<${LDP.Resource}>; rel="type"` });
});
});

View File

@@ -0,0 +1,16 @@
import { createResponse } from 'node-mocks-http';
import { MappedMetadataWriter } from '../../../../../src/http/output/metadata/MappedMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { CONTENT_TYPE } from '../../../../../src/util/Vocabularies';
describe('A MappedMetadataWriter', (): void => {
const writer = new MappedMetadataWriter({ [CONTENT_TYPE]: 'content-type', dummy: 'dummy' });
it('adds metadata to the corresponding header.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle', unused: 'text' });
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ 'content-type': 'text/turtle' });
});
});

View File

@@ -0,0 +1,29 @@
import { createResponse } from 'node-mocks-http';
import { ModifiedMetadataWriter } from '../../../../../src/http/output/metadata/ModifiedMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { updateModifiedDate } from '../../../../../src/util/ResourceUtil';
import { DC } from '../../../../../src/util/Vocabularies';
describe('A ModifiedMetadataWriter', (): void => {
const writer = new ModifiedMetadataWriter();
it('adds the Last-Modified and ETag header if there is dc:modified metadata.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
const metadata = new RepresentationMetadata();
updateModifiedDate(metadata);
const dateTime = metadata.get(DC.terms.modified)!.value;
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'last-modified': new Date(dateTime).toUTCString(),
etag: `"${new Date(dateTime).getTime()}"`,
});
});
it('does nothing if there is no matching metadata.', async(): Promise<void> => {
const response = createResponse() as HttpResponse;
const metadata = new RepresentationMetadata();
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({});
});
});

View File

@@ -0,0 +1,54 @@
import { createResponse } from 'node-mocks-http';
import { WacAllowMetadataWriter } from '../../../../../src/http/output/metadata/WacAllowMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { ACL, AUTH } from '../../../../../src/util/Vocabularies';
describe('A WacAllowMetadataWriter', (): void => {
const writer = new WacAllowMetadataWriter();
let response: HttpResponse;
beforeEach(async(): Promise<void> => {
response = createResponse() as HttpResponse;
});
it('adds no header if there is no relevant metadata.', async(): Promise<void> => {
const metadata = new RepresentationMetadata();
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ });
});
it('adds a WAC-Allow header if there is relevant metadata.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({
[AUTH.userMode]: [ ACL.terms.Read, ACL.terms.Write ],
[AUTH.publicMode]: [ ACL.terms.Read ],
});
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'wac-allow': 'user="read write",public="read"',
});
});
it('only adds a header value for entries with at least 1 permission.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({
[AUTH.userMode]: [ ACL.terms.Read, ACL.terms.Write ],
});
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'wac-allow': 'user="read write"',
});
});
it('applies public modes to user modes.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({
[AUTH.publicMode]: [ ACL.terms.Read, ACL.terms.Write ],
});
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'wac-allow': 'user="read write",public="read write"',
});
});
});

View File

@@ -0,0 +1,36 @@
import { createResponse } from 'node-mocks-http';
import { WwwAuthMetadataWriter } from '../../../../../src/http/output/metadata/WwwAuthMetadataWriter';
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
import { HTTP } from '../../../../../src/util/Vocabularies';
describe('A WwwAuthMetadataWriter', (): void => {
const auth = 'Bearer scope="openid webid"';
const writer = new WwwAuthMetadataWriter(auth);
let response: HttpResponse;
beforeEach(async(): Promise<void> => {
response = createResponse() as HttpResponse;
});
it('adds no header if there is no relevant metadata.', async(): Promise<void> => {
const metadata = new RepresentationMetadata();
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ });
});
it('adds no header if the status code is not 401.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({ [HTTP.statusCodeNumber]: '403' });
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({ });
});
it('adds a WWW-Authenticate header if the status code is 401.', async(): Promise<void> => {
const metadata = new RepresentationMetadata({ [HTTP.statusCodeNumber]: '401' });
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
expect(response.getHeaders()).toEqual({
'www-authenticate': auth,
});
});
});

View File

@@ -0,0 +1,18 @@
import { RedirectResponseDescription } from '../../../../../src/http/output/response/RedirectResponseDescription';
import { SOLID_HTTP } from '../../../../../src/util/Vocabularies';
describe('A RedirectResponseDescription', (): void => {
const location = 'http://test.com/foo';
it('has status code 302 and a location.', async(): Promise<void> => {
const description = new RedirectResponseDescription(location);
expect(description.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(location);
expect(description.statusCode).toBe(302);
});
it('has status code 301 if the change is permanent.', async(): Promise<void> => {
const description = new RedirectResponseDescription(location, true);
expect(description.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(location);
expect(description.statusCode).toBe(301);
});
});

View File

@@ -0,0 +1,118 @@
import 'jest-rdf';
import { Readable } from 'stream';
import { namedNode } from '@rdfjs/data-model';
import arrayifyStream from 'arrayify-stream';
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
import { guardedStreamFrom } from '../../../../src/util/StreamUtil';
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
describe('BasicRepresentation', (): void => {
it('creates a representation with (data, metadata, binary).', (): void => {
const data = guardedStreamFrom([ '' ]);
const metadata = new RepresentationMetadata();
const representation = new BasicRepresentation(data, metadata, true);
expect(representation.data).toBe(data);
expect(representation.metadata).toBe(metadata);
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, metadata).', (): void => {
const data = guardedStreamFrom([ '' ]);
let metadata = new RepresentationMetadata();
let representation = new BasicRepresentation(data, metadata);
expect(representation.data).toBe(data);
expect(representation.metadata).toBe(metadata);
expect(representation.binary).toBe(true);
metadata = new RepresentationMetadata(INTERNAL_QUADS);
representation = new BasicRepresentation(data, metadata);
expect(representation.data).toBe(data);
expect(representation.metadata).toBe(metadata);
expect(representation.binary).toBe(false);
});
it('creates a representation with (unguarded data, metadata).', (): void => {
const data = Readable.from([ '' ]);
const metadata = new RepresentationMetadata();
const representation = new BasicRepresentation(data, metadata);
expect(representation.data).toBe(data);
expect(representation.metadata).toBe(metadata);
expect(representation.binary).toBe(true);
});
it('creates a representation with (array data, metadata).', async(): Promise<void> => {
const data = [ 'my', 'data' ];
const metadata = new RepresentationMetadata();
const representation = new BasicRepresentation(data, metadata);
expect(await arrayifyStream(representation.data)).toEqual(data);
expect(representation.metadata).toBe(metadata);
expect(representation.binary).toBe(true);
});
it('creates a representation with (string data, metadata).', async(): Promise<void> => {
const data = 'my data';
const metadata = new RepresentationMetadata();
const representation = new BasicRepresentation(data, metadata);
expect(await arrayifyStream(representation.data)).toEqual([ data ]);
expect(representation.metadata).toBe(metadata);
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, metadata record).', (): void => {
const data = guardedStreamFrom([ '' ]);
const representation = new BasicRepresentation(data, { [CONTENT_TYPE]: 'text/custom' });
expect(representation.data).toBe(data);
expect(representation.metadata.contentType).toBe('text/custom');
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, content type).', (): void => {
const data = guardedStreamFrom([ '' ]);
const representation = new BasicRepresentation(data, 'text/custom');
expect(representation.data).toBe(data);
expect(representation.metadata.contentType).toBe('text/custom');
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, identifier, metadata record).', (): void => {
const identifier = { path: 'http://example.org/#' };
const data = guardedStreamFrom([ '' ]);
const representation = new BasicRepresentation(data, identifier, { [CONTENT_TYPE]: 'text/custom' });
expect(representation.data).toBe(data);
expect(representation.metadata.identifier).toEqualRdfTerm(namedNode(identifier.path));
expect(representation.metadata.contentType).toBe('text/custom');
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, identifier, content type).', (): void => {
const identifier = { path: 'http://example.org/#' };
const data = guardedStreamFrom([ '' ]);
const representation = new BasicRepresentation(data, identifier, 'text/custom');
expect(representation.data).toBe(data);
expect(representation.metadata.identifier).toEqualRdfTerm(namedNode(identifier.path));
expect(representation.metadata.contentType).toBe('text/custom');
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, identifier term, metadata record).', (): void => {
const identifier = namedNode('http://example.org/#');
const data = guardedStreamFrom([ '' ]);
const representation = new BasicRepresentation(data, identifier, { [CONTENT_TYPE]: 'text/custom' });
expect(representation.data).toBe(data);
expect(representation.metadata.identifier).toBe(identifier);
expect(representation.metadata.contentType).toBe('text/custom');
expect(representation.binary).toBe(true);
});
it('creates a representation with (data, identifier term, content type).', (): void => {
const identifier = namedNode('http://example.org/#');
const data = guardedStreamFrom([ '' ]);
const representation = new BasicRepresentation(data, identifier, 'text/custom');
expect(representation.data).toBe(data);
expect(representation.metadata.identifier).toBe(identifier);
expect(representation.metadata.contentType).toBe('text/custom');
expect(representation.binary).toBe(true);
});
});

View File

@@ -0,0 +1,289 @@
import 'jest-rdf';
import { defaultGraph, literal, namedNode, quad } from '@rdfjs/data-model';
import type { NamedNode, Quad } from 'rdf-js';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
// Helper functions to filter quads
function getQuads(quads: Quad[], subject?: string, predicate?: string, object?: string, graph?: string): Quad[] {
return quads.filter((qq): boolean =>
(!subject || qq.subject.value === subject) &&
(!predicate || qq.predicate.value === predicate) &&
(!object || qq.object.value === object) &&
(!graph || qq.graph.value === graph));
}
function removeQuads(quads: Quad[], subject?: string, predicate?: string, object?: string, graph?: string): Quad[] {
const filtered = getQuads(quads, subject, predicate, object, graph);
return quads.filter((qq): boolean => !filtered.includes(qq));
}
describe('A RepresentationMetadata', (): void => {
let metadata: RepresentationMetadata;
const identifier = namedNode('http://example.com/id');
const graphNode = namedNode('http://graph');
const inputQuads = [
quad(identifier, namedNode('has'), literal('data')),
quad(identifier, namedNode('has'), literal('moreData')),
quad(identifier, namedNode('hasOne'), literal('otherData')),
quad(identifier, namedNode('has'), literal('data'), graphNode),
quad(namedNode('otherNode'), namedNode('linksTo'), identifier),
quad(namedNode('otherNode'), namedNode('has'), literal('otherData')),
quad(namedNode('otherNode'), namedNode('graphData'), literal('otherData'), graphNode),
];
describe('constructor', (): void => {
it('creates a blank node if no identifier was given.', async(): Promise<void> => {
metadata = new RepresentationMetadata();
expect(metadata.identifier.termType).toEqual('BlankNode');
expect(metadata.quads()).toHaveLength(0);
});
it('stores the given identifier if given.', async(): Promise<void> => {
metadata = new RepresentationMetadata(namedNode('identifier'));
expect(metadata.identifier).toEqualRdfTerm(namedNode('identifier'));
});
it('converts identifiers to named nodes.', async(): Promise<void> => {
metadata = new RepresentationMetadata({ path: 'identifier' });
expect(metadata.identifier).toEqualRdfTerm(namedNode('identifier'));
});
it('converts string to content type.', async(): Promise<void> => {
metadata = new RepresentationMetadata('text/turtle');
expect(metadata.contentType).toEqual('text/turtle');
metadata = new RepresentationMetadata({ path: 'identifier' }, 'text/turtle');
expect(metadata.contentType).toEqual('text/turtle');
metadata = new RepresentationMetadata(new RepresentationMetadata(), 'text/turtle');
expect(metadata.contentType).toEqual('text/turtle');
});
it('copies an other metadata object.', async(): Promise<void> => {
const other = new RepresentationMetadata({ path: 'otherId' }, { 'test:pred': 'objVal' });
metadata = new RepresentationMetadata(other);
expect(metadata.identifier).toEqualRdfTerm(namedNode('otherId'));
expect(metadata.quads()).toBeRdfIsomorphic([
quad(namedNode('otherId'), namedNode('test:pred'), literal('objVal')) ]);
});
it('takes overrides for specific predicates.', async(): Promise<void> => {
metadata = new RepresentationMetadata({ predVal: 'objVal' });
expect(metadata.get('predVal')).toEqualRdfTerm(literal('objVal'));
metadata = new RepresentationMetadata({ predVal: literal('objVal') });
expect(metadata.get('predVal')).toEqualRdfTerm(literal('objVal'));
metadata = new RepresentationMetadata({ predVal: [ 'objVal1', literal('objVal2') ], predVal2: 'objVal3' });
expect(metadata.getAll('predVal')).toEqualRdfTermArray([ literal('objVal1'), literal('objVal2') ]);
expect(metadata.get('predVal2')).toEqualRdfTerm(literal('objVal3'));
});
it('can combine overrides with an identifier.', async(): Promise<void> => {
metadata = new RepresentationMetadata(identifier, { predVal: 'objVal' });
expect(metadata.quads()).toBeRdfIsomorphic([
quad(identifier, namedNode('predVal'), literal('objVal')) ]);
});
it('can combine overrides with other metadata.', async(): Promise<void> => {
const other = new RepresentationMetadata({ path: 'otherId' }, { 'test:pred': 'objVal' });
metadata = new RepresentationMetadata(other, { 'test:pred': 'objVal2' });
expect(metadata.quads()).toBeRdfIsomorphic([
quad(namedNode('otherId'), namedNode('test:pred'), literal('objVal2')) ]);
});
});
describe('instantiated', (): void => {
beforeEach(async(): Promise<void> => {
metadata = new RepresentationMetadata(identifier).addQuads(inputQuads);
});
it('can get all quads.', async(): Promise<void> => {
expect(metadata.quads()).toBeRdfIsomorphic(inputQuads);
});
it('can query quads.', async(): Promise<void> => {
expect(metadata.quads(null, namedNode('has'))).toHaveLength(getQuads(inputQuads, undefined, 'has').length);
expect(metadata.quads(null, null, literal('otherData')))
.toHaveLength(getQuads(inputQuads, undefined, undefined, 'otherData').length);
});
it('can change the stored identifier.', async(): Promise<void> => {
const newIdentifier = namedNode('newNode');
metadata.identifier = newIdentifier;
const newQuads = inputQuads.map((triple): Quad => {
if (triple.subject.equals(identifier)) {
return quad(newIdentifier, triple.predicate, triple.object, triple.graph);
}
if (triple.object.equals(identifier)) {
return quad(triple.subject, triple.predicate, newIdentifier, triple.graph);
}
return triple;
});
expect(metadata.identifier).toEqualRdfTerm(newIdentifier);
expect(metadata.quads()).toBeRdfIsomorphic(newQuads);
});
it('can copy metadata.', async(): Promise<void> => {
const other = new RepresentationMetadata(identifier, { 'test:pred': 'objVal' });
metadata.setMetadata(other);
expect(metadata.identifier).toEqual(other.identifier);
expect(metadata.quads()).toBeRdfIsomorphic([ ...inputQuads,
quad(identifier, namedNode('test:pred'), literal('objVal')) ]);
});
it('updates its identifier when copying metadata.', async(): Promise<void> => {
const other = new RepresentationMetadata({ path: 'otherId' }, { 'test:pred': 'objVal' });
metadata.setMetadata(other);
// `setMetadata` should have the same result as the following
const expectedMetadata = new RepresentationMetadata(identifier).addQuads(inputQuads);
expectedMetadata.identifier = namedNode('otherId');
expectedMetadata.add('test:pred', 'objVal');
expect(metadata.identifier).toEqual(other.identifier);
expect(metadata.quads()).toBeRdfIsomorphic(expectedMetadata.quads());
});
it('can add a quad.', async(): Promise<void> => {
const newQuad = quad(namedNode('random'), namedNode('new'), literal('triple'));
metadata.addQuad('random', 'new', 'triple');
expect(metadata.quads()).toBeRdfIsomorphic([ ...inputQuads, newQuad ]);
});
it('can add a quad with a graph.', async(): Promise<void> => {
const newQuad = quad(namedNode('random'), namedNode('new'), literal('triple'), namedNode('graph'));
metadata.addQuad('random', 'new', 'triple', 'graph');
expect(metadata.quads()).toBeRdfIsomorphic([ ...inputQuads, newQuad ]);
});
it('can add quads.', async(): Promise<void> => {
const newQuads: Quad[] = [
quad(namedNode('random'), namedNode('new'), namedNode('triple')),
];
metadata.addQuads(newQuads);
expect(metadata.quads()).toBeRdfIsomorphic([ ...newQuads, ...inputQuads ]);
});
it('can remove a quad.', async(): Promise<void> => {
const old = inputQuads[0];
metadata.removeQuad(old.subject as any, old.predicate as any, old.object as any, old.graph as any);
expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.slice(1));
});
it('removes all matching triples if graph is undefined.', async(): Promise<void> => {
metadata.removeQuad(identifier, 'has', 'data');
expect(metadata.quads()).toHaveLength(inputQuads.length - 2);
expect(metadata.quads()).toBeRdfIsomorphic(removeQuads(inputQuads, identifier.value, 'has', 'data'));
});
it('can remove quads.', async(): Promise<void> => {
metadata.removeQuads([ inputQuads[0] ]);
expect(metadata.quads()).toBeRdfIsomorphic(inputQuads.slice(1));
});
it('can add a single value for a predicate.', async(): Promise<void> => {
const newQuad = quad(identifier, namedNode('new'), namedNode('triple'));
metadata.add(newQuad.predicate as NamedNode, newQuad.object as NamedNode);
expect(metadata.quads()).toBeRdfIsomorphic([ newQuad, ...inputQuads ]);
});
it('can add single values as string.', async(): Promise<void> => {
const newQuad = quad(identifier, namedNode('new'), literal('triple'));
metadata.add(newQuad.predicate as NamedNode, newQuad.object.value);
expect(metadata.quads()).toBeRdfIsomorphic([ newQuad, ...inputQuads ]);
});
it('can add multiple values for a predicate.', async(): Promise<void> => {
const newQuads = [
quad(identifier, namedNode('new'), namedNode('triple')),
quad(identifier, namedNode('new'), namedNode('triple2')),
];
metadata.add(namedNode('new'), [ namedNode('triple'), namedNode('triple2') ]);
expect(metadata.quads()).toBeRdfIsomorphic([ ...newQuads, ...inputQuads ]);
});
it('can remove a single value for a predicate.', async(): Promise<void> => {
metadata.remove(namedNode('has'), literal('data'));
expect(metadata.quads()).toBeRdfIsomorphic(removeQuads(inputQuads, identifier.value, 'has', 'data'));
});
it('can remove single values as string.', async(): Promise<void> => {
metadata.remove(namedNode('has'), 'data');
expect(metadata.quads()).toBeRdfIsomorphic(removeQuads(inputQuads, identifier.value, 'has', 'data'));
});
it('can remove multiple values for a predicate.', async(): Promise<void> => {
metadata.remove(namedNode('has'), [ literal('data'), 'moreData' ]);
let expected = removeQuads(inputQuads, identifier.value, 'has', 'data');
expected = removeQuads(expected, identifier.value, 'has', 'moreData');
expect(metadata.quads()).toBeRdfIsomorphic(expected);
});
it('can remove all values for a predicate.', async(): Promise<void> => {
const pred = namedNode('has');
metadata.removeAll(pred);
expect(metadata.quads()).toBeRdfIsomorphic(removeQuads(inputQuads, identifier.value, 'has'));
});
it('can remove all values for a predicate in a specific graph.', async(): Promise<void> => {
const pred = namedNode('has');
metadata.removeAll(pred, graphNode);
expect(metadata.quads()).toBeRdfIsomorphic(
removeQuads(inputQuads, identifier.value, 'has', undefined, graphNode.value),
);
});
it('can get all values for a predicate.', async(): Promise<void> => {
expect(metadata.getAll(namedNode('has'))).toEqualRdfTermArray(
[ literal('data'), literal('moreData'), literal('data') ],
);
});
it('can get all values for a predicate in a graph.', async(): Promise<void> => {
expect(metadata.getAll(namedNode('has'), defaultGraph())).toEqualRdfTermArray(
[ literal('data'), literal('moreData') ],
);
});
it('can get the single value for a predicate.', async(): Promise<void> => {
expect(metadata.get(namedNode('hasOne'))).toEqualRdfTerm(literal('otherData'));
});
it('returns undefined if getting an undefined predicate.', async(): Promise<void> => {
expect(metadata.get(namedNode('doesntExist'))).toBeUndefined();
});
it('errors if there are multiple values when getting a value.', async(): Promise<void> => {
expect((): any => metadata.get(namedNode('has'))).toThrow(Error);
expect((): any => metadata.get('has')).toThrow(Error);
});
it('can set the value of a predicate.', async(): Promise<void> => {
metadata.set(namedNode('has'), literal('singleValue'));
expect(metadata.get(namedNode('has'))).toEqualRdfTerm(literal('singleValue'));
});
it('can set multiple values of a predicate.', async(): Promise<void> => {
metadata.set(namedNode('has'), [ literal('value1'), literal('value2') ]);
expect(metadata.getAll(namedNode('has'))).toEqualRdfTermArray([ literal('value1'), literal('value2') ]);
});
it('has a shorthand for content-type.', async(): Promise<void> => {
expect(metadata.contentType).toBeUndefined();
metadata.contentType = 'a/b';
expect(metadata.get(CONTENT_TYPE)).toEqualRdfTerm(literal('a/b'));
expect(metadata.contentType).toEqual('a/b');
metadata.contentType = undefined;
expect(metadata.contentType).toBeUndefined();
});
it('errors if a shorthand has multiple values.', async(): Promise<void> => {
metadata.add(CONTENT_TYPE, 'a/b');
metadata.add(CONTENT_TYPE, 'c/d');
expect((): any => metadata.contentType).toThrow();
});
});
});