mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
refactor: Rename associated resource to subject resource
This commit is contained in:
parent
13c49045d4
commit
7c7fee5f5c
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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.`);
|
||||
}
|
||||
|
@ -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.`);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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/');
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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) };
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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) };
|
||||
}
|
||||
|
||||
|
@ -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' ]);
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user