diff --git a/documentation/markdown/resource-store.md b/documentation/markdown/resource-store.md index fd42076f3..5fc76985f 100644 --- a/documentation/markdown/resource-store.md +++ b/documentation/markdown/resource-store.md @@ -30,7 +30,8 @@ and all the entries in `config/storage/backend`. This store emits the events that are necessary to emit notifications when resources change. There are 4 different events that can be emitted: -- `this.emit('changed', identifier, AS.Create | AS.Update | AS.Delete | undefined)`: is emitted for every resource that was changed/effected by a call to the store. +- `this.emit('changed', identifier, activity)`: is emitted for every resource that was changed/effected by a call to the store. + With activity being undefined or one of the available ActivityStream terms. - `this.emit(AS.Create, identifier)`: is emitted for every resource that was created by the call to the store. - `this.emit(AS.Update, identifier)`: is emitted for every resource that was updated by the call to the store. - `this.emit(AS.Delete, identifier)`: is emitted for every resource that was deleted by the call to the store. diff --git a/src/http/ldp/PostOperationHandler.ts b/src/http/ldp/PostOperationHandler.ts index aca268ca7..154ffafc0 100644 --- a/src/http/ldp/PostOperationHandler.ts +++ b/src/http/ldp/PostOperationHandler.ts @@ -1,11 +1,12 @@ import { getLoggerFor } from '../../logging/LogUtil'; import type { ResourceStore } from '../../storage/ResourceStore'; import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; +import { InternalServerError } from '../../util/errors/InternalServerError'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; +import { find } from '../../util/IterableUtil'; import { AS, SOLID_AS } from '../../util/Vocabularies'; import { CreatedResponseDescription } from '../output/response/CreatedResponseDescription'; import type { ResponseDescription } from '../output/response/ResponseDescription'; -import { createResourceIdentifier } from '../representation/ResourceIdentifier'; import type { OperationHandlerInput } from './OperationHandler'; import { OperationHandler } from './OperationHandler'; @@ -37,10 +38,12 @@ export class PostOperationHandler extends OperationHandler { this.logger.warn('POST requests require the Content-Type header to be set'); throw new BadRequestHttpError('POST requests require the Content-Type header to be set'); } - const result = await this.store.addResource(operation.target, operation.body, operation.conditions); - const createdIdentifier = Object.entries(result).find( - ([ , value ]): boolean => value.get(SOLID_AS.terms.Activity)?.value === AS.Create, - )![0]; - return new CreatedResponseDescription(createResourceIdentifier(createdIdentifier)); + const changes = await this.store.addResource(operation.target, operation.body, operation.conditions); + const createdIdentifier = find(changes.keys(), (identifier): boolean => + Boolean(changes.get(identifier)?.has(SOLID_AS.terms.Activity, AS.terms.Create))); + if (!createdIdentifier) { + throw new InternalServerError('Operation was successful but no created identifier was returned.'); + } + return new CreatedResponseDescription(createdIdentifier); } } diff --git a/src/http/representation/ResourceIdentifier.ts b/src/http/representation/ResourceIdentifier.ts index 000b7cf9a..adb11f4b8 100644 --- a/src/http/representation/ResourceIdentifier.ts +++ b/src/http/representation/ResourceIdentifier.ts @@ -14,10 +14,3 @@ export interface ResourceIdentifier { export function isResourceIdentifier(object: any): object is ResourceIdentifier { return object && (typeof object.path === 'string'); } - -/** - * Factory function creating a {@link ResourceIdentifier} for convenience. - */ -export function createResourceIdentifier(resourcePath: string): ResourceIdentifier { - return { path: resourcePath }; -} diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts index 45761c8b6..d4d4e68b0 100644 --- a/src/storage/DataAccessorBasedStore.ts +++ b/src/storage/DataAccessorBasedStore.ts @@ -19,6 +19,8 @@ import { NotFoundHttpError } from '../util/errors/NotFoundHttpError'; import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError'; import { PreconditionFailedHttpError } from '../util/errors/PreconditionFailedHttpError'; import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy'; +import { concat } from '../util/IterableUtil'; +import { IdentifierMap } from '../util/map/IdentifierMap'; import { ensureTrailingSlash, isContainerIdentifier, @@ -266,24 +268,24 @@ export class DataAccessorBasedStore implements ResourceStore { // Solid, §5.4: "When a contained resource is deleted, // the server MUST also delete the associated auxiliary resources" // https://solid.github.io/specification/protocol#deleting-resources - const changes: ChangeMap = {}; + const changes: ChangeMap = new IdentifierMap(); if (!this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) { const auxiliaries = this.auxiliaryStrategy.getAuxiliaryIdentifiers(identifier); for (const deletedId of await this.safelyDeleteAuxiliaryResources(auxiliaries)) { - changes[deletedId.path] = this.createActivityMetadata(deletedId, AS.Delete); + this.addActivityMetadata(changes, deletedId, AS.terms.Delete); } } if (!this.identifierStrategy.isRootContainer(identifier)) { const container = this.identifierStrategy.getParentContainer(identifier); - changes[container.path] = this.createActivityMetadata(container, AS.Update); + this.addActivityMetadata(changes, container, AS.terms.Update); // Update modified date of parent await this.updateContainerModifiedDate(container); } await this.accessor.deleteResource(identifier); - changes[identifier.path] = this.createActivityMetadata(identifier, AS.Delete); + this.addActivityMetadata(changes, identifier, AS.terms.Delete); return changes; } @@ -385,18 +387,17 @@ export class DataAccessorBasedStore implements ResourceStore { // Solid, §5.3: "Servers MUST create intermediate containers and include corresponding containment triples // in container representations derived from the URI path component of PUT and PATCH requests." // https://solid.github.io/specification/protocol#writing-resources - let changes: ChangeMap = {}; + let changes: ChangeMap = new IdentifierMap(); if (!this.identifierStrategy.isRootContainer(identifier) && !exists) { const parent = this.identifierStrategy.getParentContainer(identifier); - if (!createContainers) { - changes[parent.path] = this.createActivityMetadata(parent, AS.Update); - } else { - const createdContainers = await this.createRecursiveContainers(parent); - changes = { ...changes, ...createdContainers }; - if (Object.keys(createdContainers).length === 0) { - changes[parent.path] = this.createActivityMetadata(parent, AS.Update); - } + if (createContainers) { + changes = await this.createRecursiveContainers(parent); + } + + // No changes means the parent container exists and will be updated + if (changes.size === 0) { + this.addActivityMetadata(changes, parent, AS.terms.Update); } // Parent container is also modified @@ -410,7 +411,7 @@ export class DataAccessorBasedStore implements ResourceStore { this.accessor.writeContainer(identifier, representation.metadata) : this.accessor.writeDocument(identifier, representation.data, representation.metadata)); - changes[identifier.path] = this.createActivityMetadata(identifier, exists ? AS.Update : AS.Create); + this.addActivityMetadata(changes, identifier, exists ? AS.terms.Update : AS.terms.Create); return changes; } @@ -609,7 +610,7 @@ export class DataAccessorBasedStore implements ResourceStore { // Verify whether the container already exists try { const metadata = await this.getNormalizedMetadata(container); - // See #480 + // See https://github.com/CommunitySolidServer/CommunitySolidServer/issues/480 // Solid, §3.1: "If two URIs differ only in the trailing slash, and the server has associated a resource with // one of them, then the other URI MUST NOT correspond to another resource. Instead, the server MAY respond to // requests for the latter URI with a 301 redirect to the former." @@ -617,7 +618,7 @@ export class DataAccessorBasedStore implements ResourceStore { if (!isContainerPath(metadata.identifier.value)) { throw new ForbiddenHttpError(`Creating container ${container.path} conflicts with an existing resource.`); } - return {}; + return new IdentifierMap(); } catch (error: unknown) { if (!NotFoundHttpError.isInstance(error)) { throw error; @@ -625,15 +626,21 @@ export class DataAccessorBasedStore implements ResourceStore { } // Create the container, starting with its parent - const ancestors = this.identifierStrategy.isRootContainer(container) ? - {} : + const ancestors: ChangeMap = this.identifierStrategy.isRootContainer(container) ? + new IdentifierMap() : await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container)); const changes = await this.writeData(container, new BasicRepresentation([], container), true, false, false); - return { ...changes, ...ancestors }; + return new IdentifierMap(concat([ changes, ancestors ])); } - private createActivityMetadata(id: ResourceIdentifier, activity: string): RepresentationMetadata { - return new RepresentationMetadata(id, { [SOLID_AS.terms.Activity.value]: activity }); + /** + * Generates activity metadata for a resource and adds it to the {@link ChangeMap} + * @param map - ChangeMap to update. + * @param id - Identifier of the resource being changed. + * @param activity - Which activity is taking place. + */ + private addActivityMetadata(map: ChangeMap, id: ResourceIdentifier, activity: NamedNode): void { + map.set(id, new RepresentationMetadata(id, { [SOLID_AS.Activity]: activity })); } } diff --git a/src/storage/MonitoringStore.ts b/src/storage/MonitoringStore.ts index 0d1632de9..2069b63cf 100644 --- a/src/storage/MonitoringStore.ts +++ b/src/storage/MonitoringStore.ts @@ -7,6 +7,9 @@ import { AS, SOLID_AS } from '../util/Vocabularies'; import type { Conditions } from './Conditions'; import type { ResourceStore, ChangeMap } from './ResourceStore'; +// The ActivityStream terms for which we emit an event +const emittedActivities: Set = new Set([ AS.Create, AS.Delete, AS.Update ]); + /** * Store that notifies listeners of changes to its source * by emitting a `changed` event. @@ -50,11 +53,11 @@ export class MonitoringStore } private emitChanged(changes: ChangeMap): ChangeMap { - for (const [ key, value ] of Object.entries(changes)) { - const activity = value.get(SOLID_AS.terms.Activity)?.value; - this.emit('changed', { path: key }, activity); - if (activity && [ AS.Create, AS.Delete, AS.Update ].includes(activity)) { - this.emit(activity, { path: key }); + for (const [ identifier, metadata ] of changes) { + const activity = metadata.get(SOLID_AS.terms.Activity); + this.emit('changed', identifier, activity); + if (activity && emittedActivities.has(activity.value)) { + this.emit(activity.value, identifier); } } diff --git a/src/storage/ResourceStore.ts b/src/storage/ResourceStore.ts index 543d741f4..8acda17a9 100644 --- a/src/storage/ResourceStore.ts +++ b/src/storage/ResourceStore.ts @@ -3,15 +3,16 @@ import type { Representation } from '../http/representation/Representation'; import type { RepresentationMetadata } from '../http/representation/RepresentationMetadata'; import type { RepresentationPreferences } from '../http/representation/RepresentationPreferences'; import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier'; +import type { IdentifierMap } from '../util/map/IdentifierMap'; import type { Conditions } from './Conditions'; import type { ResourceSet } from './ResourceSet'; /** - * An object containing one property for each resource that was created, updated or deleted - * by this operation. Where the key of the property is the path of the resource (string) and the value is an - * instance of RepresentationMetadata containing extra information about the change of the resource. + * An {@link IdentifierMap} containing one entry for each resource that was created, updated or deleted + * by this operation. Where the value is a {@link RepresentationMetadata} + * containing extra information about the change of the resource. */ -export type ChangeMap = Record; +export type ChangeMap = IdentifierMap; /** * A ResourceStore represents a collection of resources. diff --git a/test/unit/http/ldp/PostOperationHandler.test.ts b/test/unit/http/ldp/PostOperationHandler.test.ts index db14e1a53..b9ed85546 100644 --- a/test/unit/http/ldp/PostOperationHandler.test.ts +++ b/test/unit/http/ldp/PostOperationHandler.test.ts @@ -4,33 +4,29 @@ 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 type { ResourceStore, ChangeMap } from '../../../../src/storage/ResourceStore'; +import type { ResourceStore } from '../../../../src/storage/ResourceStore'; import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError'; +import { InternalServerError } from '../../../../src/util/errors/InternalServerError'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; +import { IdentifierMap } from '../../../../src/util/map/IdentifierMap'; import { AS, SOLID_AS, SOLID_HTTP } from '../../../../src/util/Vocabularies'; describe('A PostOperationHandler', (): void => { let operation: Operation; let body: Representation; const conditions = new BasicConditions({}); - let store: ResourceStore; + let store: jest.Mocked; let handler: PostOperationHandler; beforeEach(async(): Promise => { body = new BasicRepresentation('', 'text/turtle'); operation = { method: 'POST', target: { path: 'http://test.com/foo' }, body, conditions, preferences: {}}; store = { - addResource: jest.fn(async(): Promise => ({ - 'https://example.com/parent/newPath': new RepresentationMetadata( - { path: 'https://example.com/parent/newPath' }, - { [SOLID_AS.terms.Activity.value]: AS.Create }, - ), - 'https://example.com/parent/': new RepresentationMetadata( - { path: 'https://example.come/parent/' }, - { [SOLID_AS.terms.Activity.value]: AS.Update }, - ), - })), - } as unknown as ResourceStore; + addResource: jest.fn().mockResolvedValue(new IdentifierMap([ + [{ path: 'https://example.com/parent/newPath' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Create }) ], + [{ path: 'https://example.com/parent/' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Update }) ], + ])), + } as any; handler = new PostOperationHandler(store); }); @@ -54,4 +50,11 @@ describe('A PostOperationHandler', (): void => { expect(store.addResource).toHaveBeenCalledTimes(1); expect(store.addResource).toHaveBeenLastCalledWith(operation.target, body, conditions); }); + + it('errors if the store returns no created identifier.', async(): Promise => { + store.addResource.mockResolvedValueOnce(new IdentifierMap([ + [{ path: 'https://example.com/parent/' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Update }) ], + ])); + await expect(handler.handle({ operation })).rejects.toThrow(InternalServerError); + }); }); diff --git a/test/unit/storage/DataAccessorBasedStore.test.ts b/test/unit/storage/DataAccessorBasedStore.test.ts index 454eb3d6c..e84522de2 100644 --- a/test/unit/storage/DataAccessorBasedStore.test.ts +++ b/test/unit/storage/DataAccessorBasedStore.test.ts @@ -266,21 +266,17 @@ describe('A DataAccessorBasedStore', (): void => { representation.metadata.removeAll(RDF.terms.type); const result = await store.addResource(resourceID, representation); - expect(Object.keys(result)).toEqual([ - root, - expect.stringMatching(new RegExp(`^${root}[^/]+$`, 'u')), - ]); + expect(result.size).toBe(2); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); - expect(result[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - - const generatedID = Object.keys(result).find((key): boolean => key !== root)!; + const generatedID = [ ...result.keys() ].find((id): boolean => id.path !== resourceID.path)!; expect(generatedID).toBeDefined(); - expect(generatedID).toMatch(new RegExp(`^${root}[^/]+$`, 'u')); + expect(generatedID.path).toMatch(new RegExp(`^${root}[^/]+$`, 'u')); - await expect(arrayifyStream(accessor.data[generatedID].data)).resolves.toEqual([ resourceData ]); - expect(accessor.data[generatedID]).toBeTruthy(); - expect(accessor.data[generatedID].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString()); - expect(result[generatedID].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + expect(accessor.data[generatedID.path]).toBeDefined(); + await expect(arrayifyStream(accessor.data[generatedID.path].data)).resolves.toEqual([ resourceData ]); + expect(accessor.data[generatedID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString()); + expect(result.get(generatedID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); }); it('can write containers.', async(): Promise => { @@ -290,24 +286,21 @@ describe('A DataAccessorBasedStore', (): void => { representation.data = guardedStreamFrom([ '<> a .' ]); const result = await store.addResource(resourceID, representation); - expect(Object.keys(result)).toEqual([ - root, - expect.stringMatching(new RegExp(`^${root}[^/]+?/$`, 'u')), - ]); + expect(result.size).toBe(2); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); - expect(result[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - - const generatedID = Object.keys(result).find((key): boolean => key !== root)!; + const generatedID = [ ...result.keys() ].find((id): boolean => id.path !== resourceID.path)!; expect(generatedID).toBeDefined(); - expect(generatedID).toMatch(new RegExp(`^${root}[^/]+?/$`, 'u')); - expect(accessor.data[generatedID]).toBeTruthy(); - expect(accessor.data[generatedID].metadata.contentType).toBeUndefined(); - expect(result[generatedID].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + expect(generatedID.path).toMatch(new RegExp(`^${root}[^/]+?/$`, 'u')); - const { data, metadata } = await store.getRepresentation({ path: generatedID }); + expect(accessor.data[generatedID.path]).toBeDefined(); + expect(accessor.data[generatedID.path].metadata.contentType).toBeUndefined(); + expect(result.get(generatedID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); + + const { data, metadata } = await store.getRepresentation(generatedID); const quads = await arrayifyStream(data); expect(metadata.get(DC.terms.modified)?.value).toBe(now.toISOString()); - expect(quads.some((entry): boolean => entry.subject.value === generatedID && + expect(quads.some((entry): boolean => entry.subject.value === generatedID.path && entry.object.value === 'http://test.com/coolContainer')).toBeTruthy(); }); @@ -316,13 +309,10 @@ describe('A DataAccessorBasedStore', (): void => { representation.metadata.removeAll(RDF.terms.type); representation.metadata.add(SOLID_HTTP.terms.slug, 'newName'); - const result = store.addResource(resourceID, representation); - await expect(result).resolves.toEqual({ - [root]: expect.any(RepresentationMetadata), - [`${root}newName`]: expect.any(RepresentationMetadata), - }); - expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - expect((await result)[`${root}newName`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + const result = await store.addResource(resourceID, representation); + expect(result.size).toBe(2); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get({ path: `${root}newName` })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); }); it('errors on a slug ending on / without Link rel:type Container header.', async(): Promise => { @@ -337,8 +327,7 @@ describe('A DataAccessorBasedStore', (): void => { .toThrow('Only slugs used to create containers can end with a `/`.'); }); - it('creates a URI when the incoming slug does not end with /, ' + - 'but has a Link rel:type Container header.', async(): Promise => { + it('adds a / at the end if the request metadata contains rdf:type ldp:Container.', async(): Promise => { const resourceID = { path: root }; representation.metadata.removeAll(RDF.terms.type); representation.metadata.add(RDF.terms.type, LDP.terms.Container); @@ -346,12 +335,9 @@ describe('A DataAccessorBasedStore', (): void => { representation.data = guardedStreamFrom([ `` ]); const result = await store.addResource(resourceID, representation); - expect(result).toEqual({ - [root]: expect.any(RepresentationMetadata), - [`${root}newContainer/`]: expect.any(RepresentationMetadata), - }); - expect(result[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - expect(result[`${root}newContainer/`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + expect(result.size).toBe(2); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get({ path: `${root}newContainer/` })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); }); it('generates a new URI if adding the slug would create an existing URI.', async(): Promise => { @@ -372,13 +358,10 @@ describe('A DataAccessorBasedStore', (): void => { representation.metadata.removeAll(RDF.terms.type); representation.metadata.add(SOLID_HTTP.terms.slug, '&%26'); - const result = store.addResource(resourceID, representation); - await expect(result).resolves.toEqual({ - [root]: expect.any(RepresentationMetadata), - [`${root}%26%26`]: expect.any(RepresentationMetadata), - }); - expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - expect((await result)[`${root}%26%26`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + const result = await store.addResource(resourceID, representation); + expect(result.size).toBe(2); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get({ path: `${root}%26%26` })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); }); it('errors if the slug contains a slash.', async(): Promise => { @@ -442,11 +425,9 @@ describe('A DataAccessorBasedStore', (): void => { representation.metadata.contentType = 'text/turtle'; representation.data = guardedStreamFrom([ `<${root}> a .` ]); - const result = store.setRepresentation(resourceID, representation); - await expect(result).resolves.toEqual({ - [root]: expect.any(RepresentationMetadata), - }); - expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + const result = await store.setRepresentation(resourceID, representation); + expect(result.size).toBe(1); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); expect(mock).toHaveBeenCalledTimes(1); expect(mock).toHaveBeenLastCalledWith(resourceID); @@ -475,13 +456,10 @@ describe('A DataAccessorBasedStore', (): void => { it('can write resources.', async(): Promise => { const resourceID = { path: `${root}resource` }; - const result = store.setRepresentation(resourceID, representation); - await expect(result).resolves.toEqual({ - [root]: expect.any(RepresentationMetadata), - [resourceID.path]: expect.any(RepresentationMetadata), - }); - expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + const result = await store.setRepresentation(resourceID, representation); + expect(result.size).toBe(2); + expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]); expect(accessor.data[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString()); expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString()); @@ -495,13 +473,10 @@ describe('A DataAccessorBasedStore', (): void => { representation.metadata.removeAll(RDF.terms.type); representation.metadata.contentType = 'text/turtle'; representation.data = guardedStreamFrom([ `<${root}resource/> a .` ]); - const result = store.setRepresentation(resourceID, representation); - await expect(result).resolves.toEqual({ - [root]: expect.any(RepresentationMetadata), - [resourceID.path]: expect.any(RepresentationMetadata), - }); - expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + const result = await store.setRepresentation(resourceID, representation); + expect(result.size).toBe(2); + expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); expect(accessor.data[resourceID.path]).toBeTruthy(); expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined(); expect(accessor.data[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString()); @@ -509,15 +484,12 @@ describe('A DataAccessorBasedStore', (): void => { expect(accessor.data[root].metadata.get(GENERATED_PREDICATE)).toBeUndefined(); }); - it('can overwrite resources which does not update parent metadata.', async(): Promise => { + it('can overwrite resources that do not update parent metadata.', async(): Promise => { const resourceID = { path: `${root}resource` }; - const result = store.setRepresentation(resourceID, representation); - await expect(result).resolves.toEqual({ - [root]: expect.any(RepresentationMetadata), - [resourceID.path]: expect.any(RepresentationMetadata), - }); - expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + const result = await store.setRepresentation(resourceID, representation); + expect(result.size).toBe(2); + expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]); expect(accessor.data[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString()); expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString()); @@ -525,11 +497,9 @@ describe('A DataAccessorBasedStore', (): void => { // Parent metadata does not get updated if the resource already exists representation = new BasicRepresentation('updatedText', 'text/plain'); mockDate.mockReturnValue(later); - const result2 = store.setRepresentation(resourceID, representation); - await expect(result2).resolves.toEqual({ - [resourceID.path]: expect.any(RepresentationMetadata), - }); - expect((await result2)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); + const result2 = await store.setRepresentation(resourceID, representation); + expect(result2.size).toBe(1); + expect(result2.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ 'updatedText' ]); expect(accessor.data[resourceID.path].metadata.get(DC.terms.modified)?.value).toBe(later.toISOString()); expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString()); @@ -540,13 +510,10 @@ describe('A DataAccessorBasedStore', (): void => { const resourceID = { path: `${root}resource` }; representation.metadata.add(namedNode('notGen'), 'value'); representation.metadata.add(namedNode('gen'), 'value', SOLID_META.terms.ResponseMetadata); - const result = store.setRepresentation(resourceID, representation); - await expect(result).resolves.toEqual({ - [root]: expect.any(RepresentationMetadata), - [resourceID.path]: expect.any(RepresentationMetadata), - }); - expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + const result = await store.setRepresentation(resourceID, representation); + expect(result.size).toBe(2); + expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]); expect(accessor.data[resourceID.path].metadata.get(namedNode('notGen'))?.value).toBe('value'); expect(accessor.data[resourceID.path].metadata.get(namedNode('gen'))).toBeUndefined(); @@ -556,13 +523,10 @@ describe('A DataAccessorBasedStore', (): void => { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete accessor.data[root]; const resourceID = { path: `${root}resource` }; - const result = store.setRepresentation(resourceID, representation); - await expect(result).resolves.toEqual({ - [root]: expect.any(RepresentationMetadata), - [resourceID.path]: expect.any(RepresentationMetadata), - }); - expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); - expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + const result = await store.setRepresentation(resourceID, representation); + expect(result.size).toBe(2); + expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]); }); @@ -575,13 +539,10 @@ describe('A DataAccessorBasedStore', (): void => { representation.data = guardedStreamFrom( [ quad(namedNode(`${root}resource/`), namedNode('a'), namedNode('coolContainer')) ], ); - const result = store.setRepresentation(resourceID, representation); - await expect(result).resolves.toEqual({ - [root]: expect.any(RepresentationMetadata), - [resourceID.path]: expect.any(RepresentationMetadata), - }); - expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + const result = await store.setRepresentation(resourceID, representation); + expect(result.size).toBe(2); + expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); expect(accessor.data[resourceID.path]).toBeTruthy(); expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined(); }); @@ -601,17 +562,12 @@ describe('A DataAccessorBasedStore', (): void => { it('creates recursive containers when needed.', async(): Promise => { const resourceID = { path: `${root}a/b/resource` }; - const result = store.setRepresentation(resourceID, representation); - await expect(result).resolves.toEqual({ - [`${root}`]: expect.any(RepresentationMetadata), - [`${root}a/`]: expect.any(RepresentationMetadata), - [`${root}a/b/`]: expect.any(RepresentationMetadata), - [`${root}a/b/resource`]: expect.any(RepresentationMetadata), - }); - expect((await result)[`${root}`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - expect((await result)[`${root}a/`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); - expect((await result)[`${root}a/b/`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); - expect((await result)[`${root}a/b/resource`].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + const result = await store.setRepresentation(resourceID, representation); + expect(result.size).toBe(4); + expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get({ path: `${root}a/` })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); + expect(result.get({ path: `${root}a/b/` })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); + expect(result.get({ path: `${root}a/b/resource` })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]); expect(accessor.data[`${root}a/`].metadata.getAll(RDF.terms.type).map((type): string => type.value)) .toContain(LDP.Container); @@ -636,11 +592,9 @@ describe('A DataAccessorBasedStore', (): void => { representation.metadata.removeAll(RDF.terms.type); representation.metadata.contentType = 'text/turtle'; representation.data = guardedStreamFrom([]); - const result = store.setRepresentation(resourceID, representation); - await expect(result).resolves.toEqual({ - [root]: expect.any(RepresentationMetadata), - }); - expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Create); + const result = await store.setRepresentation(resourceID, representation); + expect(result.size).toBe(1); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Create); expect(accessor.data[resourceID.path]).toBeTruthy(); expect(Object.keys(accessor.data)).toHaveLength(1); expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined(); @@ -725,10 +679,10 @@ describe('A DataAccessorBasedStore', (): void => { it('will delete resources.', async(): Promise => { const resourceID = { path: `${root}resource` }; accessor.data[resourceID.path] = representation; - await expect(store.deleteResource(resourceID)).resolves.toEqual({ - [root]: expect.any(RepresentationMetadata), - [resourceID.path]: expect.any(RepresentationMetadata), - }); + const result = await store.deleteResource(resourceID); + expect(result.size).toBe(2); + expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Delete); expect(accessor.data[resourceID.path]).toBeUndefined(); expect(accessor.data[root].metadata.get(DC.terms.modified)?.value).toBe(now.toISOString()); expect(accessor.data[root].metadata.get(GENERATED_PREDICATE)).toBeUndefined(); @@ -736,9 +690,9 @@ describe('A DataAccessorBasedStore', (): void => { it('will delete root non-storage containers.', async(): Promise => { accessor.data[root] = new BasicRepresentation(representation.data, containerMetadata); - await expect(store.deleteResource({ path: root })).resolves.toEqual( - { [root]: expect.any(RepresentationMetadata) }, - ); + const result = await store.deleteResource({ path: root }); + expect(result.size).toBe(1); + expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Delete); expect(accessor.data[root]).toBeUndefined(); }); @@ -749,15 +703,10 @@ describe('A DataAccessorBasedStore', (): void => { accessor.data[resourceID.path] = new BasicRepresentation(representation.data, storageMetadata); accessor.data[auxResourceID.path] = representation; auxiliaryStrategy.isRequiredInRoot = jest.fn().mockReturnValue(true); - const result = store.deleteResource(auxResourceID); - await expect(result).resolves.toEqual( - expect.objectContaining({ - [resourceID.path]: expect.any(RepresentationMetadata), - [auxResourceID.path]: expect.any(RepresentationMetadata), - }), - ); - expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - expect((await result)[auxResourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Delete); + const result = await store.deleteResource(auxResourceID); + expect(result.size).toBe(2); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get(auxResourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Delete); expect(accessor.data[auxResourceID.path]).toBeUndefined(); }); @@ -767,17 +716,11 @@ describe('A DataAccessorBasedStore', (): void => { accessor.data[resourceID.path] = representation; accessor.data[auxResourceID.path] = representation; - const result = store.deleteResource(resourceID); - await expect(result).resolves.toEqual( - expect.objectContaining({ - [root]: expect.any(RepresentationMetadata), - [resourceID.path]: expect.any(RepresentationMetadata), - [auxResourceID.path]: expect.any(RepresentationMetadata), - }), - ); - expect((await result)[root].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Update); - expect((await result)[resourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Delete); - expect((await result)[auxResourceID.path].get(SOLID_AS.terms.Activity)?.value).toBe(AS.Delete); + const result = await store.deleteResource(resourceID); + expect(result.size).toBe(3); + expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Delete); + expect(result.get(auxResourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Delete); expect(accessor.data[resourceID.path]).toBeUndefined(); expect(accessor.data[auxResourceID.path]).toBeUndefined(); }); @@ -796,12 +739,10 @@ describe('A DataAccessorBasedStore', (): void => { }); const { logger } = store as any; logger.error = jest.fn(); - const result = store.deleteResource(resourceID); - expect(Object.keys(await result)).toHaveLength(2); - await expect(result).resolves.toEqual({ - [root]: expect.any(RepresentationMetadata), - [resourceID.path]: expect.any(RepresentationMetadata), - }); + const result = await store.deleteResource(resourceID); + expect(result.size).toBe(2); + expect(result.get({ path: root })?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Update); + expect(result.get(resourceID)?.get(SOLID_AS.terms.Activity)).toEqual(AS.terms.Delete); expect(accessor.data[resourceID.path]).toBeUndefined(); expect(accessor.data[auxResourceID.path]).toBeDefined(); expect(logger.error).toHaveBeenCalledTimes(1); diff --git a/test/unit/storage/MonitoringStore.test.ts b/test/unit/storage/MonitoringStore.test.ts index 6de70fe5f..beb574745 100644 --- a/test/unit/storage/MonitoringStore.test.ts +++ b/test/unit/storage/MonitoringStore.test.ts @@ -2,7 +2,8 @@ import type { Patch } from '../../../src/http/representation/Patch'; import type { Representation } from '../../../src/http/representation/Representation'; import { RepresentationMetadata } from '../../../src/http/representation/RepresentationMetadata'; import { MonitoringStore } from '../../../src/storage/MonitoringStore'; -import type { ResourceStore } from '../../../src/storage/ResourceStore'; +import type { ChangeMap, ResourceStore } from '../../../src/storage/ResourceStore'; +import { IdentifierMap } from '../../../src/util/map/IdentifierMap'; import { AS, SOLID_AS } from '../../../src/util/Vocabularies'; describe('A MonitoringStore', (): void => { @@ -14,55 +15,31 @@ describe('A MonitoringStore', (): void => { let updatedCallback: () => void; let deletedCallback: () => void; - const addResourceReturnMock = { - 'http://example.org/foo/bar/new': new RepresentationMetadata( - { path: 'http://example.org/foo/bar/new' }, - { [SOLID_AS.terms.Activity.value]: AS.Create }, - ), - 'http://example.org/foo/bar/': new RepresentationMetadata( - { path: 'http://example.org/foo/bar/' }, - { [SOLID_AS.terms.Activity.value]: AS.Update }, - ), - }; - const setRepresentationReturnMock = { - 'http://example.org/foo/bar/new': new RepresentationMetadata( - { path: 'http://example.org/foo/bar/new' }, - { [SOLID_AS.terms.Activity.value]: AS.Update }, - ), - }; - const deleteResourceReturnMock = { - 'http://example.org/foo/bar/new': new RepresentationMetadata( - { path: 'http://example.org/foo/bar/new' }, - { [SOLID_AS.terms.Activity.value]: AS.Delete }, - ), - 'http://example.org/foo/bar/': new RepresentationMetadata( - { path: 'http://example.org/foo/bar/' }, - { [SOLID_AS.terms.Activity.value]: AS.Update }, - ), - }; - const modifyResourceReturnMock = { - 'http://example.org/foo/bar/old': new RepresentationMetadata( - { path: 'http://example.org/foo/bar/new' }, - { [SOLID_AS.terms.Activity.value]: AS.Delete }, - ), - 'http://example.org/foo/bar/new': new RepresentationMetadata( - { path: 'http://example.org/foo/bar/new' }, - { [SOLID_AS.terms.Activity.value]: AS.Create }, - ), - 'http://example.org/foo/bar/': new RepresentationMetadata( - { path: 'http://example.org/foo/bar/' }, - { [SOLID_AS.terms.Activity.value]: AS.Update }, - ), - }; + const addResourceReturnMock: ChangeMap = new IdentifierMap([ + [{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Create }) ], + [{ path: 'http://example.org/foo/bar/' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Update }) ], + ]); + const setRepresentationReturnMock: ChangeMap = new IdentifierMap([ + [{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Update }) ], + ]); + const deleteResourceReturnMock: ChangeMap = new IdentifierMap([ + [{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Delete }) ], + [{ path: 'http://example.org/foo/bar/' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Update }) ], + ]); + const modifyResourceReturnMock: ChangeMap = new IdentifierMap([ + [{ path: 'http://example.org/foo/bar/old' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Delete }) ], + [{ path: 'http://example.org/foo/bar/new' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Create }) ], + [{ path: 'http://example.org/foo/bar/' }, new RepresentationMetadata({ [SOLID_AS.Activity]: AS.terms.Update }) ], + ]); beforeEach(async(): Promise => { source = { - getRepresentation: jest.fn(async(): Promise => ({ success: true })), - addResource: jest.fn(async(): Promise => addResourceReturnMock), - setRepresentation: jest.fn(async(): Promise => setRepresentationReturnMock), - deleteResource: jest.fn(async(): Promise => deleteResourceReturnMock), - modifyResource: jest.fn(async(): Promise => modifyResourceReturnMock), - hasResource: jest.fn(async(): Promise => undefined), + getRepresentation: jest.fn().mockResolvedValue({ success: true }), + addResource: jest.fn().mockResolvedValue(addResourceReturnMock), + setRepresentation: jest.fn().mockResolvedValue(setRepresentationReturnMock), + deleteResource: jest.fn().mockResolvedValue(deleteResourceReturnMock), + modifyResource: jest.fn().mockResolvedValue(modifyResourceReturnMock), + hasResource: jest.fn().mockResolvedValue(true), }; store = new MonitoringStore(source); @@ -76,13 +53,6 @@ describe('A MonitoringStore', (): void => { store.on(AS.Delete, deletedCallback); }); - afterEach(async(): Promise => { - store.removeListener('changed', changedCallback); - store.removeListener(AS.Create, createdCallback); - store.removeListener(AS.Update, updatedCallback); - store.removeListener(AS.Delete, deletedCallback); - }); - it('calls getRepresentation directly from the source.', async(): Promise => { await expect(store.getRepresentation({ path: 'getPath' }, {})).resolves.toEqual({ success: true }); expect(source.getRepresentation).toHaveBeenCalledTimes(1); @@ -96,10 +66,8 @@ describe('A MonitoringStore', (): void => { }); it('calls addResource directly from the source.', async(): Promise => { - await expect(store.addResource({ path: 'http://example.org/foo/bar' }, {} as Representation)).resolves.toEqual({ - 'http://example.org/foo/bar/new': expect.any(RepresentationMetadata), - 'http://example.org/foo/bar/': expect.any(RepresentationMetadata), - }); + await expect(store.addResource({ path: 'http://example.org/foo/bar' }, {} as Representation)) + .resolves.toBe(addResourceReturnMock); expect(source.addResource).toHaveBeenCalledTimes(1); expect(source.addResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }, {}, undefined); }); @@ -109,8 +77,8 @@ describe('A MonitoringStore', (): void => { expect(changedCallback).toHaveBeenCalledTimes(0); await result; expect(changedCallback).toHaveBeenCalledTimes(2); - expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.Update); - expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.Create); + expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.terms.Update); + expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Create); expect(createdCallback).toHaveBeenCalledTimes(1); expect(createdCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }); expect(updatedCallback).toHaveBeenCalledTimes(1); @@ -130,7 +98,7 @@ describe('A MonitoringStore', (): void => { expect(changedCallback).toHaveBeenCalledTimes(0); await result; expect(changedCallback).toHaveBeenCalledTimes(1); - expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.Update); + expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Update); expect(createdCallback).toHaveBeenCalledTimes(0); expect(updatedCallback).toHaveBeenCalledTimes(1); expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }); @@ -149,8 +117,8 @@ describe('A MonitoringStore', (): void => { expect(changedCallback).toHaveBeenCalledTimes(0); await result; expect(changedCallback).toHaveBeenCalledTimes(2); - expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.Update); - expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.Delete); + expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.terms.Update); + expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Delete); expect(createdCallback).toHaveBeenCalledTimes(0); expect(updatedCallback).toHaveBeenCalledTimes(1); expect(updatedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }); @@ -170,9 +138,9 @@ describe('A MonitoringStore', (): void => { expect(changedCallback).toHaveBeenCalledTimes(0); await result; expect(changedCallback).toHaveBeenCalledTimes(3); - expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.Create); - expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.Update); - expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/old' }, AS.Delete); + expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }, AS.terms.Create); + expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/' }, AS.terms.Update); + expect(changedCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/old' }, AS.terms.Delete); expect(createdCallback).toHaveBeenCalledTimes(1); expect(createdCallback).toHaveBeenCalledWith({ path: 'http://example.org/foo/bar/new' }); expect(updatedCallback).toHaveBeenCalledTimes(1); @@ -182,18 +150,15 @@ describe('A MonitoringStore', (): void => { }); it('calls hasResource directly from the source.', async(): Promise => { - await expect(store.hasResource({ path: 'http://example.org/foo/bar' })).resolves.toBeUndefined(); + await expect(store.hasResource({ path: 'http://example.org/foo/bar' })).resolves.toBe(true); expect(source.hasResource).toHaveBeenCalledTimes(1); expect(source.hasResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' }); }); it('should not emit an extra event when the Activity is not a valid AS value.', async(): Promise => { - source.addResource = jest.fn(async(): Promise => ({ - 'http://example.com/path': new RepresentationMetadata( - { path: 'http://example.com/path' }, - { [SOLID_AS.terms.Activity.value]: 'SomethingRandom' }, - ), - })); + source.addResource = jest.fn().mockResolvedValue(new IdentifierMap([ + [{ path: 'http://example.org/path' }, new RepresentationMetadata({ [SOLID_AS.Activity]: 'SomethingRandom' }) ], + ])); await store.addResource({ path: 'http://example.org/foo/bar' }, {} as Patch);