mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Use ETagHandler for ETag generation and comparison
This commit is contained in:
parent
5ec6eddbfa
commit
afcbfdaacf
@ -15,15 +15,13 @@
|
|||||||
{ "@type": "DeleteNotificationGenerator" },
|
{ "@type": "DeleteNotificationGenerator" },
|
||||||
{
|
{
|
||||||
"@type": "AddRemoveNotificationGenerator",
|
"@type": "AddRemoveNotificationGenerator",
|
||||||
"store": {
|
"store": { "@id": "urn:solid-server:default:ResourceStore" },
|
||||||
"@id": "urn:solid-server:default:ResourceStore"
|
"eTagHandler": { "@id": "urn:solid-server:default:ETagHandler" }
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "ActivityNotificationGenerator",
|
"@type": "ActivityNotificationGenerator",
|
||||||
"store": {
|
"store": { "@id": "urn:solid-server:default:ResourceStore" },
|
||||||
"@id": "urn:solid-server:default:ResourceStore"
|
"eTagHandler": { "@id": "urn:solid-server:default:ETagHandler" }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@
|
|||||||
"@type": "ComposedNotificationHandler",
|
"@type": "ComposedNotificationHandler",
|
||||||
"generator": { "@id": "urn:solid-server:default:BaseNotificationGenerator" },
|
"generator": { "@id": "urn:solid-server:default:BaseNotificationGenerator" },
|
||||||
"serializer": { "@id": "urn:solid-server:default:BaseNotificationSerializer" },
|
"serializer": { "@id": "urn:solid-server:default:BaseNotificationSerializer" },
|
||||||
"emitter": { "@id": "urn:solid-server:default:WebHookEmitter" }
|
"emitter": { "@id": "urn:solid-server:default:WebHookEmitter" },
|
||||||
|
"eTagHandler": { "@id": "urn:solid-server:default:ETagHandler" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -10,7 +10,8 @@
|
|||||||
"@type": "ComposedNotificationHandler",
|
"@type": "ComposedNotificationHandler",
|
||||||
"generator": { "@id": "urn:solid-server:default:BaseNotificationGenerator" },
|
"generator": { "@id": "urn:solid-server:default:BaseNotificationGenerator" },
|
||||||
"serializer": { "@id": "urn:solid-server:default:BaseNotificationSerializer" },
|
"serializer": { "@id": "urn:solid-server:default:BaseNotificationSerializer" },
|
||||||
"emitter": { "@id": "urn:solid-server:default:WebSocket2023Emitter" }
|
"emitter": { "@id": "urn:solid-server:default:WebSocket2023Emitter" },
|
||||||
|
"eTagHandler": { "@id": "urn:solid-server:default:ETagHandler" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
"handlers": [
|
"handlers": [
|
||||||
{
|
{
|
||||||
"@type": "GetOperationHandler",
|
"@type": "GetOperationHandler",
|
||||||
"store": { "@id": "urn:solid-server:default:ResourceStore" }
|
"store": { "@id": "urn:solid-server:default:ResourceStore" },
|
||||||
|
"eTagHandler": { "@id": "urn:solid-server:default:ETagHandler" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "PostOperationHandler",
|
"@type": "PostOperationHandler",
|
||||||
@ -24,7 +25,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "HeadOperationHandler",
|
"@type": "HeadOperationHandler",
|
||||||
"store": { "@id": "urn:solid-server:default:ResourceStore" }
|
"store": { "@id": "urn:solid-server:default:ResourceStore" },
|
||||||
|
"eTagHandler": { "@id": "urn:solid-server:default:ETagHandler" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"@type": "PatchOperationHandler",
|
"@type": "PatchOperationHandler",
|
||||||
|
@ -5,16 +5,22 @@
|
|||||||
"comment": "Handles everything related to parsing a Request.",
|
"comment": "Handles everything related to parsing a Request.",
|
||||||
"@id": "urn:solid-server:default:RequestParser",
|
"@id": "urn:solid-server:default:RequestParser",
|
||||||
"@type": "BasicRequestParser",
|
"@type": "BasicRequestParser",
|
||||||
"args_targetExtractor": {
|
"targetExtractor": {
|
||||||
"@id": "urn:solid-server:default:TargetExtractor",
|
"@id": "urn:solid-server:default:TargetExtractor",
|
||||||
"@type": "OriginalUrlExtractor",
|
"@type": "OriginalUrlExtractor",
|
||||||
"args_identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" },
|
"identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" },
|
||||||
"args_includeQueryString": false
|
"includeQueryString": false
|
||||||
},
|
},
|
||||||
"args_preferenceParser": { "@id": "urn:solid-server:default:PreferenceParser" },
|
"preferenceParser": { "@id": "urn:solid-server:default:PreferenceParser" },
|
||||||
"args_metadataParser": { "@id": "urn:solid-server:default:MetadataParser" },
|
"metadataParser": { "@id": "urn:solid-server:default:MetadataParser" },
|
||||||
"args_conditionsParser": { "@type": "BasicConditionsParser" },
|
"conditionsParser": {
|
||||||
"args_bodyParser": {
|
"@type": "BasicConditionsParser",
|
||||||
|
"eTagHandler": {
|
||||||
|
"@id": "urn:solid-server:default:ETagHandler",
|
||||||
|
"@type": "BasicETagHandler"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bodyParser": {
|
||||||
"@type": "WaterfallHandler",
|
"@type": "WaterfallHandler",
|
||||||
"handlers": [
|
"handlers": [
|
||||||
{ "@id": "urn:solid-server:default:PatchBodyParser" },
|
{ "@id": "urn:solid-server:default:PatchBodyParser" },
|
||||||
|
@ -3,11 +3,12 @@ import type { HttpRequest } from '../../../server/HttpRequest';
|
|||||||
import type { BasicConditionsOptions } from '../../../storage/conditions/BasicConditions';
|
import type { BasicConditionsOptions } from '../../../storage/conditions/BasicConditions';
|
||||||
import { BasicConditions } from '../../../storage/conditions/BasicConditions';
|
import { BasicConditions } from '../../../storage/conditions/BasicConditions';
|
||||||
import type { Conditions } from '../../../storage/conditions/Conditions';
|
import type { Conditions } from '../../../storage/conditions/Conditions';
|
||||||
|
import type { ETagHandler } from '../../../storage/conditions/ETagHandler';
|
||||||
import { splitCommaSeparated } from '../../../util/StringUtil';
|
import { splitCommaSeparated } from '../../../util/StringUtil';
|
||||||
import { ConditionsParser } from './ConditionsParser';
|
import { ConditionsParser } from './ConditionsParser';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Conditions object based on the the following headers:
|
* Creates a Conditions object based on the following headers:
|
||||||
* - If-Modified-Since
|
* - If-Modified-Since
|
||||||
* - If-Unmodified-Since
|
* - If-Unmodified-Since
|
||||||
* - If-Match
|
* - If-Match
|
||||||
@ -18,6 +19,13 @@ import { ConditionsParser } from './ConditionsParser';
|
|||||||
export class BasicConditionsParser extends ConditionsParser {
|
export class BasicConditionsParser extends ConditionsParser {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
|
protected readonly eTagHandler: ETagHandler;
|
||||||
|
|
||||||
|
public constructor(eTagHandler: ETagHandler) {
|
||||||
|
super();
|
||||||
|
this.eTagHandler = eTagHandler;
|
||||||
|
}
|
||||||
|
|
||||||
public async handle(request: HttpRequest): Promise<Conditions | undefined> {
|
public async handle(request: HttpRequest): Promise<Conditions | undefined> {
|
||||||
const options: BasicConditionsOptions = {
|
const options: BasicConditionsOptions = {
|
||||||
matchesETag: this.parseTagHeader(request, 'if-match'),
|
matchesETag: this.parseTagHeader(request, 'if-match'),
|
||||||
@ -38,7 +46,7 @@ export class BasicConditionsParser extends ConditionsParser {
|
|||||||
// Only return a Conditions object if there is at least one condition; undefined otherwise
|
// Only return a Conditions object if there is at least one condition; undefined otherwise
|
||||||
this.logger.debug(`Found the following conditions: ${JSON.stringify(options)}`);
|
this.logger.debug(`Found the following conditions: ${JSON.stringify(options)}`);
|
||||||
if (Object.values(options).some((val): boolean => typeof val !== 'undefined')) {
|
if (Object.values(options).some((val): boolean => typeof val !== 'undefined')) {
|
||||||
return new BasicConditions(options);
|
return new BasicConditions(this.eTagHandler, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getETag } from '../../storage/conditions/Conditions';
|
import type { ETagHandler } from '../../storage/conditions/ETagHandler';
|
||||||
import type { ResourceStore } from '../../storage/ResourceStore';
|
import type { ResourceStore } from '../../storage/ResourceStore';
|
||||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
import { assertReadConditions } from '../../util/ResourceUtil';
|
import { assertReadConditions } from '../../util/ResourceUtil';
|
||||||
@ -14,10 +14,12 @@ import { OperationHandler } from './OperationHandler';
|
|||||||
*/
|
*/
|
||||||
export class GetOperationHandler extends OperationHandler {
|
export class GetOperationHandler extends OperationHandler {
|
||||||
private readonly store: ResourceStore;
|
private readonly store: ResourceStore;
|
||||||
|
private readonly eTagHandler: ETagHandler;
|
||||||
|
|
||||||
public constructor(store: ResourceStore) {
|
public constructor(store: ResourceStore, eTagHandler: ETagHandler) {
|
||||||
super();
|
super();
|
||||||
this.store = store;
|
this.store = store;
|
||||||
|
this.eTagHandler = eTagHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||||
@ -33,7 +35,7 @@ export class GetOperationHandler extends OperationHandler {
|
|||||||
assertReadConditions(body, operation.conditions);
|
assertReadConditions(body, operation.conditions);
|
||||||
|
|
||||||
// Add the ETag of the returned representation
|
// Add the ETag of the returned representation
|
||||||
const etag = getETag(body.metadata);
|
const etag = this.eTagHandler.getETag(body.metadata);
|
||||||
body.metadata.set(HH.terms.etag, etag);
|
body.metadata.set(HH.terms.etag, etag);
|
||||||
|
|
||||||
return new OkResponseDescription(body.metadata, body.data);
|
return new OkResponseDescription(body.metadata, body.data);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getETag } from '../../storage/conditions/Conditions';
|
import type { ETagHandler } from '../../storage/conditions/ETagHandler';
|
||||||
import type { ResourceStore } from '../../storage/ResourceStore';
|
import type { ResourceStore } from '../../storage/ResourceStore';
|
||||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
import { assertReadConditions } from '../../util/ResourceUtil';
|
import { assertReadConditions } from '../../util/ResourceUtil';
|
||||||
@ -14,10 +14,12 @@ import { OperationHandler } from './OperationHandler';
|
|||||||
*/
|
*/
|
||||||
export class HeadOperationHandler extends OperationHandler {
|
export class HeadOperationHandler extends OperationHandler {
|
||||||
private readonly store: ResourceStore;
|
private readonly store: ResourceStore;
|
||||||
|
private readonly eTagHandler: ETagHandler;
|
||||||
|
|
||||||
public constructor(store: ResourceStore) {
|
public constructor(store: ResourceStore, eTagHandler: ETagHandler) {
|
||||||
super();
|
super();
|
||||||
this.store = store;
|
this.store = store;
|
||||||
|
this.eTagHandler = eTagHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
public async canHandle({ operation }: OperationHandlerInput): Promise<void> {
|
||||||
@ -37,7 +39,7 @@ export class HeadOperationHandler extends OperationHandler {
|
|||||||
assertReadConditions(body, operation.conditions);
|
assertReadConditions(body, operation.conditions);
|
||||||
|
|
||||||
// Add the ETag of the returned representation
|
// Add the ETag of the returned representation
|
||||||
const etag = getETag(body.metadata);
|
const etag = this.eTagHandler.getETag(body.metadata);
|
||||||
body.metadata.set(HH.terms.etag, etag);
|
body.metadata.set(HH.terms.etag, etag);
|
||||||
|
|
||||||
return new OkResponseDescription(body.metadata);
|
return new OkResponseDescription(body.metadata);
|
||||||
|
@ -374,7 +374,9 @@ export * from './storage/accessors/ValidatingDataAccessor';
|
|||||||
|
|
||||||
// Storage/Conditions
|
// Storage/Conditions
|
||||||
export * from './storage/conditions/BasicConditions';
|
export * from './storage/conditions/BasicConditions';
|
||||||
|
export * from './storage/conditions/BasicETagHandler';
|
||||||
export * from './storage/conditions/Conditions';
|
export * from './storage/conditions/Conditions';
|
||||||
|
export * from './storage/conditions/ETagHandler';
|
||||||
|
|
||||||
// Storage/Conversion
|
// Storage/Conversion
|
||||||
export * from './storage/conversion/BaseTypedRepresentationConverter';
|
export * from './storage/conversion/BaseTypedRepresentationConverter';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { sameResourceState } from '../../storage/conditions/Conditions';
|
import type { ETagHandler } from '../../storage/conditions/ETagHandler';
|
||||||
import type { NotificationGenerator } from './generate/NotificationGenerator';
|
import type { NotificationGenerator } from './generate/NotificationGenerator';
|
||||||
import type { NotificationEmitter } from './NotificationEmitter';
|
import type { NotificationEmitter } from './NotificationEmitter';
|
||||||
import type { NotificationHandlerInput } from './NotificationHandler';
|
import type { NotificationHandlerInput } from './NotificationHandler';
|
||||||
@ -9,6 +9,7 @@ export interface ComposedNotificationHandlerArgs {
|
|||||||
generator: NotificationGenerator;
|
generator: NotificationGenerator;
|
||||||
serializer: NotificationSerializer;
|
serializer: NotificationSerializer;
|
||||||
emitter: NotificationEmitter;
|
emitter: NotificationEmitter;
|
||||||
|
eTagHandler: ETagHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,12 +22,14 @@ export class ComposedNotificationHandler extends NotificationHandler {
|
|||||||
private readonly generator: NotificationGenerator;
|
private readonly generator: NotificationGenerator;
|
||||||
private readonly serializer: NotificationSerializer;
|
private readonly serializer: NotificationSerializer;
|
||||||
private readonly emitter: NotificationEmitter;
|
private readonly emitter: NotificationEmitter;
|
||||||
|
private readonly eTagHandler: ETagHandler;
|
||||||
|
|
||||||
public constructor(args: ComposedNotificationHandlerArgs) {
|
public constructor(args: ComposedNotificationHandlerArgs) {
|
||||||
super();
|
super();
|
||||||
this.generator = args.generator;
|
this.generator = args.generator;
|
||||||
this.serializer = args.serializer;
|
this.serializer = args.serializer;
|
||||||
this.emitter = args.emitter;
|
this.emitter = args.emitter;
|
||||||
|
this.eTagHandler = args.eTagHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canHandle(input: NotificationHandlerInput): Promise<void> {
|
public async canHandle(input: NotificationHandlerInput): Promise<void> {
|
||||||
@ -38,7 +41,8 @@ export class ComposedNotificationHandler extends NotificationHandler {
|
|||||||
|
|
||||||
const { state } = input.channel;
|
const { state } = input.channel;
|
||||||
// In case the state matches there is no need to send the notification
|
// In case the state matches there is no need to send the notification
|
||||||
if (typeof state === 'string' && notification.state && sameResourceState(state, notification.state)) {
|
if (typeof state === 'string' && notification.state &&
|
||||||
|
this.eTagHandler.sameResourceState(state, notification.state)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getETag } from '../../../storage/conditions/Conditions';
|
import type { ETagHandler } from '../../../storage/conditions/ETagHandler';
|
||||||
import type { ResourceStore } from '../../../storage/ResourceStore';
|
import type { ResourceStore } from '../../../storage/ResourceStore';
|
||||||
import { NotImplementedHttpError } from '../../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../util/errors/NotImplementedHttpError';
|
||||||
import { AS } from '../../../util/Vocabularies';
|
import { AS } from '../../../util/Vocabularies';
|
||||||
@ -13,10 +13,12 @@ import { NotificationGenerator } from './NotificationGenerator';
|
|||||||
*/
|
*/
|
||||||
export class ActivityNotificationGenerator extends NotificationGenerator {
|
export class ActivityNotificationGenerator extends NotificationGenerator {
|
||||||
private readonly store: ResourceStore;
|
private readonly store: ResourceStore;
|
||||||
|
private readonly eTagHandler: ETagHandler;
|
||||||
|
|
||||||
public constructor(store: ResourceStore) {
|
public constructor(store: ResourceStore, eTagHandler: ETagHandler) {
|
||||||
super();
|
super();
|
||||||
this.store = store;
|
this.store = store;
|
||||||
|
this.eTagHandler = eTagHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canHandle({ activity }: NotificationHandlerInput): Promise<void> {
|
public async canHandle({ activity }: NotificationHandlerInput): Promise<void> {
|
||||||
@ -28,8 +30,7 @@ export class ActivityNotificationGenerator extends NotificationGenerator {
|
|||||||
public async handle({ topic, activity }: NotificationHandlerInput): Promise<Notification> {
|
public async handle({ topic, activity }: NotificationHandlerInput): Promise<Notification> {
|
||||||
const representation = await this.store.getRepresentation(topic, {});
|
const representation = await this.store.getRepresentation(topic, {});
|
||||||
representation.data.destroy();
|
representation.data.destroy();
|
||||||
|
const state = this.eTagHandler.getETag(representation.metadata);
|
||||||
const state = getETag(representation.metadata);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'@context': [
|
'@context': [
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { getETag } from '../../../storage/conditions/Conditions';
|
import type { ETagHandler } from '../../../storage/conditions/ETagHandler';
|
||||||
import type { ResourceStore } from '../../../storage/ResourceStore';
|
import type { ResourceStore } from '../../../storage/ResourceStore';
|
||||||
import { InternalServerError } from '../../../util/errors/InternalServerError';
|
import { InternalServerError } from '../../../util/errors/InternalServerError';
|
||||||
import { NotImplementedHttpError } from '../../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../util/errors/NotImplementedHttpError';
|
||||||
@ -15,10 +15,12 @@ import { NotificationGenerator } from './NotificationGenerator';
|
|||||||
*/
|
*/
|
||||||
export class AddRemoveNotificationGenerator extends NotificationGenerator {
|
export class AddRemoveNotificationGenerator extends NotificationGenerator {
|
||||||
private readonly store: ResourceStore;
|
private readonly store: ResourceStore;
|
||||||
|
private readonly eTagHandler: ETagHandler;
|
||||||
|
|
||||||
public constructor(store: ResourceStore) {
|
public constructor(store: ResourceStore, eTagHandler: ETagHandler) {
|
||||||
super();
|
super();
|
||||||
this.store = store;
|
this.store = store;
|
||||||
|
this.eTagHandler = eTagHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canHandle({ activity }: NotificationHandlerInput): Promise<void> {
|
public async canHandle({ activity }: NotificationHandlerInput): Promise<void> {
|
||||||
@ -30,8 +32,8 @@ export class AddRemoveNotificationGenerator extends NotificationGenerator {
|
|||||||
public async handle({ activity, topic, metadata }: NotificationHandlerInput): Promise<Notification> {
|
public async handle({ activity, topic, metadata }: NotificationHandlerInput): Promise<Notification> {
|
||||||
const representation = await this.store.getRepresentation(topic, {});
|
const representation = await this.store.getRepresentation(topic, {});
|
||||||
representation.data.destroy();
|
representation.data.destroy();
|
||||||
|
const state = this.eTagHandler.getETag(representation.metadata);
|
||||||
|
|
||||||
const state = getETag(representation.metadata);
|
|
||||||
const objects = metadata?.getAll(AS.terms.object);
|
const objects = metadata?.getAll(AS.terms.object);
|
||||||
if (!objects || objects.length === 0) {
|
if (!objects || objects.length === 0) {
|
||||||
throw new InternalServerError(`Missing as:object metadata for ${activity?.value} activity on ${topic.path}`);
|
throw new InternalServerError(`Missing as:object metadata for ${activity?.value} activity on ${topic.path}`);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||||
import { DC } from '../../util/Vocabularies';
|
import { DC } from '../../util/Vocabularies';
|
||||||
import { getETag, sameResourceState } from './Conditions';
|
|
||||||
import type { Conditions } from './Conditions';
|
import type { Conditions } from './Conditions';
|
||||||
|
import type { ETagHandler } from './ETagHandler';
|
||||||
|
|
||||||
export interface BasicConditionsOptions {
|
export interface BasicConditionsOptions {
|
||||||
matchesETag?: string[];
|
matchesETag?: string[];
|
||||||
@ -14,12 +14,16 @@ export interface BasicConditionsOptions {
|
|||||||
* Stores all the relevant Conditions values and matches them based on RFC7232.
|
* Stores all the relevant Conditions values and matches them based on RFC7232.
|
||||||
*/
|
*/
|
||||||
export class BasicConditions implements Conditions {
|
export class BasicConditions implements Conditions {
|
||||||
|
protected readonly eTagHandler: ETagHandler;
|
||||||
|
|
||||||
public readonly matchesETag?: string[];
|
public readonly matchesETag?: string[];
|
||||||
public readonly notMatchesETag?: string[];
|
public readonly notMatchesETag?: string[];
|
||||||
public readonly modifiedSince?: Date;
|
public readonly modifiedSince?: Date;
|
||||||
public readonly unmodifiedSince?: Date;
|
public readonly unmodifiedSince?: Date;
|
||||||
|
|
||||||
public constructor(options: BasicConditionsOptions) {
|
public constructor(eTagHandler: ETagHandler, options: BasicConditionsOptions) {
|
||||||
|
this.eTagHandler = eTagHandler;
|
||||||
|
|
||||||
this.matchesETag = options.matchesETag;
|
this.matchesETag = options.matchesETag;
|
||||||
this.notMatchesETag = options.notMatchesETag;
|
this.notMatchesETag = options.notMatchesETag;
|
||||||
this.modifiedSince = options.modifiedSince;
|
this.modifiedSince = options.modifiedSince;
|
||||||
@ -39,21 +43,12 @@ export class BasicConditions implements Conditions {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eTag = getETag(metadata);
|
const eTagMatches = (tag: string): boolean => this.eTagHandler.matchesETag(metadata, tag, Boolean(strict));
|
||||||
if (eTag) {
|
if (this.matchesETag && !this.matchesETag.includes('*') && !this.matchesETag.some(eTagMatches)) {
|
||||||
// Helper function to see if an ETag matches the provided metadata
|
return false;
|
||||||
// eslint-disable-next-line func-style
|
}
|
||||||
let eTagMatches = (tag: string): boolean => sameResourceState(tag, eTag);
|
if (this.notMatchesETag?.some(eTagMatches)) {
|
||||||
if (strict) {
|
return false;
|
||||||
eTagMatches = (tag: string): boolean => tag === eTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.matchesETag && !this.matchesETag.includes('*') && !this.matchesETag.some(eTagMatches)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (this.notMatchesETag?.some(eTagMatches)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// In practice, this will only be undefined on a backend
|
// In practice, this will only be undefined on a backend
|
||||||
|
38
src/storage/conditions/BasicETagHandler.ts
Normal file
38
src/storage/conditions/BasicETagHandler.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||||
|
import { DC } from '../../util/Vocabularies';
|
||||||
|
import type { ETagHandler } from './ETagHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard implementation of {@link ETagHandler}.
|
||||||
|
* ETags are constructed by combining the last modified date with the content type of the representation.
|
||||||
|
*/
|
||||||
|
export class BasicETagHandler implements ETagHandler {
|
||||||
|
public getETag(metadata: RepresentationMetadata): string | undefined {
|
||||||
|
const modified = metadata.get(DC.terms.modified);
|
||||||
|
const { contentType } = metadata;
|
||||||
|
if (modified && contentType) {
|
||||||
|
const date = new Date(modified.value);
|
||||||
|
return `"${date.getTime()}-${contentType}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public matchesETag(metadata: RepresentationMetadata, eTag: string, strict: boolean): boolean {
|
||||||
|
const modified = metadata.get(DC.terms.modified);
|
||||||
|
if (!modified) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const date = new Date(modified.value);
|
||||||
|
const { contentType } = metadata;
|
||||||
|
|
||||||
|
// Slicing of the double quotes
|
||||||
|
const [ eTagTimestamp, eTagContentType ] = eTag.slice(1, -1).split('-');
|
||||||
|
|
||||||
|
return eTagTimestamp === `${date.getTime()}` && (!strict || eTagContentType === contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sameResourceState(eTag1: string, eTag2: string): boolean {
|
||||||
|
// Since we base the ETag on the last modified date,
|
||||||
|
// we know the ETags match as long as the date part is the same.
|
||||||
|
return eTag1.split('-')[0] === eTag2.split('-')[0];
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||||
import { DC } from '../../util/Vocabularies';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The conditions of an HTTP conditional request.
|
* The conditions of an HTTP conditional request.
|
||||||
@ -26,37 +25,8 @@ export interface Conditions {
|
|||||||
* Checks validity based on the given metadata.
|
* Checks validity based on the given metadata.
|
||||||
* @param metadata - Metadata of the representation. Undefined if the resource does not exist.
|
* @param metadata - Metadata of the representation. Undefined if the resource does not exist.
|
||||||
* @param strict - How to compare the ETag related headers.
|
* @param strict - How to compare the ETag related headers.
|
||||||
* If true, exact string matching will be used to compare with the ETag for the given metadata.
|
* If true, the comparison will happen on representation level.
|
||||||
* If false, it will take into account that content negotiation might still happen
|
* If false, the comparison happens on resource level, ignoring the content-type.
|
||||||
* which can change the ETag.
|
|
||||||
*/
|
*/
|
||||||
matchesMetadata: (metadata?: RepresentationMetadata, strict?: boolean) => boolean;
|
matchesMetadata: (metadata?: RepresentationMetadata, strict?: boolean) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates an ETag based on the last modified date of a resource.
|
|
||||||
* @param metadata - Metadata of the resource.
|
|
||||||
*
|
|
||||||
* @returns the generated ETag. Undefined if no last modified date was found.
|
|
||||||
*/
|
|
||||||
export function getETag(metadata: RepresentationMetadata): string | undefined {
|
|
||||||
const modified = metadata.get(DC.terms.modified);
|
|
||||||
const { contentType } = metadata;
|
|
||||||
if (modified) {
|
|
||||||
const date = new Date(modified.value);
|
|
||||||
// It is possible for the content type to be undefined,
|
|
||||||
// such as when only the metadata returned by a `DataAccessor` is used.
|
|
||||||
return `"${date.getTime()}-${contentType ?? ''}"`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates whether 2 ETags correspond to the same state of a resource,
|
|
||||||
* independent of the representation the ETags correspond to.
|
|
||||||
* Assumes ETags are made with the {@link getETag} function.
|
|
||||||
*/
|
|
||||||
export function sameResourceState(eTag1: string, eTag2: string): boolean {
|
|
||||||
// Since we base the ETag on the last modified date,
|
|
||||||
// we know the ETags match as long as the date part is the same.
|
|
||||||
return eTag1.split('-')[0] === eTag2.split('-')[0];
|
|
||||||
}
|
|
||||||
|
33
src/storage/conditions/ETagHandler.ts
Normal file
33
src/storage/conditions/ETagHandler.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import type { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for everything related to ETag generation and comparison.
|
||||||
|
* ETags are constructed in such a way they can both be used for the standard ETag usage of comparing representations,
|
||||||
|
* but also to see if two ETags of different representations correspond to the same resource state.
|
||||||
|
*/
|
||||||
|
export interface ETagHandler {
|
||||||
|
/**
|
||||||
|
* Generates an ETag for the given metadata. Returns undefined if no ETag could be generated.
|
||||||
|
*
|
||||||
|
* @param metadata - Metadata of the representation to generate an ETag for.
|
||||||
|
*/
|
||||||
|
getETag: (metadata: RepresentationMetadata) => string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates whether the given metadata corresponds to the given ETag.
|
||||||
|
*
|
||||||
|
* @param metadata - Metadata of the resource.
|
||||||
|
* @param eTag - ETag to compare to.
|
||||||
|
* @param strict - True if the comparison needs to be on representation level.
|
||||||
|
* False if it is on resource level and the content-type doesn't matter.
|
||||||
|
*/
|
||||||
|
matchesETag: (metadata: RepresentationMetadata, eTag: string, strict: boolean) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates whether 2 ETags correspond to the same state of a resource,
|
||||||
|
* independent of the representation the ETags correspond to.
|
||||||
|
* @param eTag1 - First ETag to compare.
|
||||||
|
* @param eTag2 - Second ETag to compare.
|
||||||
|
*/
|
||||||
|
sameResourceState: (eTag1: string, eTag2: string) => boolean;
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import arrayifyStream from 'arrayify-stream';
|
import arrayifyStream from 'arrayify-stream';
|
||||||
import { SingleRootIdentifierStrategy } from '../../src';
|
import { BasicETagHandler, SingleRootIdentifierStrategy } from '../../src';
|
||||||
import { BasicRequestParser } from '../../src/http/input/BasicRequestParser';
|
import { BasicRequestParser } from '../../src/http/input/BasicRequestParser';
|
||||||
import { RawBodyParser } from '../../src/http/input/body/RawBodyParser';
|
import { RawBodyParser } from '../../src/http/input/body/RawBodyParser';
|
||||||
import { BasicConditionsParser } from '../../src/http/input/conditions/BasicConditionsParser';
|
import { BasicConditionsParser } from '../../src/http/input/conditions/BasicConditionsParser';
|
||||||
@ -9,7 +9,6 @@ import { ContentTypeParser } from '../../src/http/input/metadata/ContentTypePars
|
|||||||
import { AcceptPreferenceParser } from '../../src/http/input/preferences/AcceptPreferenceParser';
|
import { AcceptPreferenceParser } from '../../src/http/input/preferences/AcceptPreferenceParser';
|
||||||
import { RepresentationMetadata } from '../../src/http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../src/http/representation/RepresentationMetadata';
|
||||||
import type { HttpRequest } from '../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../src/server/HttpRequest';
|
||||||
import { BasicConditions } from '../../src/storage/conditions/BasicConditions';
|
|
||||||
import { guardedStreamFrom } from '../../src/util/StreamUtil';
|
import { guardedStreamFrom } from '../../src/util/StreamUtil';
|
||||||
|
|
||||||
describe('A BasicRequestParser with simple input parsers', (): void => {
|
describe('A BasicRequestParser with simple input parsers', (): void => {
|
||||||
@ -17,7 +16,7 @@ describe('A BasicRequestParser with simple input parsers', (): void => {
|
|||||||
const targetExtractor = new OriginalUrlExtractor({ identifierStrategy });
|
const targetExtractor = new OriginalUrlExtractor({ identifierStrategy });
|
||||||
const preferenceParser = new AcceptPreferenceParser();
|
const preferenceParser = new AcceptPreferenceParser();
|
||||||
const metadataParser = new ContentTypeParser();
|
const metadataParser = new ContentTypeParser();
|
||||||
const conditionsParser = new BasicConditionsParser();
|
const conditionsParser = new BasicConditionsParser(new BasicETagHandler());
|
||||||
const bodyParser = new RawBodyParser();
|
const bodyParser = new RawBodyParser();
|
||||||
const requestParser = new BasicRequestParser(
|
const requestParser = new BasicRequestParser(
|
||||||
{ targetExtractor, preferenceParser, metadataParser, conditionsParser, bodyParser },
|
{ targetExtractor, preferenceParser, metadataParser, conditionsParser, bodyParser },
|
||||||
@ -45,7 +44,7 @@ describe('A BasicRequestParser with simple input parsers', (): void => {
|
|||||||
type: { 'text/turtle': 0.8 },
|
type: { 'text/turtle': 0.8 },
|
||||||
language: { 'en-gb': 1, en: 0.5 },
|
language: { 'en-gb': 1, en: 0.5 },
|
||||||
},
|
},
|
||||||
conditions: new BasicConditions({
|
conditions: expect.objectContaining({
|
||||||
unmodifiedSince: new Date('2015-10-21T07:28:00.000Z'),
|
unmodifiedSince: new Date('2015-10-21T07:28:00.000Z'),
|
||||||
notMatchesETag: [ '12345' ],
|
notMatchesETag: [ '12345' ],
|
||||||
}),
|
}),
|
||||||
|
@ -1,14 +1,24 @@
|
|||||||
import { BasicConditionsParser } from '../../../../../src/http/input/conditions/BasicConditionsParser';
|
import { BasicConditionsParser } from '../../../../../src/http/input/conditions/BasicConditionsParser';
|
||||||
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
||||||
|
import type { ETagHandler } from '../../../../../src/storage/conditions/ETagHandler';
|
||||||
|
|
||||||
describe('A BasicConditionsParser', (): void => {
|
describe('A BasicConditionsParser', (): void => {
|
||||||
const dateString = 'Wed, 21 Oct 2015 07:28:00 UTC';
|
const dateString = 'Wed, 21 Oct 2015 07:28:00 UTC';
|
||||||
const date = new Date('2015-10-21T07:28:00.000Z');
|
const date = new Date('2015-10-21T07:28:00.000Z');
|
||||||
let request: HttpRequest;
|
let request: HttpRequest;
|
||||||
const parser = new BasicConditionsParser();
|
let eTagHandler: ETagHandler;
|
||||||
|
let parser: BasicConditionsParser;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
request = { headers: {}, method: 'GET' } as HttpRequest;
|
request = { headers: {}, method: 'GET' } as HttpRequest;
|
||||||
|
|
||||||
|
eTagHandler = {
|
||||||
|
getETag: jest.fn(),
|
||||||
|
matchesETag: jest.fn(),
|
||||||
|
sameResourceState: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
parser = new BasicConditionsParser(eTagHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns undefined if there are no relevant headers.', async(): Promise<void> => {
|
it('returns undefined if there are no relevant headers.', async(): Promise<void> => {
|
||||||
@ -17,28 +27,29 @@ describe('A BasicConditionsParser', (): void => {
|
|||||||
|
|
||||||
it('parses the if-modified-since header.', async(): Promise<void> => {
|
it('parses the if-modified-since header.', async(): Promise<void> => {
|
||||||
request.headers['if-modified-since'] = dateString;
|
request.headers['if-modified-since'] = dateString;
|
||||||
await expect(parser.handleSafe(request)).resolves.toEqual({ modifiedSince: date });
|
await expect(parser.handleSafe(request)).resolves.toEqual({ eTagHandler, modifiedSince: date });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses the if-unmodified-since header.', async(): Promise<void> => {
|
it('parses the if-unmodified-since header.', async(): Promise<void> => {
|
||||||
request.headers['if-unmodified-since'] = dateString;
|
request.headers['if-unmodified-since'] = dateString;
|
||||||
await expect(parser.handleSafe(request)).resolves.toEqual({ unmodifiedSince: date });
|
await expect(parser.handleSafe(request)).resolves.toEqual({ eTagHandler, unmodifiedSince: date });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses the if-match header.', async(): Promise<void> => {
|
it('parses the if-match header.', async(): Promise<void> => {
|
||||||
request.headers['if-match'] = '"1234567", "abcdefg"';
|
request.headers['if-match'] = '"1234567", "abcdefg"';
|
||||||
await expect(parser.handleSafe(request)).resolves.toEqual({ matchesETag: [ '"1234567"', '"abcdefg"' ]});
|
await expect(parser.handleSafe(request)).resolves
|
||||||
|
.toEqual({ eTagHandler, matchesETag: [ '"1234567"', '"abcdefg"' ]});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses the if-none-match header.', async(): Promise<void> => {
|
it('parses the if-none-match header.', async(): Promise<void> => {
|
||||||
request.headers['if-none-match'] = '*';
|
request.headers['if-none-match'] = '*';
|
||||||
await expect(parser.handleSafe(request)).resolves.toEqual({ notMatchesETag: [ '*' ]});
|
await expect(parser.handleSafe(request)).resolves.toEqual({ eTagHandler, notMatchesETag: [ '*' ]});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not parse the if-modified-since header if there is an if-none-match header.', async(): Promise<void> => {
|
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-modified-since'] = dateString;
|
||||||
request.headers['if-none-match'] = '*';
|
request.headers['if-none-match'] = '*';
|
||||||
await expect(parser.handleSafe(request)).resolves.toEqual({ notMatchesETag: [ '*' ]});
|
await expect(parser.handleSafe(request)).resolves.toEqual({ eTagHandler, notMatchesETag: [ '*' ]});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only parses the if-modified-since header for GET and HEAD requests.', async(): Promise<void> => {
|
it('only parses the if-modified-since header for GET and HEAD requests.', async(): Promise<void> => {
|
||||||
@ -50,7 +61,7 @@ describe('A BasicConditionsParser', (): void => {
|
|||||||
it('does not parse the if-unmodified-since header if there is an if-match header.', async(): Promise<void> => {
|
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-unmodified-since'] = dateString;
|
||||||
request.headers['if-match'] = '*';
|
request.headers['if-match'] = '*';
|
||||||
await expect(parser.handleSafe(request)).resolves.toEqual({ matchesETag: [ '*' ]});
|
await expect(parser.handleSafe(request)).resolves.toEqual({ eTagHandler, matchesETag: [ '*' ]});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores invalid dates.', async(): Promise<void> => {
|
it('ignores invalid dates.', async(): Promise<void> => {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { DeleteOperationHandler } from '../../../../src/http/ldp/DeleteOperationHandler';
|
import { DeleteOperationHandler } from '../../../../src/http/ldp/DeleteOperationHandler';
|
||||||
import type { Operation } from '../../../../src/http/Operation';
|
import type { Operation } from '../../../../src/http/Operation';
|
||||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||||
import { BasicConditions } from '../../../../src/storage/conditions/BasicConditions';
|
import type { Conditions } from '../../../../src/storage/conditions/Conditions';
|
||||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||||
|
|
||||||
describe('A DeleteOperationHandler', (): void => {
|
describe('A DeleteOperationHandler', (): void => {
|
||||||
let operation: Operation;
|
let operation: Operation;
|
||||||
const conditions = new BasicConditions({});
|
const conditions: Conditions = { matchesMetadata: jest.fn() };
|
||||||
const body = new BasicRepresentation();
|
const body = new BasicRepresentation();
|
||||||
const store = {} as unknown as ResourceStore;
|
const store = {} as unknown as ResourceStore;
|
||||||
const handler = new DeleteOperationHandler(store);
|
const handler = new DeleteOperationHandler(store);
|
||||||
|
@ -4,8 +4,8 @@ import type { Operation } from '../../../../src/http/Operation';
|
|||||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../../../src/http/representation/Representation';
|
import type { Representation } from '../../../../src/http/representation/Representation';
|
||||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||||
import { BasicConditions } from '../../../../src/storage/conditions/BasicConditions';
|
import type { Conditions } from '../../../../src/storage/conditions/Conditions';
|
||||||
import { getETag } from '../../../../src/storage/conditions/Conditions';
|
import type { ETagHandler } from '../../../../src/storage/conditions/ETagHandler';
|
||||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||||
import { NotModifiedHttpError } from '../../../../src/util/errors/NotModifiedHttpError';
|
import { NotModifiedHttpError } from '../../../../src/util/errors/NotModifiedHttpError';
|
||||||
@ -14,25 +14,37 @@ import { CONTENT_TYPE, HH } from '../../../../src/util/Vocabularies';
|
|||||||
|
|
||||||
describe('A GetOperationHandler', (): void => {
|
describe('A GetOperationHandler', (): void => {
|
||||||
let operation: Operation;
|
let operation: Operation;
|
||||||
const conditions = new BasicConditions({});
|
let conditions: jest.Mocked<Conditions>;
|
||||||
const preferences = {};
|
const preferences = {};
|
||||||
const body = new BasicRepresentation();
|
const body = new BasicRepresentation();
|
||||||
let store: ResourceStore;
|
let store: ResourceStore;
|
||||||
|
let eTagHandler: ETagHandler;
|
||||||
let handler: GetOperationHandler;
|
let handler: GetOperationHandler;
|
||||||
let data: Readable;
|
let data: Readable;
|
||||||
let metadata: RepresentationMetadata;
|
let metadata: RepresentationMetadata;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
|
conditions = {
|
||||||
|
matchesMetadata: jest.fn().mockReturnValue(true),
|
||||||
|
};
|
||||||
|
|
||||||
operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences, conditions, body };
|
operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences, conditions, body };
|
||||||
data = { destroy: jest.fn() } as any;
|
data = { destroy: jest.fn() } as any;
|
||||||
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
||||||
updateModifiedDate(metadata);
|
updateModifiedDate(metadata);
|
||||||
|
|
||||||
store = {
|
store = {
|
||||||
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
||||||
({ binary: false, data, metadata } as any)),
|
({ binary: false, data, metadata } as any)),
|
||||||
} as unknown as ResourceStore;
|
} as unknown as ResourceStore;
|
||||||
|
|
||||||
handler = new GetOperationHandler(store);
|
eTagHandler = {
|
||||||
|
getETag: jest.fn().mockReturnValue('ETag'),
|
||||||
|
matchesETag: jest.fn(),
|
||||||
|
sameResourceState: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
handler = new GetOperationHandler(store, eTagHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only supports GET operations.', async(): Promise<void> => {
|
it('only supports GET operations.', async(): Promise<void> => {
|
||||||
@ -45,16 +57,14 @@ describe('A GetOperationHandler', (): void => {
|
|||||||
const result = await handler.handle({ operation });
|
const result = await handler.handle({ operation });
|
||||||
expect(result.statusCode).toBe(200);
|
expect(result.statusCode).toBe(200);
|
||||||
expect(result.metadata).toBe(metadata);
|
expect(result.metadata).toBe(metadata);
|
||||||
expect(metadata.get(HH.terms.etag)?.value).toBe(getETag(metadata));
|
expect(metadata.get(HH.terms.etag)?.value).toBe('ETag');
|
||||||
expect(result.data).toBe(data);
|
expect(result.data).toBe(data);
|
||||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||||
expect(store.getRepresentation).toHaveBeenLastCalledWith(operation.target, preferences, conditions);
|
expect(store.getRepresentation).toHaveBeenLastCalledWith(operation.target, preferences, conditions);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a 304 if the conditions do not match.', async(): Promise<void> => {
|
it('returns a 304 if the conditions do not match.', async(): Promise<void> => {
|
||||||
operation.conditions = {
|
conditions.matchesMetadata.mockReturnValue(false);
|
||||||
matchesMetadata: (): boolean => false,
|
|
||||||
};
|
|
||||||
await expect(handler.handle({ operation })).rejects.toThrow(NotModifiedHttpError);
|
await expect(handler.handle({ operation })).rejects.toThrow(NotModifiedHttpError);
|
||||||
expect(data.destroy).toHaveBeenCalledTimes(1);
|
expect(data.destroy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
@ -4,8 +4,8 @@ import type { Operation } from '../../../../src/http/Operation';
|
|||||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../../../src/http/representation/Representation';
|
import type { Representation } from '../../../../src/http/representation/Representation';
|
||||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||||
import { BasicConditions } from '../../../../src/storage/conditions/BasicConditions';
|
import type { Conditions } from '../../../../src/storage/conditions/Conditions';
|
||||||
import { getETag } from '../../../../src/storage/conditions/Conditions';
|
import type { ETagHandler } from '../../../../src/storage/conditions/ETagHandler';
|
||||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||||
import { NotModifiedHttpError } from '../../../../src/util/errors/NotModifiedHttpError';
|
import { NotModifiedHttpError } from '../../../../src/util/errors/NotModifiedHttpError';
|
||||||
@ -14,25 +14,37 @@ import { CONTENT_TYPE, HH } from '../../../../src/util/Vocabularies';
|
|||||||
|
|
||||||
describe('A HeadOperationHandler', (): void => {
|
describe('A HeadOperationHandler', (): void => {
|
||||||
let operation: Operation;
|
let operation: Operation;
|
||||||
const conditions = new BasicConditions({});
|
let conditions: jest.Mocked<Conditions>;
|
||||||
const preferences = {};
|
const preferences = {};
|
||||||
const body = new BasicRepresentation();
|
const body = new BasicRepresentation();
|
||||||
let store: ResourceStore;
|
let store: ResourceStore;
|
||||||
|
let eTagHandler: ETagHandler;
|
||||||
let handler: HeadOperationHandler;
|
let handler: HeadOperationHandler;
|
||||||
let data: Readable;
|
let data: Readable;
|
||||||
let metadata: RepresentationMetadata;
|
let metadata: RepresentationMetadata;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
|
conditions = {
|
||||||
|
matchesMetadata: jest.fn().mockReturnValue(true),
|
||||||
|
};
|
||||||
|
|
||||||
operation = { method: 'HEAD', target: { path: 'http://test.com/foo' }, preferences, conditions, body };
|
operation = { method: 'HEAD', target: { path: 'http://test.com/foo' }, preferences, conditions, body };
|
||||||
data = { destroy: jest.fn() } as any;
|
data = { destroy: jest.fn() } as any;
|
||||||
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
||||||
updateModifiedDate(metadata);
|
updateModifiedDate(metadata);
|
||||||
|
|
||||||
store = {
|
store = {
|
||||||
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
getRepresentation: jest.fn(async(): Promise<Representation> =>
|
||||||
({ binary: false, data, metadata } as any)),
|
({ binary: false, data, metadata } as any)),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
handler = new HeadOperationHandler(store);
|
eTagHandler = {
|
||||||
|
getETag: jest.fn().mockReturnValue('ETag'),
|
||||||
|
matchesETag: jest.fn(),
|
||||||
|
sameResourceState: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
handler = new HeadOperationHandler(store, eTagHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only supports HEAD operations.', async(): Promise<void> => {
|
it('only supports HEAD operations.', async(): Promise<void> => {
|
||||||
@ -47,7 +59,7 @@ describe('A HeadOperationHandler', (): void => {
|
|||||||
const result = await handler.handle({ operation });
|
const result = await handler.handle({ operation });
|
||||||
expect(result.statusCode).toBe(200);
|
expect(result.statusCode).toBe(200);
|
||||||
expect(result.metadata).toBe(metadata);
|
expect(result.metadata).toBe(metadata);
|
||||||
expect(metadata.get(HH.terms.etag)?.value).toBe(getETag(metadata));
|
expect(metadata.get(HH.terms.etag)?.value).toBe('ETag');
|
||||||
expect(result.data).toBeUndefined();
|
expect(result.data).toBeUndefined();
|
||||||
expect(data.destroy).toHaveBeenCalledTimes(1);
|
expect(data.destroy).toHaveBeenCalledTimes(1);
|
||||||
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
expect(store.getRepresentation).toHaveBeenCalledTimes(1);
|
||||||
@ -55,9 +67,7 @@ describe('A HeadOperationHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns a 304 if the conditions do not match.', async(): Promise<void> => {
|
it('returns a 304 if the conditions do not match.', async(): Promise<void> => {
|
||||||
operation.conditions = {
|
conditions.matchesMetadata.mockReturnValue(false);
|
||||||
matchesMetadata: (): boolean => false,
|
|
||||||
};
|
|
||||||
await expect(handler.handle({ operation })).rejects.toThrow(NotModifiedHttpError);
|
await expect(handler.handle({ operation })).rejects.toThrow(NotModifiedHttpError);
|
||||||
expect(data.destroy).toHaveBeenCalledTimes(2);
|
expect(data.destroy).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,7 @@ import { PatchOperationHandler } from '../../../../src/http/ldp/PatchOperationHa
|
|||||||
import type { Operation } from '../../../../src/http/Operation';
|
import type { Operation } from '../../../../src/http/Operation';
|
||||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../../../src/http/representation/Representation';
|
import type { Representation } from '../../../../src/http/representation/Representation';
|
||||||
import { BasicConditions } from '../../../../src/storage/conditions/BasicConditions';
|
import type { Conditions } from '../../../../src/storage/conditions/Conditions';
|
||||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
|
||||||
@ -11,7 +11,7 @@ import { SOLID_HTTP } from '../../../../src/util/Vocabularies';
|
|||||||
describe('A PatchOperationHandler', (): void => {
|
describe('A PatchOperationHandler', (): void => {
|
||||||
let operation: Operation;
|
let operation: Operation;
|
||||||
let body: Representation;
|
let body: Representation;
|
||||||
const conditions = new BasicConditions({});
|
const conditions: Conditions = { matchesMetadata: jest.fn() };
|
||||||
let store: jest.Mocked<ResourceStore>;
|
let store: jest.Mocked<ResourceStore>;
|
||||||
let handler: PatchOperationHandler;
|
let handler: PatchOperationHandler;
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import type { Operation } from '../../../../src/http/Operation';
|
|||||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../../../src/http/representation/Representation';
|
import type { Representation } from '../../../../src/http/representation/Representation';
|
||||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||||
import { BasicConditions } from '../../../../src/storage/conditions/BasicConditions';
|
import type { Conditions } from '../../../../src/storage/conditions/Conditions';
|
||||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
||||||
@ -14,7 +14,7 @@ import { AS, LDP, RDF, SOLID_AS, SOLID_HTTP } from '../../../../src/util/Vocabul
|
|||||||
describe('A PostOperationHandler', (): void => {
|
describe('A PostOperationHandler', (): void => {
|
||||||
let operation: Operation;
|
let operation: Operation;
|
||||||
let body: Representation;
|
let body: Representation;
|
||||||
const conditions = new BasicConditions({});
|
const conditions: Conditions = { matchesMetadata: jest.fn() };
|
||||||
let store: jest.Mocked<ResourceStore>;
|
let store: jest.Mocked<ResourceStore>;
|
||||||
let handler: PostOperationHandler;
|
let handler: PostOperationHandler;
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { PutOperationHandler } from '../../../../src/http/ldp/PutOperationHandle
|
|||||||
import type { Operation } from '../../../../src/http/Operation';
|
import type { Operation } from '../../../../src/http/Operation';
|
||||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../../../src/http/representation/Representation';
|
import type { Representation } from '../../../../src/http/representation/Representation';
|
||||||
import { BasicConditions } from '../../../../src/storage/conditions/BasicConditions';
|
import type { Conditions } from '../../../../src/storage/conditions/Conditions';
|
||||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
import { MethodNotAllowedHttpError } from '../../../../src/util/errors/MethodNotAllowedHttpError';
|
import { MethodNotAllowedHttpError } from '../../../../src/util/errors/MethodNotAllowedHttpError';
|
||||||
@ -13,7 +13,7 @@ import { SimpleSuffixStrategy } from '../../../util/SimpleSuffixStrategy';
|
|||||||
describe('A PutOperationHandler', (): void => {
|
describe('A PutOperationHandler', (): void => {
|
||||||
let operation: Operation;
|
let operation: Operation;
|
||||||
let body: Representation;
|
let body: Representation;
|
||||||
const conditions = new BasicConditions({});
|
const conditions: Conditions = { matchesMetadata: jest.fn() };
|
||||||
let store: jest.Mocked<ResourceStore>;
|
let store: jest.Mocked<ResourceStore>;
|
||||||
let handler: PutOperationHandler;
|
let handler: PutOperationHandler;
|
||||||
const metaStrategy = new SimpleSuffixStrategy('.meta');
|
const metaStrategy = new SimpleSuffixStrategy('.meta');
|
||||||
|
@ -6,6 +6,7 @@ import type { Notification } from '../../../../src/server/notifications/Notifica
|
|||||||
import type { NotificationChannel } from '../../../../src/server/notifications/NotificationChannel';
|
import type { NotificationChannel } from '../../../../src/server/notifications/NotificationChannel';
|
||||||
import type { NotificationEmitter } from '../../../../src/server/notifications/NotificationEmitter';
|
import type { NotificationEmitter } from '../../../../src/server/notifications/NotificationEmitter';
|
||||||
import type { NotificationSerializer } from '../../../../src/server/notifications/serialize/NotificationSerializer';
|
import type { NotificationSerializer } from '../../../../src/server/notifications/serialize/NotificationSerializer';
|
||||||
|
import type { ETagHandler } from '../../../../src/storage/conditions/ETagHandler';
|
||||||
|
|
||||||
describe('A ComposedNotificationHandler', (): void => {
|
describe('A ComposedNotificationHandler', (): void => {
|
||||||
const topic: ResourceIdentifier = { path: 'http://example.com/foo' };
|
const topic: ResourceIdentifier = { path: 'http://example.com/foo' };
|
||||||
@ -25,6 +26,7 @@ describe('A ComposedNotificationHandler', (): void => {
|
|||||||
let generator: jest.Mocked<NotificationGenerator>;
|
let generator: jest.Mocked<NotificationGenerator>;
|
||||||
let serializer: jest.Mocked<NotificationSerializer>;
|
let serializer: jest.Mocked<NotificationSerializer>;
|
||||||
let emitter: jest.Mocked<NotificationEmitter>;
|
let emitter: jest.Mocked<NotificationEmitter>;
|
||||||
|
let eTagHandler: jest.Mocked<ETagHandler>;
|
||||||
let handler: ComposedNotificationHandler;
|
let handler: ComposedNotificationHandler;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
@ -47,7 +49,13 @@ describe('A ComposedNotificationHandler', (): void => {
|
|||||||
handleSafe: jest.fn(),
|
handleSafe: jest.fn(),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
handler = new ComposedNotificationHandler({ generator, serializer, emitter });
|
eTagHandler = {
|
||||||
|
getETag: jest.fn(),
|
||||||
|
matchesETag: jest.fn(),
|
||||||
|
sameResourceState: jest.fn().mockReturnValue(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
handler = new ComposedNotificationHandler({ generator, serializer, emitter, eTagHandler });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can only handle input supported by the generator.', async(): Promise<void> => {
|
it('can only handle input supported by the generator.', async(): Promise<void> => {
|
||||||
@ -68,6 +76,7 @@ describe('A ComposedNotificationHandler', (): void => {
|
|||||||
|
|
||||||
it('does not emit the notification if it has the same resource state as the channel.', async(): Promise<void> => {
|
it('does not emit the notification if it has the same resource state as the channel.', async(): Promise<void> => {
|
||||||
channel.state = '"123456-application/ld+json"';
|
channel.state = '"123456-application/ld+json"';
|
||||||
|
eTagHandler.sameResourceState.mockReturnValue(true);
|
||||||
await expect(handler.handle({ channel, topic })).resolves.toBeUndefined();
|
await expect(handler.handle({ channel, topic })).resolves.toBeUndefined();
|
||||||
expect(generator.handle).toHaveBeenCalledTimes(1);
|
expect(generator.handle).toHaveBeenCalledTimes(1);
|
||||||
expect(generator.handle).toHaveBeenLastCalledWith({ channel, topic });
|
expect(generator.handle).toHaveBeenLastCalledWith({ channel, topic });
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
ActivityNotificationGenerator,
|
ActivityNotificationGenerator,
|
||||||
} from '../../../../../src/server/notifications/generate/ActivityNotificationGenerator';
|
} from '../../../../../src/server/notifications/generate/ActivityNotificationGenerator';
|
||||||
import type { NotificationChannel } from '../../../../../src/server/notifications/NotificationChannel';
|
import type { NotificationChannel } from '../../../../../src/server/notifications/NotificationChannel';
|
||||||
|
import type { ETagHandler } from '../../../../../src/storage/conditions/ETagHandler';
|
||||||
import type { ResourceStore } from '../../../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../../../src/storage/ResourceStore';
|
||||||
import { AS, CONTENT_TYPE, DC, LDP, RDF } from '../../../../../src/util/Vocabularies';
|
import { AS, CONTENT_TYPE, DC, LDP, RDF } from '../../../../../src/util/Vocabularies';
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ describe('An ActivityNotificationGenerator', (): void => {
|
|||||||
[CONTENT_TYPE]: 'text/turtle',
|
[CONTENT_TYPE]: 'text/turtle',
|
||||||
});
|
});
|
||||||
let store: jest.Mocked<ResourceStore>;
|
let store: jest.Mocked<ResourceStore>;
|
||||||
|
let eTagHandler: jest.Mocked<ETagHandler>;
|
||||||
let generator: ActivityNotificationGenerator;
|
let generator: ActivityNotificationGenerator;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
@ -30,7 +32,13 @@ describe('An ActivityNotificationGenerator', (): void => {
|
|||||||
getRepresentation: jest.fn().mockResolvedValue(new BasicRepresentation('', metadata)),
|
getRepresentation: jest.fn().mockResolvedValue(new BasicRepresentation('', metadata)),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
generator = new ActivityNotificationGenerator(store);
|
eTagHandler = {
|
||||||
|
getETag: jest.fn().mockReturnValue('ETag'),
|
||||||
|
matchesETag: jest.fn(),
|
||||||
|
sameResourceState: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
generator = new ActivityNotificationGenerator(store, eTagHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only handles defined activities.', async(): Promise<void> => {
|
it('only handles defined activities.', async(): Promise<void> => {
|
||||||
@ -52,7 +60,7 @@ describe('An ActivityNotificationGenerator', (): void => {
|
|||||||
id: `urn:${ms}:http://example.com/foo`,
|
id: `urn:${ms}:http://example.com/foo`,
|
||||||
type: 'Update',
|
type: 'Update',
|
||||||
object: 'http://example.com/foo',
|
object: 'http://example.com/foo',
|
||||||
state: expect.stringMatching(/"\d+-text\/turtle"/u),
|
state: 'ETag',
|
||||||
published: date,
|
published: date,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
AddRemoveNotificationGenerator,
|
AddRemoveNotificationGenerator,
|
||||||
} from '../../../../../src/server/notifications/generate/AddRemoveNotificationGenerator';
|
} from '../../../../../src/server/notifications/generate/AddRemoveNotificationGenerator';
|
||||||
import type { NotificationChannel } from '../../../../../src/server/notifications/NotificationChannel';
|
import type { NotificationChannel } from '../../../../../src/server/notifications/NotificationChannel';
|
||||||
|
import type { ETagHandler } from '../../../../../src/storage/conditions/ETagHandler';
|
||||||
import type { ResourceStore } from '../../../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../../../src/storage/ResourceStore';
|
||||||
import { AS, CONTENT_TYPE, DC, LDP, RDF } from '../../../../../src/util/Vocabularies';
|
import { AS, CONTENT_TYPE, DC, LDP, RDF } from '../../../../../src/util/Vocabularies';
|
||||||
|
|
||||||
@ -18,6 +19,7 @@ describe('An AddRemoveNotificationGenerator', (): void => {
|
|||||||
};
|
};
|
||||||
let metadata: RepresentationMetadata;
|
let metadata: RepresentationMetadata;
|
||||||
let store: jest.Mocked<ResourceStore>;
|
let store: jest.Mocked<ResourceStore>;
|
||||||
|
let eTagHandler: jest.Mocked<ETagHandler>;
|
||||||
let generator: AddRemoveNotificationGenerator;
|
let generator: AddRemoveNotificationGenerator;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
@ -33,7 +35,13 @@ describe('An AddRemoveNotificationGenerator', (): void => {
|
|||||||
getRepresentation: jest.fn().mockResolvedValue(new BasicRepresentation('', responseMetadata)),
|
getRepresentation: jest.fn().mockResolvedValue(new BasicRepresentation('', responseMetadata)),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
generator = new AddRemoveNotificationGenerator(store);
|
eTagHandler = {
|
||||||
|
getETag: jest.fn().mockReturnValue('ETag'),
|
||||||
|
matchesETag: jest.fn(),
|
||||||
|
sameResourceState: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
generator = new AddRemoveNotificationGenerator(store, eTagHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only handles Add/Remove activities.', async(): Promise<void> => {
|
it('only handles Add/Remove activities.', async(): Promise<void> => {
|
||||||
@ -73,7 +81,7 @@ describe('An AddRemoveNotificationGenerator', (): void => {
|
|||||||
type: 'Add',
|
type: 'Add',
|
||||||
object: 'http://example.com/foo',
|
object: 'http://example.com/foo',
|
||||||
target: 'http://example.com/',
|
target: 'http://example.com/',
|
||||||
state: expect.stringMatching(/"\d+-text\/turtle"/u),
|
state: 'ETag',
|
||||||
published: date,
|
published: date,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import 'jest-rdf';
|
|||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
import arrayifyStream from 'arrayify-stream';
|
import arrayifyStream from 'arrayify-stream';
|
||||||
import { DataFactory, Store } from 'n3';
|
import { DataFactory, Store } from 'n3';
|
||||||
|
import type { Conditions } from '../../../src';
|
||||||
import { CONTENT_TYPE_TERM } from '../../../src';
|
import { CONTENT_TYPE_TERM } from '../../../src';
|
||||||
import type { AuxiliaryStrategy } from '../../../src/http/auxiliary/AuxiliaryStrategy';
|
import type { AuxiliaryStrategy } from '../../../src/http/auxiliary/AuxiliaryStrategy';
|
||||||
import { BasicRepresentation } from '../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../src/http/representation/BasicRepresentation';
|
||||||
@ -9,7 +10,6 @@ import type { Representation } from '../../../src/http/representation/Representa
|
|||||||
import { RepresentationMetadata } from '../../../src/http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../src/http/representation/RepresentationMetadata';
|
||||||
import type { ResourceIdentifier } from '../../../src/http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../../src/http/representation/ResourceIdentifier';
|
||||||
import type { DataAccessor } from '../../../src/storage/accessors/DataAccessor';
|
import type { DataAccessor } from '../../../src/storage/accessors/DataAccessor';
|
||||||
import { BasicConditions } from '../../../src/storage/conditions/BasicConditions';
|
|
||||||
import { DataAccessorBasedStore } from '../../../src/storage/DataAccessorBasedStore';
|
import { DataAccessorBasedStore } from '../../../src/storage/DataAccessorBasedStore';
|
||||||
import { INTERNAL_QUADS } from '../../../src/util/ContentTypes';
|
import { INTERNAL_QUADS } from '../../../src/util/ContentTypes';
|
||||||
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
|
||||||
@ -103,6 +103,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
let auxiliaryStrategy: AuxiliaryStrategy;
|
let auxiliaryStrategy: AuxiliaryStrategy;
|
||||||
let containerMetadata: RepresentationMetadata;
|
let containerMetadata: RepresentationMetadata;
|
||||||
let representation: Representation;
|
let representation: Representation;
|
||||||
|
const failingConditions: Conditions = { matchesMetadata: (): boolean => false };
|
||||||
const resourceData = 'text';
|
const resourceData = 'text';
|
||||||
const metadataStrategy = new SimpleSuffixStrategy('.meta');
|
const metadataStrategy = new SimpleSuffixStrategy('.meta');
|
||||||
|
|
||||||
@ -256,8 +257,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
|
|
||||||
it('throws a 412 if the conditions are not matched.', async(): Promise<void> => {
|
it('throws a 412 if the conditions are not matched.', async(): Promise<void> => {
|
||||||
const resourceID = { path: root };
|
const resourceID = { path: root };
|
||||||
const conditions = new BasicConditions({ notMatchesETag: [ '*' ]});
|
await expect(store.addResource(resourceID, representation, failingConditions))
|
||||||
await expect(store.addResource(resourceID, representation, conditions))
|
|
||||||
.rejects.toThrow(PreconditionFailedHttpError);
|
.rejects.toThrow(PreconditionFailedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -417,8 +417,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
it('throws a 412 if the conditions are not matched.', async(): Promise<void> => {
|
it('throws a 412 if the conditions are not matched.', async(): Promise<void> => {
|
||||||
const resourceID = { path: `${root}resource` };
|
const resourceID = { path: `${root}resource` };
|
||||||
await store.setRepresentation(resourceID, representation);
|
await store.setRepresentation(resourceID, representation);
|
||||||
const conditions = new BasicConditions({ notMatchesETag: [ '*' ]});
|
await expect(store.setRepresentation(resourceID, representation, failingConditions))
|
||||||
await expect(store.setRepresentation(resourceID, representation, conditions))
|
|
||||||
.rejects.toThrow(PreconditionFailedHttpError);
|
.rejects.toThrow(PreconditionFailedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -697,15 +696,13 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
describe('modifying a Representation', (): void => {
|
describe('modifying a Representation', (): void => {
|
||||||
it('throws a 412 if the conditions are not matched.', async(): Promise<void> => {
|
it('throws a 412 if the conditions are not matched.', async(): Promise<void> => {
|
||||||
const resourceID = { path: root };
|
const resourceID = { path: root };
|
||||||
const conditions = new BasicConditions({ notMatchesETag: [ '*' ]});
|
await expect(store.modifyResource(resourceID, representation, failingConditions))
|
||||||
await expect(store.modifyResource(resourceID, representation, conditions))
|
|
||||||
.rejects.toThrow(PreconditionFailedHttpError);
|
.rejects.toThrow(PreconditionFailedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws a 412 if the conditions are not matched on resources that do not exist.', async(): Promise<void> => {
|
it('throws a 412 if the conditions are not matched on resources that do not exist.', async(): Promise<void> => {
|
||||||
const resourceID = { path: `${root}notHere` };
|
const resourceID = { path: `${root}notHere` };
|
||||||
const conditions = new BasicConditions({ matchesETag: [ '*' ]});
|
await expect(store.modifyResource(resourceID, representation, failingConditions))
|
||||||
await expect(store.modifyResource(resourceID, representation, conditions))
|
|
||||||
.rejects.toThrow(PreconditionFailedHttpError);
|
.rejects.toThrow(PreconditionFailedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -715,8 +712,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resourceID = { path: root };
|
const resourceID = { path: root };
|
||||||
const conditions = new BasicConditions({ notMatchesETag: [ '*' ]});
|
await expect(store.modifyResource(resourceID, representation, failingConditions))
|
||||||
await expect(store.modifyResource(resourceID, representation, conditions))
|
|
||||||
.rejects.toThrow('error');
|
.rejects.toThrow('error');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -764,8 +760,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
|
|
||||||
it('throws a 412 if the conditions are not matched.', async(): Promise<void> => {
|
it('throws a 412 if the conditions are not matched.', async(): Promise<void> => {
|
||||||
const resourceID = { path: root };
|
const resourceID = { path: root };
|
||||||
const conditions = new BasicConditions({ notMatchesETag: [ '*' ]});
|
await expect(store.deleteResource(resourceID, failingConditions))
|
||||||
await expect(store.deleteResource(resourceID, conditions))
|
|
||||||
.rejects.toThrow(PreconditionFailedHttpError);
|
.rejects.toThrow(PreconditionFailedHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,89 +1,95 @@
|
|||||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||||
import { BasicConditions } from '../../../../src/storage/conditions/BasicConditions';
|
import { BasicConditions } from '../../../../src/storage/conditions/BasicConditions';
|
||||||
import { getETag } from '../../../../src/storage/conditions/Conditions';
|
import type { ETagHandler } from '../../../../src/storage/conditions/ETagHandler';
|
||||||
import { CONTENT_TYPE, DC } from '../../../../src/util/Vocabularies';
|
import { DC } from '../../../../src/util/Vocabularies';
|
||||||
|
|
||||||
function getMetadata(modified: Date, type = 'application/ld+json'): RepresentationMetadata {
|
|
||||||
return new RepresentationMetadata({
|
|
||||||
[DC.modified]: `${modified.toISOString()}`,
|
|
||||||
[CONTENT_TYPE]: type,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('A BasicConditions', (): void => {
|
describe('A BasicConditions', (): void => {
|
||||||
const now = new Date(2020, 10, 20);
|
const now = new Date(2020, 10, 20);
|
||||||
const tomorrow = new Date(2020, 10, 21);
|
const tomorrow = new Date(2020, 10, 21);
|
||||||
const yesterday = new Date(2020, 10, 19);
|
const yesterday = new Date(2020, 10, 19);
|
||||||
const turtleTag = getETag(getMetadata(now, 'text/turtle'))!;
|
const eTag = `"${now.getTime()}-text/turtle"`;
|
||||||
const jsonLdTag = getETag(getMetadata(now))!;
|
let eTagHandler: jest.Mocked<ETagHandler>;
|
||||||
|
let metadata: RepresentationMetadata;
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
eTagHandler = {
|
||||||
|
getETag: jest.fn(),
|
||||||
|
matchesETag: jest.fn().mockReturnValue(true),
|
||||||
|
sameResourceState: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
metadata = new RepresentationMetadata({ [DC.modified]: `${now.toISOString()}` });
|
||||||
|
});
|
||||||
|
|
||||||
it('copies the input parameters.', async(): Promise<void> => {
|
it('copies the input parameters.', async(): Promise<void> => {
|
||||||
const eTags = [ '123456', 'abcdefg' ];
|
const eTags = [ '123456', 'abcdefg' ];
|
||||||
const options = { matchesETag: eTags, notMatchesETag: eTags, modifiedSince: now, unmodifiedSince: now };
|
const options = { matchesETag: eTags, notMatchesETag: eTags, modifiedSince: now, unmodifiedSince: now };
|
||||||
expect(new BasicConditions(options)).toMatchObject(options);
|
expect(new BasicConditions(eTagHandler, options)).toMatchObject(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('always returns false if notMatchesETag contains *.', async(): Promise<void> => {
|
it('always returns false if notMatchesETag contains *.', async(): Promise<void> => {
|
||||||
const conditions = new BasicConditions({ notMatchesETag: [ '*' ]});
|
const conditions = new BasicConditions(eTagHandler, { notMatchesETag: [ '*' ]});
|
||||||
expect(conditions.matchesMetadata(new RepresentationMetadata())).toBe(false);
|
expect(conditions.matchesMetadata(new RepresentationMetadata())).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('requires matchesETag to match the provided ETag timestamp.', async(): Promise<void> => {
|
it('requires matchesETag to match the provided ETag with the metadata.', async(): Promise<void> => {
|
||||||
const conditions = new BasicConditions({ matchesETag: [ turtleTag ]});
|
const conditions = new BasicConditions(eTagHandler, { matchesETag: [ eTag ]});
|
||||||
expect(conditions.matchesMetadata(getMetadata(yesterday))).toBe(false);
|
expect(conditions.matchesMetadata(metadata)).toBe(true);
|
||||||
expect(conditions.matchesMetadata(getMetadata(now))).toBe(true);
|
expect(eTagHandler.matchesETag).toHaveBeenCalledTimes(1);
|
||||||
|
expect(eTagHandler.matchesETag).toHaveBeenLastCalledWith(metadata, eTag, false);
|
||||||
|
|
||||||
|
eTagHandler.matchesETag.mockReturnValue(false);
|
||||||
|
expect(conditions.matchesMetadata(metadata)).toBe(false);
|
||||||
|
expect(eTagHandler.matchesETag).toHaveBeenCalledTimes(2);
|
||||||
|
expect(eTagHandler.matchesETag).toHaveBeenLastCalledWith(metadata, eTag, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('requires matchesETag to match the exact provided ETag in strict mode.', async(): Promise<void> => {
|
it('calls the ETagHandler in strict mode if required.', async(): Promise<void> => {
|
||||||
const turtleConditions = new BasicConditions({ matchesETag: [ turtleTag ]});
|
const conditions = new BasicConditions(eTagHandler, { matchesETag: [ eTag ]});
|
||||||
const jsonLdConditions = new BasicConditions({ matchesETag: [ jsonLdTag ]});
|
expect(conditions.matchesMetadata(metadata, true)).toBe(true);
|
||||||
expect(turtleConditions.matchesMetadata(getMetadata(now), true)).toBe(false);
|
expect(eTagHandler.matchesETag).toHaveBeenCalledTimes(1);
|
||||||
expect(jsonLdConditions.matchesMetadata(getMetadata(now), true)).toBe(true);
|
expect(eTagHandler.matchesETag).toHaveBeenLastCalledWith(metadata, eTag, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports all ETags if matchesETag contains *.', async(): Promise<void> => {
|
it('supports all ETags if matchesETag contains *.', async(): Promise<void> => {
|
||||||
const conditions = new BasicConditions({ matchesETag: [ '*' ]});
|
eTagHandler.matchesETag.mockReturnValue(false);
|
||||||
expect(conditions.matchesMetadata(getMetadata(yesterday))).toBe(true);
|
const conditions = new BasicConditions(eTagHandler, { matchesETag: [ '*' ]});
|
||||||
expect(conditions.matchesMetadata(getMetadata(now))).toBe(true);
|
expect(conditions.matchesMetadata(metadata, true)).toBe(true);
|
||||||
|
expect(eTagHandler.matchesETag).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('requires notMatchesETag to not match the provided ETag timestamp.', async(): Promise<void> => {
|
it('requires notMatchesETag to not match the provided ETag with the metadata.', async(): Promise<void> => {
|
||||||
const conditions = new BasicConditions({ notMatchesETag: [ turtleTag ]});
|
const conditions = new BasicConditions(eTagHandler, { notMatchesETag: [ eTag ]});
|
||||||
expect(conditions.matchesMetadata(getMetadata(yesterday))).toBe(true);
|
expect(conditions.matchesMetadata(metadata)).toBe(false);
|
||||||
expect(conditions.matchesMetadata(getMetadata(now))).toBe(false);
|
expect(eTagHandler.matchesETag).toHaveBeenCalledTimes(1);
|
||||||
});
|
expect(eTagHandler.matchesETag).toHaveBeenLastCalledWith(metadata, eTag, false);
|
||||||
|
|
||||||
it('requires notMatchesETag to not match the exact provided ETag in strict mode.', async(): Promise<void> => {
|
eTagHandler.matchesETag.mockReturnValue(false);
|
||||||
const turtleConditions = new BasicConditions({ notMatchesETag: [ turtleTag ]});
|
expect(conditions.matchesMetadata(metadata)).toBe(true);
|
||||||
const jsonLdConditions = new BasicConditions({ notMatchesETag: [ jsonLdTag ]});
|
expect(eTagHandler.matchesETag).toHaveBeenCalledTimes(2);
|
||||||
expect(turtleConditions.matchesMetadata(getMetadata(now), true)).toBe(true);
|
expect(eTagHandler.matchesETag).toHaveBeenLastCalledWith(metadata, eTag, false);
|
||||||
expect(jsonLdConditions.matchesMetadata(getMetadata(now), true)).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('requires lastModified to be after modifiedSince.', async(): Promise<void> => {
|
it('requires lastModified to be after modifiedSince.', async(): Promise<void> => {
|
||||||
const conditions = new BasicConditions({ modifiedSince: now });
|
const conditions = new BasicConditions(eTagHandler, { modifiedSince: now });
|
||||||
expect(conditions.matchesMetadata(getMetadata(yesterday))).toBe(false);
|
metadata.set(DC.terms.modified, yesterday.toISOString());
|
||||||
expect(conditions.matchesMetadata(getMetadata(tomorrow))).toBe(true);
|
expect(conditions.matchesMetadata(metadata)).toBe(false);
|
||||||
});
|
|
||||||
|
|
||||||
it('requires lastModified to be before unmodifiedSince.', async(): Promise<void> => {
|
metadata.set(DC.terms.modified, tomorrow.toISOString());
|
||||||
const conditions = new BasicConditions({ unmodifiedSince: now });
|
|
||||||
expect(conditions.matchesMetadata(getMetadata(yesterday))).toBe(true);
|
|
||||||
expect(conditions.matchesMetadata(getMetadata(tomorrow))).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('matches if no date is found in the metadata.', async(): Promise<void> => {
|
|
||||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
|
||||||
const conditions = new BasicConditions({
|
|
||||||
modifiedSince: yesterday,
|
|
||||||
unmodifiedSince: tomorrow,
|
|
||||||
notMatchesETag: [ '123456' ],
|
|
||||||
});
|
|
||||||
expect(conditions.matchesMetadata(metadata)).toBe(true);
|
expect(conditions.matchesMetadata(metadata)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('requires lastModified to be before unmodifiedSince.', async(): Promise<void> => {
|
||||||
|
const conditions = new BasicConditions(eTagHandler, { unmodifiedSince: now });
|
||||||
|
metadata.set(DC.terms.modified, yesterday.toISOString());
|
||||||
|
expect(conditions.matchesMetadata(metadata)).toBe(true);
|
||||||
|
|
||||||
|
metadata.set(DC.terms.modified, tomorrow.toISOString());
|
||||||
|
expect(conditions.matchesMetadata(metadata)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('checks if matchesETag contains * for resources that do not exist.', async(): Promise<void> => {
|
it('checks if matchesETag contains * for resources that do not exist.', async(): Promise<void> => {
|
||||||
expect(new BasicConditions({ matchesETag: [ '*' ]}).matchesMetadata()).toBe(false);
|
expect(new BasicConditions(eTagHandler, { matchesETag: [ '*' ]}).matchesMetadata()).toBe(false);
|
||||||
expect(new BasicConditions({}).matchesMetadata()).toBe(true);
|
expect(new BasicConditions(eTagHandler, {}).matchesMetadata()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
56
test/unit/storage/conditions/BasicETagHandler.test.ts
Normal file
56
test/unit/storage/conditions/BasicETagHandler.test.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||||
|
import { BasicETagHandler } from '../../../../src/storage/conditions/BasicETagHandler';
|
||||||
|
import { DC } from '../../../../src/util/Vocabularies';
|
||||||
|
|
||||||
|
describe('A BasicETagHandler', (): void => {
|
||||||
|
const now = new Date();
|
||||||
|
const contentType = 'text/turtle';
|
||||||
|
const eTag = `"${now.getTime()}-${contentType}"`;
|
||||||
|
let metadata: RepresentationMetadata;
|
||||||
|
const handler = new BasicETagHandler();
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
metadata = new RepresentationMetadata();
|
||||||
|
metadata.add(DC.terms.modified, now.toISOString());
|
||||||
|
metadata.contentType = 'text/turtle';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can generate ETags.', async(): Promise<void> => {
|
||||||
|
expect(handler.getETag(metadata)).toBe(eTag);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not generate an ETag if the last modified date is missing.', async(): Promise<void> => {
|
||||||
|
metadata.removeAll(DC.terms.modified);
|
||||||
|
expect(handler.getETag(metadata)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not generate an ETag if the content-type is missing.', async(): Promise<void> => {
|
||||||
|
metadata.contentType = undefined;
|
||||||
|
expect(handler.getETag(metadata)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can validate an ETag against metadata.', async(): Promise<void> => {
|
||||||
|
expect(handler.matchesETag(metadata, eTag, true)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires a last modified date when comparing metadata with an ETag.', async(): Promise<void> => {
|
||||||
|
metadata.removeAll(DC.terms.modified);
|
||||||
|
expect(handler.matchesETag(metadata, eTag, true)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires a content type when comparing metadata with an ETag.', async(): Promise<void> => {
|
||||||
|
metadata.contentType = undefined;
|
||||||
|
expect(handler.matchesETag(metadata, eTag, true)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not require a content type when comparing metadata with an ETag.', async(): Promise<void> => {
|
||||||
|
metadata.contentType = undefined;
|
||||||
|
expect(handler.matchesETag(metadata, eTag, false)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can verify if 2 ETags reference the same resource state.', async(): Promise<void> => {
|
||||||
|
expect(handler.sameResourceState(eTag, eTag)).toBe(true);
|
||||||
|
expect(handler.sameResourceState(eTag, `"${now.getTime()}-text/plain"`)).toBe(true);
|
||||||
|
expect(handler.sameResourceState(eTag, `"${now.getTime() + 1}-${contentType}"`)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
@ -1,44 +0,0 @@
|
|||||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
|
||||||
import { getETag, sameResourceState } from '../../../../src/storage/conditions/Conditions';
|
|
||||||
import { CONTENT_TYPE, DC } from '../../../../src/util/Vocabularies';
|
|
||||||
|
|
||||||
describe('Conditions', (): void => {
|
|
||||||
describe('#getETag', (): void => {
|
|
||||||
it('creates an ETag based on the date last modified and content-type.', async(): Promise<void> => {
|
|
||||||
const now = new Date();
|
|
||||||
const metadata = new RepresentationMetadata({
|
|
||||||
[DC.modified]: now.toISOString(),
|
|
||||||
[CONTENT_TYPE]: 'text/turtle',
|
|
||||||
});
|
|
||||||
expect(getETag(metadata)).toBe(`"${now.getTime()}-text/turtle"`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates a simpler ETag if no content type was found.', async(): Promise<void> => {
|
|
||||||
const now = new Date();
|
|
||||||
expect(getETag(new RepresentationMetadata({ [DC.modified]: now.toISOString() }))).toBe(`"${now.getTime()}-"`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns undefined if no date found.', async(): Promise<void> => {
|
|
||||||
expect(getETag(new RepresentationMetadata())).toBeUndefined();
|
|
||||||
expect(getETag(new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }))).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sameResourceState', (): void => {
|
|
||||||
const eTag = '"123456-text/turtle"';
|
|
||||||
const eTagJson = '"123456-application/ld+json"';
|
|
||||||
const eTagWrongTime = '"654321-text/turtle"';
|
|
||||||
|
|
||||||
it('returns true if the ETags are the same.', async(): Promise<void> => {
|
|
||||||
expect(sameResourceState(eTag, eTag)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns true if the ETags target the same timestamp.', async(): Promise<void> => {
|
|
||||||
expect(sameResourceState(eTag, eTagJson)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns false if the timestamp differs.', async(): Promise<void> => {
|
|
||||||
expect(sameResourceState(eTag, eTagWrongTime)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
x
Reference in New Issue
Block a user