refactor: Rename associated resource to subject resource

This commit is contained in:
Joachim Van Herwegen 2021-10-07 16:38:25 +02:00
parent 13c49045d4
commit 7c7fee5f5c
20 changed files with 93 additions and 93 deletions

View File

@ -8,8 +8,8 @@ import { PermissionReader } from './PermissionReader';
/**
* A PermissionReader for auxiliary resources such as acl or shape resources.
* The access permissions of an auxiliary resource depend on those of the resource it is associated with.
* This authorizer calls the source authorizer with the identifier of the associated resource.
* By default, the access permissions of an auxiliary resource depend on those of its subject resource.
* This authorizer calls the source authorizer with the identifier of the subject resource.
*/
export class AuxiliaryReader extends PermissionReader {
protected readonly logger = getLoggerFor(this);
@ -51,7 +51,7 @@ export class AuxiliaryReader extends PermissionReader {
return {
...auxiliaryAuth,
identifier: this.auxiliaryStrategy.getAssociatedIdentifier(auxiliaryAuth.identifier),
identifier: this.auxiliaryStrategy.getSubjectIdentifier(auxiliaryAuth.identifier),
};
}
}

View File

@ -62,7 +62,7 @@ export class WebAclReader extends PermissionReader {
this.logger.debug(`Retrieving permissions of ${credentials.agent?.webId} for ${identifier.path}`);
const isAcl = this.aclStrategy.isAuxiliaryIdentifier(identifier);
const mainIdentifier = isAcl ? this.aclStrategy.getAssociatedIdentifier(identifier) : identifier;
const mainIdentifier = isAcl ? this.aclStrategy.getSubjectIdentifier(identifier) : identifier;
// Determine the full authorization for the agent granted by the applicable ACL
const acl = await this.getAclRecursive(mainIdentifier);

View File

@ -42,7 +42,7 @@ export interface AuxiliaryIdentifierStrategy {
* This does not guarantee that this resource exists.
* @param identifier - Identifier of the auxiliary resource.
*
* @returns The ResourceIdentifier of the corresponding resource.
* @returns The ResourceIdentifier of the subject resource.
*/
getAssociatedIdentifier: (identifier: ResourceIdentifier) => ResourceIdentifier;
getSubjectIdentifier: (identifier: ResourceIdentifier) => ResourceIdentifier;
}

View File

@ -10,7 +10,7 @@ import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy'
*/
export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
/**
* Whether this auxiliary resources uses its own authorization instead of the associated resource authorization.
* Whether this auxiliary resources uses its own authorization instead of the subject resource authorization.
* @param identifier - Identifier of the auxiliary resource.
*/
usesOwnAuthorization: (identifier: ResourceIdentifier) => boolean;
@ -25,7 +25,7 @@ export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
/**
* Adds metadata related to this auxiliary resource,
* in case this is required for this type of auxiliary resource.
* The metadata that is added depends on the given identifier being an auxiliary or associated resource:
* The metadata that is added depends on the given identifier being an auxiliary or subject resource:
* the metadata will be used to link to the other one, and potentially add extra typing info.
*
* Used for:
@ -33,7 +33,7 @@ export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
* that resource MUST include an HTTP Link header pointing to the location of each auxiliary resource."
* https://solid.github.io/specification/protocol#auxiliary-resources-server
*
* The above is an example of how that metadata would only be added in case the input is the associated identifier.
* The above is an example of how that metadata would only be added in case the input is the subject identifier.
*
* @param metadata - Metadata to update.
*/

View File

@ -38,8 +38,8 @@ export class ComposedAuxiliaryStrategy implements AuxiliaryStrategy {
return this.identifierStrategy.isAuxiliaryIdentifier(identifier);
}
public getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return this.identifierStrategy.getAssociatedIdentifier(identifier);
public getSubjectIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return this.identifierStrategy.getSubjectIdentifier(identifier);
}
public usesOwnAuthorization(): boolean {

View File

@ -5,8 +5,8 @@ import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy'
import { MetadataGenerator } from './MetadataGenerator';
/**
* Adds a link to the auxiliary resource when called on the associated resource.
* Specifically: <associatedId> <link> <auxiliaryId> will be added.
* Adds a link to the auxiliary resource when called on the subject resource.
* Specifically: <subjectId> <link> <auxiliaryId> will be added.
*
* In case the input is metadata of an auxiliary resource no metadata will be added
*/

View File

@ -28,9 +28,9 @@ export class RoutingAuxiliaryIdentifierStrategy implements AuxiliaryIdentifierSt
return this.sources.some((source): boolean => source.isAuxiliaryIdentifier(identifier));
}
public getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
public getSubjectIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
const source = this.getMatchingSource(identifier);
return source.getAssociatedIdentifier(identifier);
return source.getSubjectIdentifier(identifier);
}
protected getMatchingSource(identifier: ResourceIdentifier): AuxiliaryIdentifierStrategy {

View File

@ -8,7 +8,7 @@ import { RoutingAuxiliaryIdentifierStrategy } from './RoutingAuxiliaryIdentifier
* An {@link AuxiliaryStrategy} that combines multiple AuxiliaryStrategies into one.
* Uses `isAuxiliaryIdentifier` to know which strategy to call for which call.
*
* `addMetadata` will either call all strategies if the input is the associated identifier,
* `addMetadata` will either call all strategies if the input is the subject identifier,
* or only the matching strategy if the input is an auxiliary identifier.
*/
export class RoutingAuxiliaryStrategy extends RoutingAuxiliaryIdentifierStrategy implements AuxiliaryStrategy {

View File

@ -28,7 +28,7 @@ export class SuffixAuxiliaryIdentifierStrategy implements AuxiliaryIdentifierStr
return identifier.path.endsWith(this.suffix);
}
public getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
public getSubjectIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
if (!this.isAuxiliaryIdentifier(identifier)) {
throw new InternalServerError(`${identifier.path} does not end on ${this.suffix} so no conversion is possible.`);
}

View File

@ -244,8 +244,8 @@ export class DataAccessorBasedStore implements ResourceStore {
}
if (this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier) &&
this.auxiliaryStrategy.isRequiredInRoot(identifier)) {
const associatedIdentifier = this.auxiliaryStrategy.getAssociatedIdentifier(identifier);
const parentMetadata = await this.accessor.getMetadata(associatedIdentifier);
const subjectIdentifier = this.auxiliaryStrategy.getSubjectIdentifier(identifier);
const parentMetadata = await this.accessor.getMetadata(subjectIdentifier);
if (this.isRootStorage(parentMetadata)) {
throw new MethodNotAllowedHttpError(`Cannot delete ${identifier.path} from a root storage container.`);
}

View File

@ -17,7 +17,7 @@ import type { ResourceStore } from './ResourceStore';
* and releases it afterwards.
* In case the request returns a Representation the lock will only be released when the data stream is finished.
*
* For auxiliary resources the lock will be applied to the associated resource.
* For auxiliary resources the lock will be applied to the subject resource.
* The actual operation is still executed on the auxiliary resource.
*/
export class LockingResourceStore implements AtomicResourceStore {
@ -71,11 +71,11 @@ export class LockingResourceStore implements AtomicResourceStore {
/**
* Acquires the correct identifier to lock this resource.
* For auxiliary resources this means the associated identifier.
* For auxiliary resources this means the subject identifier.
*/
protected getLockIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier) ?
this.auxiliaryStrategy.getAssociatedIdentifier(identifier) :
this.auxiliaryStrategy.getSubjectIdentifier(identifier) :
identifier;
}

View File

@ -9,7 +9,7 @@ import { NotImplementedHttpError } from '../../../src/util/errors/NotImplemented
describe('An AuxiliaryReader', (): void => {
const suffix = '.dummy';
const credentials = {};
const associatedIdentifier = { path: 'http://test.com/foo' };
const subjectIdentifier = { path: 'http://test.com/foo' };
const auxiliaryIdentifier = { path: 'http://test.com/foo.dummy' };
const permissionSet: PermissionSet = { [CredentialGroup.agent]: { read: true }};
let source: jest.Mocked<PermissionReader>;
@ -25,20 +25,20 @@ describe('An AuxiliaryReader', (): void => {
strategy = {
isAuxiliaryIdentifier: jest.fn((identifier: ResourceIdentifier): boolean => identifier.path.endsWith(suffix)),
getAssociatedIdentifier: jest.fn((identifier: ResourceIdentifier): ResourceIdentifier =>
getSubjectIdentifier: jest.fn((identifier: ResourceIdentifier): ResourceIdentifier =>
({ path: identifier.path.slice(0, -suffix.length) })),
usesOwnAuthorization: jest.fn().mockReturnValue(false),
} as any;
reader = new AuxiliaryReader(source, strategy);
});
it('can handle auxiliary resources if the source supports the associated resource.', async(): Promise<void> => {
it('can handle auxiliary resources if the source supports the subject resource.', async(): Promise<void> => {
await expect(reader.canHandle({ identifier: auxiliaryIdentifier, credentials }))
.resolves.toBeUndefined();
expect(source.canHandle).toHaveBeenLastCalledWith(
{ identifier: associatedIdentifier, credentials },
{ identifier: subjectIdentifier, credentials },
);
await expect(reader.canHandle({ identifier: associatedIdentifier, credentials }))
await expect(reader.canHandle({ identifier: subjectIdentifier, credentials }))
.rejects.toThrow(NotImplementedHttpError);
strategy.usesOwnAuthorization.mockReturnValueOnce(true);
@ -54,10 +54,10 @@ describe('An AuxiliaryReader', (): void => {
await expect(reader.handle({ identifier: auxiliaryIdentifier, credentials }))
.resolves.toBe(permissionSet);
expect(source.handle).toHaveBeenLastCalledWith(
{ identifier: associatedIdentifier, credentials },
{ identifier: subjectIdentifier, credentials },
);
// Safety checks are not present when calling `handle`
await expect(reader.handle({ identifier: associatedIdentifier, credentials }))
await expect(reader.handle({ identifier: subjectIdentifier, credentials }))
.rejects.toThrow(NotImplementedHttpError);
});
@ -65,10 +65,10 @@ describe('An AuxiliaryReader', (): void => {
await expect(reader.handleSafe({ identifier: auxiliaryIdentifier, credentials }))
.resolves.toBe(permissionSet);
expect(source.handleSafe).toHaveBeenLastCalledWith(
{ identifier: associatedIdentifier, credentials },
{ identifier: subjectIdentifier, credentials },
);
await expect(reader.handleSafe({ identifier: associatedIdentifier, credentials }))
await expect(reader.handleSafe({ identifier: subjectIdentifier, credentials }))
.rejects.toThrow(NotImplementedHttpError);
strategy.usesOwnAuthorization.mockReturnValueOnce(true);

View File

@ -25,7 +25,7 @@ describe('A WebAclReader', (): void => {
const aclStrategy: AuxiliaryIdentifierStrategy = {
getAuxiliaryIdentifier: (id: ResourceIdentifier): ResourceIdentifier => ({ path: `${id.path}.acl` }),
isAuxiliaryIdentifier: (id: ResourceIdentifier): boolean => id.path.endsWith('.acl'),
getAssociatedIdentifier: (id: ResourceIdentifier): ResourceIdentifier => ({ path: id.path.slice(0, -4) }),
getSubjectIdentifier: (id: ResourceIdentifier): ResourceIdentifier => ({ path: id.path.slice(0, -4) }),
} as any;
let store: jest.Mocked<ResourceStore>;
const identifierStrategy = new SingleRootIdentifierStrategy('http://test.com/');

View File

@ -15,7 +15,7 @@ describe('A ComposedAuxiliaryStrategy', (): void => {
identifierStrategy = {
getAuxiliaryIdentifier: jest.fn(),
getAuxiliaryIdentifiers: jest.fn(),
getAssociatedIdentifier: jest.fn(),
getSubjectIdentifier: jest.fn(),
isAuxiliaryIdentifier: jest.fn(),
};
metadataGenerator = {
@ -36,9 +36,9 @@ describe('A ComposedAuxiliaryStrategy', (): void => {
expect(identifierStrategy.getAuxiliaryIdentifiers).toHaveBeenCalledTimes(1);
expect(identifierStrategy.getAuxiliaryIdentifiers).toHaveBeenLastCalledWith(identifier);
strategy.getAssociatedIdentifier(identifier);
expect(identifierStrategy.getAssociatedIdentifier).toHaveBeenCalledTimes(1);
expect(identifierStrategy.getAssociatedIdentifier).toHaveBeenLastCalledWith(identifier);
strategy.getSubjectIdentifier(identifier);
expect(identifierStrategy.getSubjectIdentifier).toHaveBeenCalledTimes(1);
expect(identifierStrategy.getSubjectIdentifier).toHaveBeenLastCalledWith(identifier);
strategy.isAuxiliaryIdentifier(identifier);
expect(identifierStrategy.isAuxiliaryIdentifier).toHaveBeenCalledTimes(1);

View File

@ -6,7 +6,7 @@ import { SOLID_META } from '../../../../src/util/Vocabularies';
describe('A LinkMetadataGenerator', (): void => {
const link = 'link';
const associatedId: ResourceIdentifier = { path: 'http://test.com/foo' };
const subjectId: ResourceIdentifier = { path: 'http://test.com/foo' };
const auxiliaryId: ResourceIdentifier = { path: 'http://test.com/foo.dummy' };
let generator: LinkMetadataGenerator;
@ -15,7 +15,7 @@ describe('A LinkMetadataGenerator', (): void => {
getAuxiliaryIdentifier: (identifier: ResourceIdentifier): ResourceIdentifier =>
({ path: `${identifier.path}.dummy` }),
isAuxiliaryIdentifier: (identifier: ResourceIdentifier): boolean => identifier.path.endsWith('.dummy'),
getAssociatedIdentifier: (identifier: ResourceIdentifier): ResourceIdentifier =>
getSubjectIdentifier: (identifier: ResourceIdentifier): ResourceIdentifier =>
({ path: identifier.path.slice(0, -'.dummy'.length) }),
} as AuxiliaryIdentifierStrategy;
generator = new LinkMetadataGenerator(link, strategy);
@ -25,14 +25,14 @@ describe('A LinkMetadataGenerator', (): void => {
await expect(generator.canHandle(null as any)).resolves.toBeUndefined();
});
it('stores no metadata if the input is an associated resource.', async(): Promise<void> => {
it('stores no metadata if the input is a subject resource.', async(): Promise<void> => {
const metadata = new RepresentationMetadata(auxiliaryId);
await expect(generator.handle(metadata)).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(0);
});
it('uses the stored link to add metadata for associated resources.', async(): Promise<void> => {
const metadata = new RepresentationMetadata(associatedId);
it('uses the stored link to add metadata for subject resources.', async(): Promise<void> => {
const metadata = new RepresentationMetadata(subjectId);
await expect(generator.handle(metadata)).resolves.toBeUndefined();
expect(metadata.quads()).toHaveLength(1);
expect(metadata.get(link)?.value).toBe(auxiliaryId.path);

View File

@ -23,7 +23,7 @@ class SimpleSuffixStrategy implements AuxiliaryIdentifierStrategy {
return identifier.path.endsWith(this.suffix);
}
public getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
public getSubjectIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return { path: identifier.path.slice(0, -this.suffix.length) };
}
}
@ -58,9 +58,9 @@ describe('A RoutingAuxiliaryIdentifierStrategy', (): void => {
expect(strategy.isAuxiliaryIdentifier(dummy3Id)).toBe(false);
});
it('#getAssociatedIdentifier returns the base id if a match is found.', async(): Promise<void> => {
expect(strategy.getAssociatedIdentifier(dummy1Id)).toEqual(baseId);
expect(strategy.getAssociatedIdentifier(dummy2Id)).toEqual(baseId);
expect((): any => strategy.getAssociatedIdentifier(dummy3Id)).toThrow(NotImplementedHttpError);
it('#getSubjectIdentifier returns the base id if a match is found.', async(): Promise<void> => {
expect(strategy.getSubjectIdentifier(dummy1Id)).toEqual(baseId);
expect(strategy.getSubjectIdentifier(dummy2Id)).toEqual(baseId);
expect((): any => strategy.getSubjectIdentifier(dummy3Id)).toThrow(NotImplementedHttpError);
});
});

View File

@ -27,7 +27,7 @@ class SimpleSuffixStrategy implements AuxiliaryStrategy {
return identifier.path.endsWith(this.suffix);
}
public getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
public getSubjectIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return { path: identifier.path.slice(0, -this.suffix.length) };
}

View File

@ -7,7 +7,7 @@ const suffix = '.dummy';
describe('A SuffixAuxiliaryManager', (): void => {
let strategy: SuffixAuxiliaryIdentifierStrategy;
const associatedId: ResourceIdentifier = { path: 'http://test.com/foo' };
const subjectId: ResourceIdentifier = { path: 'http://test.com/foo' };
const auxiliaryId: ResourceIdentifier = { path: 'http://test.com/foo.dummy' };
beforeEach(async(): Promise<void> => {
@ -19,23 +19,23 @@ describe('A SuffixAuxiliaryManager', (): void => {
});
it('creates new identifiers by appending the suffix.', async(): Promise<void> => {
expect(strategy.getAuxiliaryIdentifier(associatedId)).toEqual(auxiliaryId);
expect(strategy.getAuxiliaryIdentifier(subjectId)).toEqual(auxiliaryId);
});
it('returns the same single identifier when requesting all of them.', async(): Promise<void> => {
expect(strategy.getAuxiliaryIdentifiers(associatedId)).toEqual([ auxiliaryId ]);
expect(strategy.getAuxiliaryIdentifiers(subjectId)).toEqual([ auxiliaryId ]);
});
it('checks the suffix to determine if an identifier is auxiliary.', async(): Promise<void> => {
expect(strategy.isAuxiliaryIdentifier(associatedId)).toBe(false);
expect(strategy.isAuxiliaryIdentifier(subjectId)).toBe(false);
expect(strategy.isAuxiliaryIdentifier(auxiliaryId)).toBe(true);
});
it('errors when trying to get the associated id from a non-auxiliary identifier.', async(): Promise<void> => {
expect((): any => strategy.getAssociatedIdentifier(associatedId)).toThrow(InternalServerError);
it('errors when trying to get the subject id from a non-auxiliary identifier.', async(): Promise<void> => {
expect((): any => strategy.getSubjectIdentifier(subjectId)).toThrow(InternalServerError);
});
it('removes the suffix to create the associated identifier.', async(): Promise<void> => {
expect(strategy.getAssociatedIdentifier(auxiliaryId)).toEqual(associatedId);
it('removes the suffix to create the subject identifier.', async(): Promise<void> => {
expect(strategy.getSubjectIdentifier(auxiliaryId)).toEqual(subjectId);
});
});

View File

@ -107,7 +107,7 @@ class SimpleSuffixStrategy implements AuxiliaryStrategy {
return identifier.path.endsWith(this.suffix);
}
public getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
public getSubjectIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return { path: identifier.path.slice(0, -this.suffix.length) };
}

View File

@ -14,7 +14,7 @@ function emptyFn(): void {
describe('A LockingResourceStore', (): void => {
const auxiliaryId = { path: 'http://test.com/foo.dummy' };
const associatedId = { path: 'http://test.com/foo' };
const subjectId = { path: 'http://test.com/foo' };
const data = { data: 'data!' } as any;
let store: LockingResourceStore;
let locker: ExpiringReadWriteLocker;
@ -72,7 +72,7 @@ describe('A LockingResourceStore', (): void => {
auxiliaryStrategy = {
isAuxiliaryIdentifier: jest.fn((id: ResourceIdentifier): any => id.path.endsWith('.dummy')),
getAssociatedIdentifier: jest.fn((id: ResourceIdentifier): any => ({ path: id.path.slice(0, -6) })),
getSubjectIdentifier: jest.fn((id: ResourceIdentifier): any => ({ path: id.path.slice(0, -6) })),
} as any;
store = new LockingResourceStore(source, locker, auxiliaryStrategy);
@ -85,68 +85,68 @@ describe('A LockingResourceStore', (): void => {
}
it('acquires a lock on the container when adding a representation.', async(): Promise<void> => {
await store.addResource(associatedId, data);
await store.addResource(subjectId, data);
expect(locker.withWriteLock).toHaveBeenCalledTimes(1);
expect((locker.withWriteLock as jest.Mock).mock.calls[0][0]).toEqual(associatedId);
expect((locker.withWriteLock as jest.Mock).mock.calls[0][0]).toEqual(subjectId);
expect(source.addResource).toHaveBeenCalledTimes(1);
expect(source.addResource).toHaveBeenLastCalledWith(associatedId, data, undefined);
expect(source.addResource).toHaveBeenLastCalledWith(subjectId, data, undefined);
expect(order).toEqual([ 'lock write', 'addResource', 'unlock write' ]);
order = [];
await expect(store.addResource(auxiliaryId, data)).resolves.toBeUndefined();
expect(locker.withWriteLock).toHaveBeenCalledTimes(2);
expect((locker.withWriteLock as jest.Mock).mock.calls[1][0]).toEqual(associatedId);
expect((locker.withWriteLock as jest.Mock).mock.calls[1][0]).toEqual(subjectId);
expect(source.addResource).toHaveBeenCalledTimes(2);
expect(source.addResource).toHaveBeenLastCalledWith(auxiliaryId, data, undefined);
expect(order).toEqual([ 'lock write', 'addResource', 'unlock write' ]);
});
it('acquires a lock on the resource when setting its representation.', async(): Promise<void> => {
await store.setRepresentation(associatedId, data);
await store.setRepresentation(subjectId, data);
expect(locker.withWriteLock).toHaveBeenCalledTimes(1);
expect((locker.withWriteLock as jest.Mock).mock.calls[0][0]).toEqual(associatedId);
expect((locker.withWriteLock as jest.Mock).mock.calls[0][0]).toEqual(subjectId);
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
expect(source.setRepresentation).toHaveBeenLastCalledWith(associatedId, data, undefined);
expect(source.setRepresentation).toHaveBeenLastCalledWith(subjectId, data, undefined);
expect(order).toEqual([ 'lock write', 'setRepresentation', 'unlock write' ]);
order = [];
await expect(store.setRepresentation(auxiliaryId, data)).resolves.toBeUndefined();
expect(locker.withWriteLock).toHaveBeenCalledTimes(2);
expect((locker.withWriteLock as jest.Mock).mock.calls[1][0]).toEqual(associatedId);
expect((locker.withWriteLock as jest.Mock).mock.calls[1][0]).toEqual(subjectId);
expect(source.setRepresentation).toHaveBeenCalledTimes(2);
expect(source.setRepresentation).toHaveBeenLastCalledWith(auxiliaryId, data, undefined);
expect(order).toEqual([ 'lock write', 'setRepresentation', 'unlock write' ]);
});
it('acquires a lock on the resource when deleting it.', async(): Promise<void> => {
await store.deleteResource(associatedId);
await store.deleteResource(subjectId);
expect(locker.withWriteLock).toHaveBeenCalledTimes(1);
expect((locker.withWriteLock as jest.Mock).mock.calls[0][0]).toEqual(associatedId);
expect((locker.withWriteLock as jest.Mock).mock.calls[0][0]).toEqual(subjectId);
expect(source.deleteResource).toHaveBeenCalledTimes(1);
expect(source.deleteResource).toHaveBeenLastCalledWith(associatedId, undefined);
expect(source.deleteResource).toHaveBeenLastCalledWith(subjectId, undefined);
expect(order).toEqual([ 'lock write', 'deleteResource', 'unlock write' ]);
order = [];
await expect(store.deleteResource(auxiliaryId)).resolves.toBeUndefined();
expect(locker.withWriteLock).toHaveBeenCalledTimes(2);
expect((locker.withWriteLock as jest.Mock).mock.calls[1][0]).toEqual(associatedId);
expect((locker.withWriteLock as jest.Mock).mock.calls[1][0]).toEqual(subjectId);
expect(source.deleteResource).toHaveBeenCalledTimes(2);
expect(source.deleteResource).toHaveBeenLastCalledWith(auxiliaryId, undefined);
expect(order).toEqual([ 'lock write', 'deleteResource', 'unlock write' ]);
});
it('acquires a lock on the resource when modifying its representation.', async(): Promise<void> => {
await store.modifyResource(associatedId, data as Patch);
await store.modifyResource(subjectId, data as Patch);
expect(locker.withWriteLock).toHaveBeenCalledTimes(1);
expect((locker.withWriteLock as jest.Mock).mock.calls[0][0]).toEqual(associatedId);
expect((locker.withWriteLock as jest.Mock).mock.calls[0][0]).toEqual(subjectId);
expect(source.modifyResource).toHaveBeenCalledTimes(1);
expect(source.modifyResource).toHaveBeenLastCalledWith(associatedId, data, undefined);
expect(source.modifyResource).toHaveBeenLastCalledWith(subjectId, data, undefined);
expect(order).toEqual([ 'lock write', 'modifyResource', 'unlock write' ]);
order = [];
await expect(store.modifyResource(auxiliaryId, data as Patch)).resolves.toBeUndefined();
expect(locker.withWriteLock).toHaveBeenCalledTimes(2);
expect((locker.withWriteLock as jest.Mock).mock.calls[1][0]).toEqual(associatedId);
expect((locker.withWriteLock as jest.Mock).mock.calls[1][0]).toEqual(subjectId);
expect(source.modifyResource).toHaveBeenCalledTimes(2);
expect(source.modifyResource).toHaveBeenLastCalledWith(auxiliaryId, data, undefined);
expect(order).toEqual([ 'lock write', 'modifyResource', 'unlock write' ]);
@ -157,15 +157,15 @@ describe('A LockingResourceStore', (): void => {
order.push('bad get');
throw new Error('dummy');
};
await expect(store.getRepresentation(associatedId, {})).rejects.toThrow('dummy');
await expect(store.getRepresentation(subjectId, {})).rejects.toThrow('dummy');
expect(locker.withReadLock).toHaveBeenCalledTimes(1);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(associatedId);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(subjectId);
expect(order).toEqual([ 'lock read', 'bad get', 'unlock read' ]);
});
it('releases the lock on the resource when data has been read.', async(): Promise<void> => {
// Read all data from the representation
const representation = await store.getRepresentation(associatedId, {});
const representation = await store.getRepresentation(subjectId, {});
representation.data.on('data', (): any => true);
registerEventOrder(representation.data, 'end');
@ -174,13 +174,13 @@ describe('A LockingResourceStore', (): void => {
// Verify the lock was acquired and released at the right time
expect(locker.withReadLock).toHaveBeenCalledTimes(1);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(associatedId);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(subjectId);
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
expect(source.getRepresentation).toHaveBeenLastCalledWith(associatedId, {}, undefined);
expect(source.getRepresentation).toHaveBeenLastCalledWith(subjectId, {}, undefined);
expect(order).toEqual([ 'lock read', 'getRepresentation', 'end', 'unlock read' ]);
});
it('acquires the lock on the associated resource when reading an auxiliary resource.', async(): Promise<void> => {
it('acquires the lock on the subject resource when reading an auxiliary resource.', async(): Promise<void> => {
// Read all data from the representation
const representation = await store.getRepresentation(auxiliaryId, {});
representation.data.on('data', (): any => true);
@ -191,7 +191,7 @@ describe('A LockingResourceStore', (): void => {
// Verify the lock was acquired and released at the right time
expect(locker.withReadLock).toHaveBeenCalledTimes(1);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(associatedId);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(subjectId);
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
expect(source.getRepresentation).toHaveBeenLastCalledWith(auxiliaryId, {}, undefined);
expect(order).toEqual([ 'lock read', 'getRepresentation', 'end', 'unlock read' ]);
@ -199,7 +199,7 @@ describe('A LockingResourceStore', (): void => {
it('destroys the resource and releases the lock when the readable errors.', async(): Promise<void> => {
// Make the representation error
const representation = await store.getRepresentation(associatedId, {});
const representation = await store.getRepresentation(subjectId, {});
setImmediate((): any => representation.data.emit('error', new Error('Error on the readable')));
registerEventOrder(representation.data, 'error');
registerEventOrder(representation.data, 'close');
@ -209,7 +209,7 @@ describe('A LockingResourceStore', (): void => {
// Verify the lock was acquired and released at the right time
expect(locker.withReadLock).toHaveBeenCalledTimes(1);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(associatedId);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(subjectId);
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
expect(representation.data.destroy).toHaveBeenCalledTimes(1);
expect(order).toEqual([ 'lock read', 'getRepresentation', 'error', 'unlock read', 'close' ]);
@ -217,7 +217,7 @@ describe('A LockingResourceStore', (): void => {
it('releases the lock on the resource when readable is destroyed.', async(): Promise<void> => {
// Make the representation close
const representation = await store.getRepresentation(associatedId, {});
const representation = await store.getRepresentation(subjectId, {});
representation.data.destroy();
registerEventOrder(representation.data, 'close');
@ -226,14 +226,14 @@ describe('A LockingResourceStore', (): void => {
// Verify the lock was acquired and released at the right time
expect(locker.withReadLock).toHaveBeenCalledTimes(1);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(associatedId);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(subjectId);
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
expect(order).toEqual([ 'lock read', 'getRepresentation', 'close', 'unlock read' ]);
});
it('releases the lock only once when multiple events are triggered.', async(): Promise<void> => {
// Read all data from the representation and trigger an additional close event
const representation = await store.getRepresentation(associatedId, {});
const representation = await store.getRepresentation(subjectId, {});
representation.data.on('data', (): any => true);
representation.data.prependListener('end', (): any => {
order.push('end');
@ -245,13 +245,13 @@ describe('A LockingResourceStore', (): void => {
// Verify the lock was acquired and released at the right time
expect(locker.withReadLock).toHaveBeenCalledTimes(1);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(associatedId);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(subjectId);
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
expect(order).toEqual([ 'lock read', 'getRepresentation', 'end', 'unlock read' ]);
});
it('releases the lock on the resource when readable times out.', async(): Promise<void> => {
const representation = await store.getRepresentation(associatedId, {});
const representation = await store.getRepresentation(subjectId, {});
registerEventOrder(representation.data, 'close');
registerEventOrder(representation.data, 'error');
@ -262,7 +262,7 @@ describe('A LockingResourceStore', (): void => {
// Verify the lock was acquired and released at the right time
expect(locker.withReadLock).toHaveBeenCalledTimes(1);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(associatedId);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(subjectId);
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
expect(representation.data.destroy).toHaveBeenCalledTimes(1);
expect(representation.data.destroy).toHaveBeenLastCalledWith(new Error('timeout'));
@ -276,23 +276,23 @@ describe('A LockingResourceStore', (): void => {
return new Promise(emptyFn);
});
const prom = store.getRepresentation(associatedId, {});
const prom = store.getRepresentation(subjectId, {});
timeoutTrigger.emit('timeout');
await expect(prom).rejects.toThrow('timeout');
expect(locker.withReadLock).toHaveBeenCalledTimes(1);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(associatedId);
expect((locker.withReadLock as jest.Mock).mock.calls[0][0]).toEqual(subjectId);
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
expect(order).toEqual([ 'lock read', 'useless get', 'timeout', 'unlock read' ]);
});
it('resourceExists should only acquire and release the read lock.', async(): Promise<void> => {
await store.resourceExists(associatedId);
await store.resourceExists(subjectId);
expect(locker.withReadLock).toHaveBeenCalledTimes(1);
expect(locker.withWriteLock).toHaveBeenCalledTimes(0);
expect(source.resourceExists).toHaveBeenCalledTimes(1);
expect(source.resourceExists).toHaveBeenLastCalledWith(associatedId, undefined);
expect(source.resourceExists).toHaveBeenLastCalledWith(subjectId, undefined);
expect(order).toEqual([ 'lock read', 'resourceExists', 'unlock read' ]);
});
});