From b608080d5f23a1326932b2b2c476db660e2dab2e Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Wed, 26 Jul 2023 10:33:23 +0200 Subject: [PATCH] feat: Store ETag in metadata --- src/http/ldp/GetOperationHandler.ts | 6 ++++++ src/http/ldp/HeadOperationHandler.ts | 6 ++++++ src/http/output/metadata/ModifiedMetadataWriter.ts | 7 +++---- src/util/Vocabularies.ts | 1 + test/unit/http/ldp/GetOperationHandler.test.ts | 8 +++++++- test/unit/http/ldp/HeadOperationHandler.test.ts | 8 +++++++- .../http/output/metadata/ModifiedMetadataWriter.test.ts | 7 +++---- 7 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/http/ldp/GetOperationHandler.ts b/src/http/ldp/GetOperationHandler.ts index 5f7de96bab..ded1fc3f59 100644 --- a/src/http/ldp/GetOperationHandler.ts +++ b/src/http/ldp/GetOperationHandler.ts @@ -1,6 +1,8 @@ +import { getETag } from '../../storage/Conditions'; import type { ResourceStore } from '../../storage/ResourceStore'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; import { assertReadConditions } from '../../util/ResourceUtil'; +import { HH } from '../../util/Vocabularies'; import { OkResponseDescription } from '../output/response/OkResponseDescription'; import type { ResponseDescription } from '../output/response/ResponseDescription'; import type { OperationHandlerInput } from './OperationHandler'; @@ -30,6 +32,10 @@ export class GetOperationHandler extends OperationHandler { // Check whether the cached representation is still valid or it is necessary to send a new representation assertReadConditions(body, operation.conditions); + // Add the ETag of the returned representation + const etag = getETag(body.metadata); + body.metadata.set(HH.terms.etag, etag); + return new OkResponseDescription(body.metadata, body.data); } } diff --git a/src/http/ldp/HeadOperationHandler.ts b/src/http/ldp/HeadOperationHandler.ts index 276c572294..cac12f5a01 100644 --- a/src/http/ldp/HeadOperationHandler.ts +++ b/src/http/ldp/HeadOperationHandler.ts @@ -1,6 +1,8 @@ +import { getETag } from '../../storage/Conditions'; import type { ResourceStore } from '../../storage/ResourceStore'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; import { assertReadConditions } from '../../util/ResourceUtil'; +import { HH } from '../../util/Vocabularies'; import { OkResponseDescription } from '../output/response/OkResponseDescription'; import type { ResponseDescription } from '../output/response/ResponseDescription'; import type { OperationHandlerInput } from './OperationHandler'; @@ -34,6 +36,10 @@ export class HeadOperationHandler extends OperationHandler { // Generally it doesn't make much sense to use condition headers with a HEAD request, but it should be supported. assertReadConditions(body, operation.conditions); + // Add the ETag of the returned representation + const etag = getETag(body.metadata); + body.metadata.set(HH.terms.etag, etag); + return new OkResponseDescription(body.metadata); } } diff --git a/src/http/output/metadata/ModifiedMetadataWriter.ts b/src/http/output/metadata/ModifiedMetadataWriter.ts index 93504c837c..401bf259ce 100644 --- a/src/http/output/metadata/ModifiedMetadataWriter.ts +++ b/src/http/output/metadata/ModifiedMetadataWriter.ts @@ -1,7 +1,6 @@ import type { HttpResponse } from '../../../server/HttpResponse'; -import { getETag } from '../../../storage/Conditions'; import { addHeader } from '../../../util/HeaderUtil'; -import { DC } from '../../../util/Vocabularies'; +import { DC, HH } from '../../../util/Vocabularies'; import type { RepresentationMetadata } from '../../representation/RepresentationMetadata'; import { MetadataWriter } from './MetadataWriter'; @@ -15,9 +14,9 @@ export class ModifiedMetadataWriter extends MetadataWriter { const date = new Date(modified.value); addHeader(input.response, 'Last-Modified', date.toUTCString()); } - const etag = getETag(input.metadata); + const etag = input.metadata.get(HH.terms.etag); if (etag) { - addHeader(input.response, 'ETag', etag); + addHeader(input.response, 'ETag', etag.value); } } } diff --git a/src/util/Vocabularies.ts b/src/util/Vocabularies.ts index 70e6e34db5..49237876e7 100644 --- a/src/util/Vocabularies.ts +++ b/src/util/Vocabularies.ts @@ -171,6 +171,7 @@ export const FOAF = createVocabulary('http://xmlns.com/foaf/0.1/', export const HH = createVocabulary('http://www.w3.org/2011/http-headers#', 'content-length', + 'etag', ); export const HTTP = createVocabulary('http://www.w3.org/2011/http#', diff --git a/test/unit/http/ldp/GetOperationHandler.test.ts b/test/unit/http/ldp/GetOperationHandler.test.ts index 20044c6e13..976c9a4657 100644 --- a/test/unit/http/ldp/GetOperationHandler.test.ts +++ b/test/unit/http/ldp/GetOperationHandler.test.ts @@ -5,9 +5,12 @@ import { BasicRepresentation } from '../../../../src/http/representation/BasicRe import type { Representation } from '../../../../src/http/representation/Representation'; import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata'; import { BasicConditions } from '../../../../src/storage/BasicConditions'; +import { getETag } from '../../../../src/storage/Conditions'; import type { ResourceStore } from '../../../../src/storage/ResourceStore'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; import { NotModifiedHttpError } from '../../../../src/util/errors/NotModifiedHttpError'; +import { updateModifiedDate } from '../../../../src/util/ResourceUtil'; +import { CONTENT_TYPE, HH } from '../../../../src/util/Vocabularies'; describe('A GetOperationHandler', (): void => { let operation: Operation; @@ -17,11 +20,13 @@ describe('A GetOperationHandler', (): void => { let store: ResourceStore; let handler: GetOperationHandler; let data: Readable; - const metadata = new RepresentationMetadata(); + let metadata: RepresentationMetadata; beforeEach(async(): Promise => { operation = { method: 'GET', target: { path: 'http://test.com/foo' }, preferences, conditions, body }; data = { destroy: jest.fn() } as any; + metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }); + updateModifiedDate(metadata); store = { getRepresentation: jest.fn(async(): Promise => ({ binary: false, data, metadata } as any)), @@ -40,6 +45,7 @@ describe('A GetOperationHandler', (): void => { const result = await handler.handle({ operation }); expect(result.statusCode).toBe(200); expect(result.metadata).toBe(metadata); + expect(metadata.get(HH.terms.etag)?.value).toBe(getETag(metadata)); expect(result.data).toBe(data); expect(store.getRepresentation).toHaveBeenCalledTimes(1); expect(store.getRepresentation).toHaveBeenLastCalledWith(operation.target, preferences, conditions); diff --git a/test/unit/http/ldp/HeadOperationHandler.test.ts b/test/unit/http/ldp/HeadOperationHandler.test.ts index 140d98fca9..39094ba900 100644 --- a/test/unit/http/ldp/HeadOperationHandler.test.ts +++ b/test/unit/http/ldp/HeadOperationHandler.test.ts @@ -5,9 +5,12 @@ import { BasicRepresentation } from '../../../../src/http/representation/BasicRe import type { Representation } from '../../../../src/http/representation/Representation'; import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata'; import { BasicConditions } from '../../../../src/storage/BasicConditions'; +import { getETag } from '../../../../src/storage/Conditions'; import type { ResourceStore } from '../../../../src/storage/ResourceStore'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; import { NotModifiedHttpError } from '../../../../src/util/errors/NotModifiedHttpError'; +import { updateModifiedDate } from '../../../../src/util/ResourceUtil'; +import { CONTENT_TYPE, HH } from '../../../../src/util/Vocabularies'; describe('A HeadOperationHandler', (): void => { let operation: Operation; @@ -17,11 +20,13 @@ describe('A HeadOperationHandler', (): void => { let store: ResourceStore; let handler: HeadOperationHandler; let data: Readable; - const metadata = new RepresentationMetadata(); + let metadata: RepresentationMetadata; beforeEach(async(): Promise => { operation = { method: 'HEAD', target: { path: 'http://test.com/foo' }, preferences, conditions, body }; data = { destroy: jest.fn() } as any; + metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }); + updateModifiedDate(metadata); store = { getRepresentation: jest.fn(async(): Promise => ({ binary: false, data, metadata } as any)), @@ -42,6 +47,7 @@ describe('A HeadOperationHandler', (): void => { const result = await handler.handle({ operation }); expect(result.statusCode).toBe(200); expect(result.metadata).toBe(metadata); + expect(metadata.get(HH.terms.etag)?.value).toBe(getETag(metadata)); expect(result.data).toBeUndefined(); expect(data.destroy).toHaveBeenCalledTimes(1); expect(store.getRepresentation).toHaveBeenCalledTimes(1); diff --git a/test/unit/http/output/metadata/ModifiedMetadataWriter.test.ts b/test/unit/http/output/metadata/ModifiedMetadataWriter.test.ts index d0ad42bbd1..11697ca945 100644 --- a/test/unit/http/output/metadata/ModifiedMetadataWriter.test.ts +++ b/test/unit/http/output/metadata/ModifiedMetadataWriter.test.ts @@ -2,22 +2,21 @@ import { createResponse } from 'node-mocks-http'; import { ModifiedMetadataWriter } from '../../../../../src/http/output/metadata/ModifiedMetadataWriter'; import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata'; import type { HttpResponse } from '../../../../../src/server/HttpResponse'; -import { getETag } from '../../../../../src/storage/Conditions'; import { updateModifiedDate } from '../../../../../src/util/ResourceUtil'; -import { CONTENT_TYPE, DC } from '../../../../../src/util/Vocabularies'; +import { DC, HH } from '../../../../../src/util/Vocabularies'; describe('A ModifiedMetadataWriter', (): void => { const writer = new ModifiedMetadataWriter(); it('adds the Last-Modified and ETag header if there is dc:modified metadata.', async(): Promise => { const response = createResponse() as HttpResponse; - const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' }); + const metadata = new RepresentationMetadata({ [HH.etag]: '123456-turtle' }); updateModifiedDate(metadata); const dateTime = metadata.get(DC.terms.modified)!.value; await expect(writer.handle({ response, metadata })).resolves.toBeUndefined(); expect(response.getHeaders()).toEqual({ 'last-modified': new Date(dateTime).toUTCString(), - etag: getETag(metadata), + etag: '123456-turtle', }); });