mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
refactor: Restructure source code folder
This way the location of certain classes should make more sense
This commit is contained in:
201
test/unit/http/UnsecureWebSocketsProtocol.test.ts
Normal file
201
test/unit/http/UnsecureWebSocketsProtocol.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
84
test/unit/http/auxiliary/ComposedAuxiliaryStrategy.test.ts
Normal file
84
test/unit/http/auxiliary/ComposedAuxiliaryStrategy.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
41
test/unit/http/auxiliary/LinkMetadataGenerator.test.ts
Normal file
41
test/unit/http/auxiliary/LinkMetadataGenerator.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
44
test/unit/http/auxiliary/RdfValidator.test.ts
Normal file
44
test/unit/http/auxiliary/RdfValidator.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
119
test/unit/http/auxiliary/RoutingAuxiliaryStrategy.test.ts
Normal file
119
test/unit/http/auxiliary/RoutingAuxiliaryStrategy.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
48
test/unit/http/input/BasicRequestParser.test.ts
Normal file
48
test/unit/http/input/BasicRequestParser.test.ts
Normal 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') },
|
||||
});
|
||||
});
|
||||
});
|
||||
73
test/unit/http/input/body/RawBodyParser.test.ts
Normal file
73
test/unit/http/input/body/RawBodyParser.test.ts
Normal 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>.' ],
|
||||
);
|
||||
});
|
||||
});
|
||||
88
test/unit/http/input/body/SparqlUpdateBodyParser.test.ts
Normal file
88
test/unit/http/input/body/SparqlUpdateBodyParser.test.ts
Normal 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> }' ],
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
133
test/unit/http/input/identifier/OriginalUrlExtractor.test.ts
Normal file
133
test/unit/http/input/identifier/OriginalUrlExtractor.test.ts
Normal 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' });
|
||||
});
|
||||
});
|
||||
26
test/unit/http/input/metadata/ContentTypeParser.test.ts
Normal file
26
test/unit/http/input/metadata/ContentTypeParser.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
55
test/unit/http/input/metadata/LinkParser.test.ts
Normal file
55
test/unit/http/input/metadata/LinkParser.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
35
test/unit/http/input/metadata/SlugParser.test.ts
Normal file
35
test/unit/http/input/metadata/SlugParser.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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 }});
|
||||
});
|
||||
});
|
||||
31
test/unit/http/ldp/DeleteOperationHandler.test.ts
Normal file
31
test/unit/http/ldp/DeleteOperationHandler.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
39
test/unit/http/ldp/GetOperationHandler.test.ts
Normal file
39
test/unit/http/ldp/GetOperationHandler.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
45
test/unit/http/ldp/HeadOperationHandler.test.ts
Normal file
45
test/unit/http/ldp/HeadOperationHandler.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
43
test/unit/http/ldp/PatchOperationHandler.test.ts
Normal file
43
test/unit/http/ldp/PatchOperationHandler.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
51
test/unit/http/ldp/PostOperationHandler.test.ts
Normal file
51
test/unit/http/ldp/PostOperationHandler.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
44
test/unit/http/ldp/PutOperationHandler.test.ts
Normal file
44
test/unit/http/ldp/PutOperationHandler.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
61
test/unit/http/ldp/metadata/WebAclMetadataCollector.test.ts
Normal file
61
test/unit/http/ldp/metadata/WebAclMetadataCollector.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
92
test/unit/http/output/BasicResponseWriter.test.ts
Normal file
92
test/unit/http/output/BasicResponseWriter.test.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
95
test/unit/http/output/error/ConvertingErrorHandler.test.ts
Normal file
95
test/unit/http/output/error/ConvertingErrorHandler.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
77
test/unit/http/output/error/SafeErrorHandler.test.ts
Normal file
77
test/unit/http/output/error/SafeErrorHandler.test.ts
Normal 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`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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' ]});
|
||||
});
|
||||
});
|
||||
16
test/unit/http/output/metadata/LinkRelMetadataWriter.test.ts
Normal file
16
test/unit/http/output/metadata/LinkRelMetadataWriter.test.ts
Normal 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"` });
|
||||
});
|
||||
});
|
||||
16
test/unit/http/output/metadata/MappedMetadataWriter.test.ts
Normal file
16
test/unit/http/output/metadata/MappedMetadataWriter.test.ts
Normal 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' });
|
||||
});
|
||||
});
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
@@ -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"',
|
||||
});
|
||||
});
|
||||
});
|
||||
36
test/unit/http/output/metadata/WwwAuthMetadataWriter.test.ts
Normal file
36
test/unit/http/output/metadata/WwwAuthMetadataWriter.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
118
test/unit/http/representation/BasicRepresentation.test.ts
Normal file
118
test/unit/http/representation/BasicRepresentation.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
289
test/unit/http/representation/RepresentationMetadata.test.ts
Normal file
289
test/unit/http/representation/RepresentationMetadata.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user