mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Introduce generic auxiliary resource support
This commit is contained in:
parent
766e6318ba
commit
d6cdd7dbdf
12
src/index.ts
12
src/index.ts
@ -22,6 +22,18 @@ export * from './init/LoggerInitializer';
|
||||
export * from './init/RootContainerInitializer';
|
||||
export * from './init/ServerInitializer';
|
||||
|
||||
// LDP/Authorization
|
||||
export * from './ldp/auxiliary/AuxiliaryIdentifierStrategy';
|
||||
export * from './ldp/auxiliary/AuxiliaryStrategy';
|
||||
export * from './ldp/auxiliary/ComposedAuxiliaryStrategy';
|
||||
export * from './ldp/auxiliary/LinkMetadataGenerator';
|
||||
export * from './ldp/auxiliary/MetadataGenerator';
|
||||
export * from './ldp/auxiliary/RdfValidator';
|
||||
export * from './ldp/auxiliary/RoutingAuxiliaryIdentifierStrategy';
|
||||
export * from './ldp/auxiliary/RoutingAuxiliaryStrategy';
|
||||
export * from './ldp/auxiliary/SuffixAuxiliaryIdentifierStrategy';
|
||||
export * from './ldp/auxiliary/Validator';
|
||||
|
||||
// LDP/HTTP/Metadata
|
||||
export * from './ldp/http/metadata/AclLinkMetadataWriter';
|
||||
export * from './ldp/http/metadata/BasicMetadataExtractor';
|
||||
|
48
src/ldp/auxiliary/AuxiliaryIdentifierStrategy.ts
Normal file
48
src/ldp/auxiliary/AuxiliaryIdentifierStrategy.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||
|
||||
/**
|
||||
* A strategy for handling auxiliary related ResourceIdentifiers.
|
||||
*/
|
||||
export interface AuxiliaryIdentifierStrategy {
|
||||
/**
|
||||
* Returns the identifier of the auxiliary resource corresponding to the given resource.
|
||||
* This does not guarantee that this auxiliary resource exists.
|
||||
*
|
||||
* Should error if there are multiple results: see {@link getAuxiliaryIdentifiers}.
|
||||
*
|
||||
* @param identifier - The ResourceIdentifier of which we need the corresponding auxiliary resource.
|
||||
*
|
||||
* @returns The ResourceIdentifier of the corresponding auxiliary resource.
|
||||
*/
|
||||
getAuxiliaryIdentifier: (identifier: ResourceIdentifier) => ResourceIdentifier;
|
||||
|
||||
/**
|
||||
* Returns all the identifiers of corresponding auxiliary resources.
|
||||
* This can be used when there are potentially multiple results.
|
||||
* In the case of a single result this should be an array containing the result of {@link getAuxiliaryIdentifier}.
|
||||
*
|
||||
* @param identifier - The ResourceIdentifier of which we need the corresponding auxiliary resources.
|
||||
*
|
||||
* @returns The ResourceIdentifiers of the corresponding auxiliary resources.
|
||||
*/
|
||||
getAuxiliaryIdentifiers: (identifier: ResourceIdentifier) => ResourceIdentifier[];
|
||||
|
||||
/**
|
||||
* Checks if the input identifier corresponds to an auxiliary resource.
|
||||
* This does not check if that auxiliary resource exists,
|
||||
* only if the identifier indicates that there could be an auxiliary resource there.
|
||||
* @param identifier - Identifier to check.
|
||||
*
|
||||
* @returns true if the input identifier points to an auxiliary resource.
|
||||
*/
|
||||
isAuxiliaryIdentifier: (identifier: ResourceIdentifier) => boolean;
|
||||
|
||||
/**
|
||||
* Returns the identifier of the resource which this auxiliary resource is referring to.
|
||||
* This does not guarantee that this resource exists.
|
||||
* @param identifier - Identifier of the auxiliary resource.
|
||||
*
|
||||
* @returns The ResourceIdentifier of the corresponding resource.
|
||||
*/
|
||||
getAssociatedIdentifier: (identifier: ResourceIdentifier) => ResourceIdentifier;
|
||||
}
|
42
src/ldp/auxiliary/AuxiliaryStrategy.ts
Normal file
42
src/ldp/auxiliary/AuxiliaryStrategy.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { Representation } from '../representation/Representation';
|
||||
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
|
||||
|
||||
/**
|
||||
* A strategy for handling one or more types of auxiliary resources.
|
||||
* References to "an auxiliary resource" implicitly imply a specific type of auxiliary resources
|
||||
* supported by this strategy.
|
||||
*/
|
||||
export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
|
||||
/**
|
||||
* Whether this auxiliary resource can be deleted when it's in a root storage container.
|
||||
* @param identifier - Identifier of the auxiliary resource.
|
||||
*/
|
||||
requiresRootAuxiliary: (identifier: ResourceIdentifier) => boolean;
|
||||
|
||||
/**
|
||||
* Adds metadata related to this auxiliary resource,
|
||||
* in case this is required for this type of auxiliary resource.
|
||||
* The metadata that is added depends on the given identifier being an auxiliary or associated resource:
|
||||
* the metadata will be used to link to the other one, and potentially add extra typing info.
|
||||
*
|
||||
* Used for:
|
||||
* Solid, §4.3.1: "For any defined auxiliary resource available for a given Solid resource, all representations of
|
||||
* that resource MUST include an HTTP Link header pointing to the location of each auxiliary resource."
|
||||
* https://solid.github.io/specification/protocol#auxiliary-resources-server
|
||||
*
|
||||
* The above is an example of how that metadata would only be added in case the input is the associated identifier.
|
||||
*
|
||||
* @param metadata - Metadata to update.
|
||||
*/
|
||||
addMetadata: (metadata: RepresentationMetadata) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Validates if the representation contains valid data for an auxiliary resource.
|
||||
* Should throw an error in case the data is invalid.
|
||||
* @param identifier - Identifier of the auxiliary resource.
|
||||
* @param representation - Representation of the auxiliary resource.
|
||||
*/
|
||||
validate: (representation: Representation) => Promise<void>;
|
||||
}
|
58
src/ldp/auxiliary/ComposedAuxiliaryStrategy.ts
Normal file
58
src/ldp/auxiliary/ComposedAuxiliaryStrategy.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import type { Representation } from '../representation/Representation';
|
||||
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
|
||||
import type { AuxiliaryStrategy } from './AuxiliaryStrategy';
|
||||
import type { MetadataGenerator } from './MetadataGenerator';
|
||||
import type { Validator } from './Validator';
|
||||
|
||||
/**
|
||||
* An {@link AuxiliaryStrategy} that provides its functionality through the combination of
|
||||
* an {@link AuxiliaryIdentifierStrategy}, {@link MetadataGenerator} and {@link Validator}.
|
||||
*/
|
||||
export class ComposedAuxiliaryStrategy implements AuxiliaryStrategy {
|
||||
private readonly identifierStrategy: AuxiliaryIdentifierStrategy;
|
||||
private readonly metadataGenerator?: MetadataGenerator;
|
||||
private readonly validator?: Validator;
|
||||
private readonly canDelete: boolean;
|
||||
|
||||
public constructor(identifierStrategy: AuxiliaryIdentifierStrategy, metadataGenerator?: MetadataGenerator,
|
||||
validator?: Validator, canDeleteRoot = false) {
|
||||
this.identifierStrategy = identifierStrategy;
|
||||
this.metadataGenerator = metadataGenerator;
|
||||
this.validator = validator;
|
||||
this.canDelete = canDeleteRoot;
|
||||
}
|
||||
|
||||
public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
|
||||
return this.identifierStrategy.getAuxiliaryIdentifier(identifier);
|
||||
}
|
||||
|
||||
public getAuxiliaryIdentifiers(identifier: ResourceIdentifier): ResourceIdentifier[] {
|
||||
return this.identifierStrategy.getAuxiliaryIdentifiers(identifier);
|
||||
}
|
||||
|
||||
public isAuxiliaryIdentifier(identifier: ResourceIdentifier): boolean {
|
||||
return this.identifierStrategy.isAuxiliaryIdentifier(identifier);
|
||||
}
|
||||
|
||||
public getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
|
||||
return this.identifierStrategy.getAssociatedIdentifier(identifier);
|
||||
}
|
||||
|
||||
public requiresRootAuxiliary(): boolean {
|
||||
return this.canDelete;
|
||||
}
|
||||
|
||||
public async addMetadata(metadata: RepresentationMetadata): Promise<void> {
|
||||
if (this.metadataGenerator) {
|
||||
return this.metadataGenerator.handleSafe(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
public async validate(representation: Representation): Promise<void> {
|
||||
if (this.validator) {
|
||||
return this.validator.handleSafe(representation);
|
||||
}
|
||||
}
|
||||
}
|
28
src/ldp/auxiliary/LinkMetadataGenerator.ts
Normal file
28
src/ldp/auxiliary/LinkMetadataGenerator.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { namedNode } from '@rdfjs/data-model';
|
||||
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
|
||||
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
|
||||
import { MetadataGenerator } from './MetadataGenerator';
|
||||
|
||||
/**
|
||||
* Adds a link to the auxiliary resource when called on the associated resource.
|
||||
* Specifically: <associatedId> <link> <auxiliaryId> will be added.
|
||||
*
|
||||
* In case the input is metadata of an auxiliary resource no metadata will be added
|
||||
*/
|
||||
export class LinkMetadataGenerator extends MetadataGenerator {
|
||||
private readonly link: string;
|
||||
private readonly identifierStrategy: AuxiliaryIdentifierStrategy;
|
||||
|
||||
public constructor(link: string, identifierStrategy: AuxiliaryIdentifierStrategy) {
|
||||
super();
|
||||
this.link = link;
|
||||
this.identifierStrategy = identifierStrategy;
|
||||
}
|
||||
|
||||
public async handle(metadata: RepresentationMetadata): Promise<void> {
|
||||
const identifier = { path: metadata.identifier.value };
|
||||
if (!this.identifierStrategy.isAuxiliaryIdentifier(identifier)) {
|
||||
metadata.add(this.link, namedNode(this.identifierStrategy.getAuxiliaryIdentifier(identifier).path));
|
||||
}
|
||||
}
|
||||
}
|
7
src/ldp/auxiliary/MetadataGenerator.ts
Normal file
7
src/ldp/auxiliary/MetadataGenerator.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
||||
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
|
||||
|
||||
/**
|
||||
* Generic interface for classes that add metadata to a RepresentationMetadata.
|
||||
*/
|
||||
export abstract class MetadataGenerator extends AsyncHandler<RepresentationMetadata> { }
|
45
src/ldp/auxiliary/RdfValidator.ts
Normal file
45
src/ldp/auxiliary/RdfValidator.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
|
||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { guardedStreamFrom } from '../../util/StreamUtil';
|
||||
import { BasicRepresentation } from '../representation/BasicRepresentation';
|
||||
import type { Representation } from '../representation/Representation';
|
||||
import { Validator } from './Validator';
|
||||
|
||||
/**
|
||||
* Validates a Representation by verifying if the data stream contains valid RDF data.
|
||||
* It does this by letting the stored RepresentationConverter convert the data.
|
||||
*/
|
||||
export class RdfValidator extends Validator {
|
||||
protected readonly converter: RepresentationConverter;
|
||||
|
||||
public constructor(converter: RepresentationConverter) {
|
||||
super();
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
public async handle(representation: Representation): Promise<void> {
|
||||
// If the data already is quads format we know it's RDF
|
||||
if (representation.metadata.contentType === INTERNAL_QUADS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/expiring-todo-comments
|
||||
// TODO: Everything below should be part of a utility cloneRepresentation function.
|
||||
|
||||
const identifier = { path: representation.metadata.identifier.value };
|
||||
|
||||
// Read data in memory first so it does not get lost
|
||||
const data = await arrayifyStream(representation.data);
|
||||
const preferences = { type: { [INTERNAL_QUADS]: 1 }};
|
||||
|
||||
// Creating new representation since converter might edit metadata
|
||||
const tempRepresentation = new BasicRepresentation(data, identifier, representation.metadata.contentType);
|
||||
const result = await this.converter.handleSafe({ identifier, representation: tempRepresentation, preferences });
|
||||
// Drain stream to make sure data was parsed correctly
|
||||
await arrayifyStream(result.data);
|
||||
|
||||
// Stream has been drained so need to create new stream
|
||||
representation.data = guardedStreamFrom(data);
|
||||
}
|
||||
}
|
44
src/ldp/auxiliary/RoutingAuxiliaryIdentifierStrategy.ts
Normal file
44
src/ldp/auxiliary/RoutingAuxiliaryIdentifierStrategy.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
|
||||
|
||||
/**
|
||||
* An {@link AuxiliaryIdentifierStrategy} that combines multiple AuxiliaryIdentifierStrategies into one.
|
||||
* Uses `isAuxiliaryIdentifier` to know which strategy to route to.
|
||||
*/
|
||||
export class RoutingAuxiliaryIdentifierStrategy implements AuxiliaryIdentifierStrategy {
|
||||
protected readonly sources: AuxiliaryIdentifierStrategy[];
|
||||
|
||||
public constructor(sources: AuxiliaryIdentifierStrategy[]) {
|
||||
this.sources = sources;
|
||||
}
|
||||
|
||||
public getAuxiliaryIdentifier(): never {
|
||||
throw new InternalServerError(
|
||||
'RoutingAuxiliaryIdentifierStrategy has multiple auxiliary strategies and thus no single auxiliary identifier.',
|
||||
);
|
||||
}
|
||||
|
||||
public getAuxiliaryIdentifiers(identifier: ResourceIdentifier): ResourceIdentifier[] {
|
||||
return this.sources.flatMap((source): ResourceIdentifier[] => source.getAuxiliaryIdentifiers(identifier));
|
||||
}
|
||||
|
||||
public isAuxiliaryIdentifier(identifier: ResourceIdentifier): boolean {
|
||||
return this.sources.some((source): boolean => source.isAuxiliaryIdentifier(identifier));
|
||||
}
|
||||
|
||||
public getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
|
||||
const source = this.getMatchingSource(identifier);
|
||||
return source.getAssociatedIdentifier(identifier);
|
||||
}
|
||||
|
||||
protected getMatchingSource(identifier: ResourceIdentifier): AuxiliaryIdentifierStrategy {
|
||||
const match = this.sources.find((source): boolean => source.isAuxiliaryIdentifier(identifier));
|
||||
if (!match) {
|
||||
throw new NotImplementedHttpError(`Could not find an AuxiliaryManager for ${identifier.path}`);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
}
|
49
src/ldp/auxiliary/RoutingAuxiliaryStrategy.ts
Normal file
49
src/ldp/auxiliary/RoutingAuxiliaryStrategy.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import type { Representation } from '../representation/Representation';
|
||||
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||
import type { AuxiliaryStrategy } from './AuxiliaryStrategy';
|
||||
import { RoutingAuxiliaryIdentifierStrategy } from './RoutingAuxiliaryIdentifierStrategy';
|
||||
|
||||
/**
|
||||
* An {@link AuxiliaryStrategy} that combines multiple AuxiliaryStrategies into one.
|
||||
* Uses `isAuxiliaryIdentifier` to know which strategy to call for which call.
|
||||
*
|
||||
* `addMetadata` will either call all strategies if the input is the associated identifier,
|
||||
* or only the matching strategy if the input is an auxiliary identifier.
|
||||
*/
|
||||
export class RoutingAuxiliaryStrategy extends RoutingAuxiliaryIdentifierStrategy implements AuxiliaryStrategy {
|
||||
protected readonly sources!: AuxiliaryStrategy[];
|
||||
|
||||
public constructor(sources: AuxiliaryStrategy[]) {
|
||||
super(sources);
|
||||
}
|
||||
|
||||
public requiresRootAuxiliary(identifier: ResourceIdentifier): boolean {
|
||||
const source = this.getMatchingSource(identifier);
|
||||
return source.requiresRootAuxiliary(identifier);
|
||||
}
|
||||
|
||||
public async addMetadata(metadata: RepresentationMetadata): Promise<void> {
|
||||
const identifier = { path: metadata.identifier.value };
|
||||
// Make sure unrelated auxiliary strategies don't add metadata to another auxiliary resource
|
||||
const match = this.sources.find((source): boolean => source.isAuxiliaryIdentifier(identifier));
|
||||
if (match) {
|
||||
await match.addMetadata(metadata);
|
||||
} else {
|
||||
for (const source of this.sources) {
|
||||
await source.addMetadata(metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async validate(representation: Representation): Promise<void> {
|
||||
const identifier = { path: representation.metadata.identifier.value };
|
||||
const source = this.getMatchingSource(identifier);
|
||||
return source.validate(representation);
|
||||
}
|
||||
|
||||
// Updated with new source typings
|
||||
protected getMatchingSource(identifier: ResourceIdentifier): AuxiliaryStrategy {
|
||||
return super.getMatchingSource(identifier) as AuxiliaryStrategy;
|
||||
}
|
||||
}
|
41
src/ldp/auxiliary/SuffixAuxiliaryIdentifierStrategy.ts
Normal file
41
src/ldp/auxiliary/SuffixAuxiliaryIdentifierStrategy.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
|
||||
|
||||
/**
|
||||
* Helper class that uses a suffix to determine if a resource is an auxiliary resource or not.
|
||||
* Simple string matching is used, so the dot needs to be included if needed, e.g. ".acl".
|
||||
*/
|
||||
export class SuffixAuxiliaryIdentifierStrategy implements AuxiliaryIdentifierStrategy {
|
||||
protected readonly suffix: string;
|
||||
|
||||
public constructor(suffix: string) {
|
||||
if (suffix.length === 0) {
|
||||
throw new InternalServerError('Suffix length should be non-zero.');
|
||||
}
|
||||
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 getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
|
||||
if (!this.isAuxiliaryIdentifier(identifier)) {
|
||||
throw new InternalServerError(`${identifier.path} does not end on ${this.suffix} so no conversion is possible.`);
|
||||
}
|
||||
return { path: identifier.path.slice(0, -this.suffix.length) };
|
||||
}
|
||||
|
||||
public canDeleteRoot(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
7
src/ldp/auxiliary/Validator.ts
Normal file
7
src/ldp/auxiliary/Validator.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
||||
import type { Representation } from '../representation/Representation';
|
||||
|
||||
/**
|
||||
* Generic interface for classes that validate Representations in some way.
|
||||
*/
|
||||
export abstract class Validator extends AsyncHandler<Representation> { }
|
80
test/unit/ldp/auxiliary/ComposedAuxiliaryStrategy.test.ts
Normal file
80
test/unit/ldp/auxiliary/ComposedAuxiliaryStrategy.test.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import type { AuxiliaryIdentifierStrategy } from '../../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
|
||||
import { ComposedAuxiliaryStrategy } from '../../../../src/ldp/auxiliary/ComposedAuxiliaryStrategy';
|
||||
import type { MetadataGenerator } from '../../../../src/ldp/auxiliary/MetadataGenerator';
|
||||
import type { Validator } from '../../../../src/ldp/auxiliary/Validator';
|
||||
import { RepresentationMetadata } from '../../../../src/ldp/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(),
|
||||
getAssociatedIdentifier: jest.fn(),
|
||||
isAuxiliaryIdentifier: jest.fn(),
|
||||
};
|
||||
metadataGenerator = {
|
||||
handleSafe: jest.fn(),
|
||||
} as any;
|
||||
validator = {
|
||||
handleSafe: jest.fn(),
|
||||
} as any;
|
||||
strategy = new ComposedAuxiliaryStrategy(identifierStrategy, metadataGenerator, validator, 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.getAssociatedIdentifier(identifier);
|
||||
expect(identifierStrategy.getAssociatedIdentifier).toHaveBeenCalledTimes(1);
|
||||
expect(identifierStrategy.getAssociatedIdentifier).toHaveBeenLastCalledWith(identifier);
|
||||
|
||||
strategy.isAuxiliaryIdentifier(identifier);
|
||||
expect(identifierStrategy.isAuxiliaryIdentifier).toHaveBeenCalledTimes(1);
|
||||
expect(identifierStrategy.isAuxiliaryIdentifier).toHaveBeenLastCalledWith(identifier);
|
||||
});
|
||||
|
||||
it('returns the injected value for canDeleteRoot.', async(): Promise<void> => {
|
||||
expect(strategy.requiresRootAuxiliary()).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 canDeleteRoot to false.', async(): Promise<void> => {
|
||||
strategy = new ComposedAuxiliaryStrategy(identifierStrategy, metadataGenerator, validator);
|
||||
expect(strategy.requiresRootAuxiliary()).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();
|
||||
});
|
||||
});
|
39
test/unit/ldp/auxiliary/LinkMetadataGenerator.test.ts
Normal file
39
test/unit/ldp/auxiliary/LinkMetadataGenerator.test.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import type { AuxiliaryIdentifierStrategy } from '../../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
|
||||
import { LinkMetadataGenerator } from '../../../../src/ldp/auxiliary/LinkMetadataGenerator';
|
||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
|
||||
|
||||
describe('A LinkMetadataGenerator', (): void => {
|
||||
const link = 'link';
|
||||
const associatedId: 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'),
|
||||
getAssociatedIdentifier: (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 an associated 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 associated resources.', async(): Promise<void> => {
|
||||
const metadata = new RepresentationMetadata(associatedId);
|
||||
await expect(generator.handle(metadata)).resolves.toBeUndefined();
|
||||
expect(metadata.quads()).toHaveLength(1);
|
||||
expect(metadata.get(link)?.value).toBe(auxiliaryId.path);
|
||||
});
|
||||
});
|
44
test/unit/ldp/auxiliary/RdfValidator.test.ts
Normal file
44
test/unit/ldp/auxiliary/RdfValidator.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { RdfValidator } from '../../../../src/ldp/auxiliary/RdfValidator';
|
||||
import { BasicRepresentation } from '../../../../src/ldp/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.readableEnded).toBe(true);
|
||||
});
|
||||
});
|
@ -0,0 +1,66 @@
|
||||
import type { AuxiliaryIdentifierStrategy } from '../../../../src/ldp/auxiliary/AuxiliaryIdentifierStrategy';
|
||||
import { RoutingAuxiliaryIdentifierStrategy } from '../../../../src/ldp/auxiliary/RoutingAuxiliaryIdentifierStrategy';
|
||||
import type { ResourceIdentifier } from '../../../../src/ldp/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 getAssociatedIdentifier(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('#getAssociatedIdentifier returns the base id if a match is found.', async(): Promise<void> => {
|
||||
expect(strategy.getAssociatedIdentifier(dummy1Id)).toEqual(baseId);
|
||||
expect(strategy.getAssociatedIdentifier(dummy2Id)).toEqual(baseId);
|
||||
expect((): any => strategy.getAssociatedIdentifier(dummy3Id)).toThrow(NotImplementedHttpError);
|
||||
});
|
||||
});
|
106
test/unit/ldp/auxiliary/RoutingAuxiliaryStrategy.test.ts
Normal file
106
test/unit/ldp/auxiliary/RoutingAuxiliaryStrategy.test.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import type { AuxiliaryStrategy } from '../../../../src/ldp/auxiliary/AuxiliaryStrategy';
|
||||
import { RoutingAuxiliaryStrategy } from '../../../../src/ldp/auxiliary/RoutingAuxiliaryStrategy';
|
||||
import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../../../src/ldp/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 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 getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
|
||||
return { path: identifier.path.slice(0, -this.suffix.length) };
|
||||
}
|
||||
|
||||
public requiresRootAuxiliary(): 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('#canDeleteRoot returns the result of the correct source.', async(): Promise<void> => {
|
||||
sources[0].requiresRootAuxiliary = jest.fn();
|
||||
sources[1].requiresRootAuxiliary = jest.fn();
|
||||
strategy.requiresRootAuxiliary(dummy2Id);
|
||||
expect(sources[0].requiresRootAuxiliary).toHaveBeenCalledTimes(0);
|
||||
expect(sources[1].requiresRootAuxiliary).toHaveBeenCalledTimes(1);
|
||||
expect(sources[1].requiresRootAuxiliary).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,45 @@
|
||||
import { SuffixAuxiliaryIdentifierStrategy } from '../../../../src/ldp/auxiliary/SuffixAuxiliaryIdentifierStrategy';
|
||||
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
|
||||
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
||||
import 'jest-rdf';
|
||||
|
||||
const suffix = '.dummy';
|
||||
|
||||
describe('A SuffixAuxiliaryManager', (): void => {
|
||||
let strategy: SuffixAuxiliaryIdentifierStrategy;
|
||||
const associatedId: 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(associatedId)).toEqual(auxiliaryId);
|
||||
});
|
||||
|
||||
it('returns the same single identifier when requesting all of them.', async(): Promise<void> => {
|
||||
expect(strategy.getAuxiliaryIdentifiers(associatedId)).toEqual([ auxiliaryId ]);
|
||||
});
|
||||
|
||||
it('checks the suffix to determine if an identifier is auxiliary.', async(): Promise<void> => {
|
||||
expect(strategy.isAuxiliaryIdentifier(associatedId)).toBe(false);
|
||||
expect(strategy.isAuxiliaryIdentifier(auxiliaryId)).toBe(true);
|
||||
});
|
||||
|
||||
it('errors when trying to get the associated id from a non-auxiliary identifier.', async(): Promise<void> => {
|
||||
expect((): any => strategy.getAssociatedIdentifier(associatedId)).toThrow(InternalServerError);
|
||||
});
|
||||
|
||||
it('removes the suffix to create the associated identifier.', async(): Promise<void> => {
|
||||
expect(strategy.getAssociatedIdentifier(auxiliaryId)).toEqual(associatedId);
|
||||
});
|
||||
|
||||
it('returns true on canDeleteRoot.', async(): Promise<void> => {
|
||||
expect(strategy.canDeleteRoot()).toEqual(true);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user