diff --git a/config/identity/registration/disabled.json b/config/identity/registration/disabled.json index ac7d0d1f6..967b8e01f 100644 --- a/config/identity/registration/disabled.json +++ b/config/identity/registration/disabled.json @@ -3,6 +3,12 @@ "@graph": [ { "comment": "Disable registration by not attaching a registration handler." + }, + { + "comment": "If registration is disabled, the base URL of the server is the root storage.", + "@id": "urn:solid-server:default:StorageLocationStrategy", + "@type": "RootStorageLocationStrategy", + "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" } } ] } diff --git a/config/identity/registration/enabled.json b/config/identity/registration/enabled.json index 99b9370e1..7dd9fb766 100644 --- a/config/identity/registration/enabled.json +++ b/config/identity/registration/enabled.json @@ -4,6 +4,12 @@ "css:config/identity/registration/route/registration.json" ], "@graph": [ + { + "comment": "If registration is enabled, the pod locations will be root storages.", + "@id": "urn:solid-server:default:StorageLocationStrategy", + "@type": "PodStorageLocationStrategy", + "generator": { "@id": "urn:solid-server:default:IdentifierGenerator" } + }, { "@id": "urn:solid-server:auth:password:InteractionRouteHandler", "@type": "WaterfallHandler", diff --git a/config/ldp/metadata-writer/writers/storage-description.json b/config/ldp/metadata-writer/writers/storage-description.json index c25833df0..7636b4854 100644 --- a/config/ldp/metadata-writer/writers/storage-description.json +++ b/config/ldp/metadata-writer/writers/storage-description.json @@ -5,10 +5,8 @@ "comment": "Adds a link header pointing to the storage description resource.", "@id": "urn:solid-server:default:MetadataWriter_StorageDescription", "@type": "StorageDescriptionAdvertiser", - "targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" }, - "identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" }, - "store": { "@id": "urn:solid-server:default:ResourceStore" }, - "path": { "@id": "urn:solid-server:default:variable:storageDescriptionPath" } + "storageStrategy": { "@id": "urn:solid-server:default:StorageLocationStrategy" }, + "relativePath": { "@id": "urn:solid-server:default:variable:storageDescriptionPath" } } ] } diff --git a/src/http/output/metadata/StorageDescriptionAdvertiser.ts b/src/http/output/metadata/StorageDescriptionAdvertiser.ts index b9adc557a..c166325a4 100644 --- a/src/http/output/metadata/StorageDescriptionAdvertiser.ts +++ b/src/http/output/metadata/StorageDescriptionAdvertiser.ts @@ -1,11 +1,9 @@ import { getLoggerFor } from '../../../logging/LogUtil'; -import type { ResourceStore } from '../../../storage/ResourceStore'; +import type { StorageLocationStrategy } from '../../../server/description/StorageLocationStrategy'; import { createErrorMessage } from '../../../util/errors/ErrorUtil'; import { addHeader } from '../../../util/HeaderUtil'; -import type { IdentifierStrategy } from '../../../util/identifiers/IdentifierStrategy'; import { joinUrl } from '../../../util/PathUtil'; -import { LDP, PIM, RDF, SOLID } from '../../../util/Vocabularies'; -import type { TargetExtractor } from '../../input/identifier/TargetExtractor'; +import { LDP, RDF, SOLID } from '../../../util/Vocabularies'; import type { ResourceIdentifier } from '../../representation/ResourceIdentifier'; import type { MetadataWriterInput } from './MetadataWriter'; import { MetadataWriter } from './MetadataWriter'; @@ -18,18 +16,13 @@ import { MetadataWriter } from './MetadataWriter'; export class StorageDescriptionAdvertiser extends MetadataWriter { protected readonly logger = getLoggerFor(this); - private readonly targetExtractor: TargetExtractor; - private readonly identifierStrategy: IdentifierStrategy; - private readonly store: ResourceStore; - private readonly path: string; + private readonly storageStrategy: StorageLocationStrategy; + private readonly relativePath: string; - public constructor(targetExtractor: TargetExtractor, identifierStrategy: IdentifierStrategy, store: ResourceStore, - path: string) { + public constructor(storageStrategy: StorageLocationStrategy, relativePath: string) { super(); - this.identifierStrategy = identifierStrategy; - this.targetExtractor = targetExtractor; - this.store = store; - this.path = path; + this.storageStrategy = storageStrategy; + this.relativePath = relativePath; } public async handle({ response, metadata }: MetadataWriterInput): Promise { @@ -40,22 +33,13 @@ export class StorageDescriptionAdvertiser extends MetadataWriter { const identifier = { path: metadata.identifier.value }; let storageRoot: ResourceIdentifier; try { - storageRoot = await this.findStorageRoot(identifier); + storageRoot = await this.storageStrategy.getStorageIdentifier(identifier); + this.logger.debug(`Found storage root ${storageRoot.path}`); } catch (error: unknown) { this.logger.error(`Unable to find storage root: ${createErrorMessage(error)}`); return; } - const storageDescription = joinUrl(storageRoot.path, this.path); + const storageDescription = joinUrl(storageRoot.path, this.relativePath); addHeader(response, 'Link', `<${storageDescription}>; rel="${SOLID.storageDescription}"`); } - - private async findStorageRoot(identifier: ResourceIdentifier): Promise { - const representation = await this.store.getRepresentation(identifier, {}); - // We only need the metadata - representation.data.destroy(); - if (representation.metadata.has(RDF.terms.type, PIM.terms.Storage)) { - return identifier; - } - return this.findStorageRoot(this.identifierStrategy.getParentContainer(identifier)); - } } diff --git a/src/index.ts b/src/index.ts index bb5579513..2d3b94977 100644 --- a/src/index.ts +++ b/src/index.ts @@ -295,9 +295,12 @@ export * from './server/WacAllowHttpHandler'; export * from './server/WebSocketServerConfigurator'; // Server/Description +export * from './server/description/PodStorageLocationStrategy'; +export * from './server/description/RootStorageLocationStrategy'; export * from './server/description/StaticStorageDescriber'; export * from './server/description/StorageDescriber'; export * from './server/description/StorageDescriptionHandler'; +export * from './server/description/StorageLocationStrategy'; // Server/Middleware export * from './server/middleware/AcpHeaderHandler'; diff --git a/src/pods/generate/IdentifierGenerator.ts b/src/pods/generate/IdentifierGenerator.ts index be6fa6208..9f961af6a 100644 --- a/src/pods/generate/IdentifierGenerator.ts +++ b/src/pods/generate/IdentifierGenerator.ts @@ -9,4 +9,10 @@ export interface IdentifierGenerator { * This is simply string generation, no resource-related checks are run. */ generate: (name: string) => ResourceIdentifier; + + /** + * Extracts the root pod this identifier would be in. + * This assumes the identifier of that pod was generated by the same instance of this interface. + */ + extractPod: (identifier: ResourceIdentifier) => ResourceIdentifier; } diff --git a/src/pods/generate/SubdomainIdentifierGenerator.ts b/src/pods/generate/SubdomainIdentifierGenerator.ts index 38b02c50c..6cd3f7cd0 100644 --- a/src/pods/generate/SubdomainIdentifierGenerator.ts +++ b/src/pods/generate/SubdomainIdentifierGenerator.ts @@ -1,4 +1,5 @@ import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; +import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { ensureTrailingSlash, extractScheme } from '../../util/PathUtil'; import { sanitizeUrlPart } from '../../util/StringUtil'; import type { IdentifierGenerator } from './IdentifierGenerator'; @@ -19,4 +20,24 @@ export class SubdomainIdentifierGenerator implements IdentifierGenerator { const cleanName = sanitizeUrlPart(name); return { path: `${this.baseParts.scheme}${cleanName}.${this.baseParts.rest}` }; } + + public extractPod(identifier: ResourceIdentifier): ResourceIdentifier { + const { path } = identifier; + + // Invalid identifiers that have no result should never reach this point, + // but some safety checks just in case. + if (!path.startsWith(this.baseParts.scheme)) { + throw new BadRequestHttpError(`Invalid identifier ${path}`); + } + + const idx = path.indexOf(this.baseParts.rest); + + // If the idx is smaller than this, either there was no match, or there is no subdomain + if (idx <= this.baseParts.scheme.length) { + throw new BadRequestHttpError(`Invalid identifier ${path}`); + } + + // Slice of everything after the base URL tail + return { path: path.slice(0, idx + this.baseParts.rest.length) }; + } } diff --git a/src/pods/generate/SuffixIdentifierGenerator.ts b/src/pods/generate/SuffixIdentifierGenerator.ts index 335042826..ba070370d 100644 --- a/src/pods/generate/SuffixIdentifierGenerator.ts +++ b/src/pods/generate/SuffixIdentifierGenerator.ts @@ -1,4 +1,5 @@ import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; +import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { ensureTrailingSlash } from '../../util/PathUtil'; import { sanitizeUrlPart } from '../../util/StringUtil'; import type { IdentifierGenerator } from './IdentifierGenerator'; @@ -18,4 +19,24 @@ export class SuffixIdentifierGenerator implements IdentifierGenerator { const cleanName = sanitizeUrlPart(name); return { path: ensureTrailingSlash(new URL(cleanName, this.base).href) }; } + + public extractPod(identifier: ResourceIdentifier): ResourceIdentifier { + const { path } = identifier; + + // Invalid identifiers that have no result should never reach this point, + // but some safety checks just in case. + if (!path.startsWith(this.base)) { + throw new BadRequestHttpError(`Invalid identifier ${path}`); + } + + // The first slash after the base URL indicates the first container on the path + const idx = path.indexOf('/', this.base.length + 1); + + if (idx < 0) { + throw new BadRequestHttpError(`Invalid identifier ${path}`); + } + + // Slice of everything after the first container + return { path: path.slice(0, idx + 1) }; + } } diff --git a/src/server/description/PodStorageLocationStrategy.ts b/src/server/description/PodStorageLocationStrategy.ts new file mode 100644 index 000000000..fb4aa18ec --- /dev/null +++ b/src/server/description/PodStorageLocationStrategy.ts @@ -0,0 +1,20 @@ +import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; +import type { IdentifierGenerator } from '../../pods/generate/IdentifierGenerator'; +import type { StorageLocationStrategy } from './StorageLocationStrategy'; + +/** + * A {@link StorageLocationStrategy} to be used when the server has pods which each are a different storage. + * The {@link IdentifierGenerator} that is used to generate URLs for the pods + * is used here to determine what the root pod URL is. + */ +export class PodStorageLocationStrategy implements StorageLocationStrategy { + private readonly generator: IdentifierGenerator; + + public constructor(generator: IdentifierGenerator) { + this.generator = generator; + } + + public async getStorageIdentifier(identifier: ResourceIdentifier): Promise { + return this.generator.extractPod(identifier); + } +} diff --git a/src/server/description/RootStorageLocationStrategy.ts b/src/server/description/RootStorageLocationStrategy.ts new file mode 100644 index 000000000..7a9fbf230 --- /dev/null +++ b/src/server/description/RootStorageLocationStrategy.ts @@ -0,0 +1,17 @@ +import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; +import type { StorageLocationStrategy } from './StorageLocationStrategy'; + +/** + * A {@link StorageLocationStrategy} to be used when the server has one storage in the root container of the server. + */ +export class RootStorageLocationStrategy implements StorageLocationStrategy { + private readonly root: ResourceIdentifier; + + public constructor(baseUrl: string) { + this.root = { path: baseUrl }; + } + + public async getStorageIdentifier(): Promise { + return this.root; + } +} diff --git a/src/server/description/StorageLocationStrategy.ts b/src/server/description/StorageLocationStrategy.ts new file mode 100644 index 000000000..2b723bcf0 --- /dev/null +++ b/src/server/description/StorageLocationStrategy.ts @@ -0,0 +1,12 @@ +import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; + +/** + * Interface used to find the storage a specific identifier is located in. + */ +export interface StorageLocationStrategy { + /** + * Returns the identifier of the storage that contains the given resource. + * Can error if the input identifier is not part of any storage. + */ + getStorageIdentifier: (identifier: ResourceIdentifier) => Promise; +} diff --git a/test/integration/config/ldp-with-acp.json b/test/integration/config/ldp-with-acp.json index ac6e3da7c..896a17e47 100644 --- a/test/integration/config/ldp-with-acp.json +++ b/test/integration/config/ldp-with-acp.json @@ -14,7 +14,7 @@ "css:config/identity/handler/default.json", "css:config/identity/ownership/token.json", "css:config/identity/pod/static.json", - + "css:config/identity/registration/disabled.json", "css:config/ldp/authentication/debug-auth-header.json", "css:config/ldp/authorization/acp.json", "css:config/ldp/handler/default.json", diff --git a/test/integration/config/permission-table.json b/test/integration/config/permission-table.json index 2eb5f2fe6..ad95277ea 100644 --- a/test/integration/config/permission-table.json +++ b/test/integration/config/permission-table.json @@ -14,7 +14,7 @@ "css:config/identity/handler/default.json", "css:config/identity/ownership/token.json", "css:config/identity/pod/static.json", - + "css:config/identity/registration/disabled.json", "css:config/ldp/authentication/debug-auth-header.json", "css:config/ldp/handler/default.json", diff --git a/test/integration/config/server-redis-lock.json b/test/integration/config/server-redis-lock.json index b4e13247c..86c89e086 100644 --- a/test/integration/config/server-redis-lock.json +++ b/test/integration/config/server-redis-lock.json @@ -14,7 +14,7 @@ "css:config/identity/pod/static.json", - + "css:config/identity/registration/disabled.json", "css:config/ldp/authentication/debug-auth-header.json", "css:config/ldp/authorization/allow-all.json", "css:config/ldp/handler/default.json", diff --git a/test/integration/config/webhook-notifications.json b/test/integration/config/webhook-notifications.json index abfd04cdb..d3f21adeb 100644 --- a/test/integration/config/webhook-notifications.json +++ b/test/integration/config/webhook-notifications.json @@ -14,7 +14,7 @@ "css:config/identity/handler/default.json", "css:config/identity/ownership/token.json", "css:config/identity/pod/static.json", - "css:config/identity/registration/enabled.json", + "css:config/identity/registration/disabled.json", "css:config/ldp/authentication/debug-auth-header.json", "css:config/ldp/authorization/webacl.json", "css:config/ldp/handler/default.json", diff --git a/test/integration/config/websocket-notifications.json b/test/integration/config/websocket-notifications.json index e0b7f56b6..80b7acdfd 100644 --- a/test/integration/config/websocket-notifications.json +++ b/test/integration/config/websocket-notifications.json @@ -14,7 +14,7 @@ "css:config/identity/handler/default.json", "css:config/identity/ownership/token.json", "css:config/identity/pod/static.json", - "css:config/identity/registration/enabled.json", + "css:config/identity/registration/disabled.json", "css:config/ldp/authentication/debug-auth-header.json", "css:config/ldp/authorization/webacl.json", "css:config/ldp/handler/default.json", diff --git a/test/unit/http/output/metadata/StorageDescriptionAdvertiser.test.ts b/test/unit/http/output/metadata/StorageDescriptionAdvertiser.test.ts index 66a41f36b..abf270bfa 100644 --- a/test/unit/http/output/metadata/StorageDescriptionAdvertiser.test.ts +++ b/test/unit/http/output/metadata/StorageDescriptionAdvertiser.test.ts @@ -1,24 +1,18 @@ -import type { TargetExtractor } from '../../../../../src/http/input/identifier/TargetExtractor'; import type { MetadataWriterInput } from '../../../../../src/http/output/metadata/MetadataWriter'; import { StorageDescriptionAdvertiser } from '../../../../../src/http/output/metadata/StorageDescriptionAdvertiser'; -import { BasicRepresentation } from '../../../../../src/http/representation/BasicRepresentation'; import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata'; -import type { ResourceIdentifier } from '../../../../../src/http/representation/ResourceIdentifier'; +import type { StorageLocationStrategy } from '../../../../../src/server/description/StorageLocationStrategy'; import type { HttpResponse } from '../../../../../src/server/HttpResponse'; -import type { ResourceStore } from '../../../../../src/storage/ResourceStore'; -import { SingleRootIdentifierStrategy } from '../../../../../src/util/identifiers/SingleRootIdentifierStrategy'; -import { joinUrl } from '../../../../../src/util/PathUtil'; -import { LDP, PIM, RDF } from '../../../../../src/util/Vocabularies'; +import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError'; +import { LDP, RDF } from '../../../../../src/util/Vocabularies'; describe('A StorageDescriptionAdvertiser', (): void => { let metadata: RepresentationMetadata; let response: jest.Mocked; let input: MetadataWriterInput; - const baseUrl = 'http://example.com/'; - const path = '.well-known/solid'; - let targetExtractor: jest.Mocked; - const identifierStrategy = new SingleRootIdentifierStrategy(baseUrl); - let store: jest.Mocked; + const storageIdentifier = { path: 'http://example.com/foo/' }; + let strategy: jest.Mocked; + const relativePath = '.well-known/solid'; let advertiser: StorageDescriptionAdvertiser; beforeEach(async(): Promise => { @@ -33,15 +27,11 @@ describe('A StorageDescriptionAdvertiser', (): void => { input = { metadata, response }; - targetExtractor = { - handleSafe: jest.fn(({ request: req }): ResourceIdentifier => ({ path: joinUrl(baseUrl, req.url!) })), - } as any; + strategy = { + getStorageIdentifier: jest.fn().mockResolvedValue(storageIdentifier), + }; - store = { - getRepresentation: jest.fn().mockResolvedValue(new BasicRepresentation('', { [RDF.type]: PIM.terms.Storage })), - } as any; - - advertiser = new StorageDescriptionAdvertiser(targetExtractor, identifierStrategy, store, path); + advertiser = new StorageDescriptionAdvertiser(strategy, relativePath); }); it('adds a storage description link header.', async(): Promise => { @@ -59,7 +49,7 @@ describe('A StorageDescriptionAdvertiser', (): void => { it('does nothing if it cannot find a storage root.', async(): Promise => { // No storage container will be found - store.getRepresentation.mockResolvedValue(new BasicRepresentation()); + strategy.getStorageIdentifier.mockRejectedValue(new BadRequestHttpError('bad identifier')); await expect(advertiser.handle(input)).resolves.toBeUndefined(); expect(response.setHeader).toHaveBeenCalledTimes(0); }); diff --git a/test/unit/identity/interaction/email-password/util/RegistrationManager.test.ts b/test/unit/identity/interaction/email-password/util/RegistrationManager.test.ts index c72150aec..f6befb7de 100644 --- a/test/unit/identity/interaction/email-password/util/RegistrationManager.test.ts +++ b/test/unit/identity/interaction/email-password/util/RegistrationManager.test.ts @@ -36,6 +36,7 @@ describe('A RegistrationManager', (): void => { identifierGenerator = { generate: jest.fn((name: string): ResourceIdentifier => ({ path: `${baseUrl}${name}/` })), + extractPod: jest.fn(), }; ownershipValidator = { diff --git a/test/unit/pods/generate/SubdomainIdentifierGenerator.test.ts b/test/unit/pods/generate/SubdomainIdentifierGenerator.test.ts index fc16a309c..39184ec5a 100644 --- a/test/unit/pods/generate/SubdomainIdentifierGenerator.test.ts +++ b/test/unit/pods/generate/SubdomainIdentifierGenerator.test.ts @@ -1,14 +1,35 @@ import { SubdomainIdentifierGenerator } from '../../../../src/pods/generate/SubdomainIdentifierGenerator'; +import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError'; describe('A SubdomainIdentifierGenerator', (): void => { - const base = 'http://test.com/'; + const base = 'http://example.com/'; const generator = new SubdomainIdentifierGenerator(base); it('generates identifiers by using the slug as subdomain.', async(): Promise => { - expect(generator.generate('slug')).toEqual({ path: 'http://slug.test.com/' }); + expect(generator.generate('slug')).toEqual({ path: 'http://slug.example.com/' }); }); it('converts slugs using punycode.', async(): Promise => { - expect(generator.generate('sàl/u㋡g')).toEqual({ path: 'http://s-l-u-g.test.com/' }); + expect(generator.generate('sàl/u㋡g')).toEqual({ path: 'http://s-l-u-g.example.com/' }); + }); + + it('can extract the pod from an identifier.', async(): Promise => { + const identifier = { path: 'http://foo.example.com/bar/baz' }; + expect(generator.extractPod(identifier)).toEqual({ path: 'http://foo.example.com/' }); + }); + + it('can detect if the identifier itself is the pod.', async(): Promise => { + const identifier = { path: 'http://foo.example.com/' }; + expect(generator.extractPod(identifier)).toEqual({ path: 'http://foo.example.com/' }); + }); + + it('errors when extracting if the identifier has the wrong scheme.', async(): Promise => { + const identifier = { path: 'https://foo.example.com/bar/baz' }; + expect((): any => generator.extractPod(identifier)).toThrow(BadRequestHttpError); + }); + + it('errors when extracting if there is no pod.', async(): Promise => { + const identifier = { path: 'http://example.com/bar/baz' }; + expect((): any => generator.extractPod(identifier)).toThrow(BadRequestHttpError); }); }); diff --git a/test/unit/pods/generate/SuffixIdentifierGenerator.test.ts b/test/unit/pods/generate/SuffixIdentifierGenerator.test.ts index 724e14cda..b7ead2c0c 100644 --- a/test/unit/pods/generate/SuffixIdentifierGenerator.test.ts +++ b/test/unit/pods/generate/SuffixIdentifierGenerator.test.ts @@ -1,7 +1,8 @@ import { SuffixIdentifierGenerator } from '../../../../src/pods/generate/SuffixIdentifierGenerator'; +import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError'; describe('A SuffixIdentifierGenerator', (): void => { - const base = 'http://test.com/'; + const base = 'http://example.com/'; const generator = new SuffixIdentifierGenerator(base); it('generates identifiers by appending the slug.', async(): Promise => { @@ -11,4 +12,24 @@ describe('A SuffixIdentifierGenerator', (): void => { it('converts non-alphanumerics to dashes.', async(): Promise => { expect(generator.generate('sàl/u㋡g')).toEqual({ path: `${base}s-l-u-g/` }); }); + + it('can extract the pod from an identifier.', async(): Promise => { + const identifier = { path: 'http://example.com/foo/bar/baz' }; + expect(generator.extractPod(identifier)).toEqual({ path: 'http://example.com/foo/' }); + }); + + it('can detect if the identifier itself is the pod.', async(): Promise => { + const identifier = { path: 'http://example.com/foo/' }; + expect(generator.extractPod(identifier)).toEqual({ path: 'http://example.com/foo/' }); + }); + + it('errors when extracting if the identifier is in the wrong domain.', async(): Promise => { + const identifier = { path: 'http://bad.example.com/foo/bar/baz' }; + expect((): any => generator.extractPod(identifier)).toThrow(BadRequestHttpError); + }); + + it('errors when extracting if there is no pod.', async(): Promise => { + const identifier = { path: 'http://example.com/foo' }; + expect((): any => generator.extractPod(identifier)).toThrow(BadRequestHttpError); + }); }); diff --git a/test/unit/server/description/PodStorageLocationStrategy.test.ts b/test/unit/server/description/PodStorageLocationStrategy.test.ts new file mode 100644 index 000000000..4c1219135 --- /dev/null +++ b/test/unit/server/description/PodStorageLocationStrategy.test.ts @@ -0,0 +1,15 @@ +import type { IdentifierGenerator } from '../../../../src/pods/generate/IdentifierGenerator'; +import { PodStorageLocationStrategy } from '../../../../src/server/description/PodStorageLocationStrategy'; + +describe('A PodStorageLocationStrategy', (): void => { + const generator: IdentifierGenerator = { + generate: jest.fn(), + extractPod: jest.fn().mockReturnValue({ path: 'http://example.com/' }), + }; + const strategy = new PodStorageLocationStrategy(generator); + + it('returns the result of the identifier generator.', async(): Promise => { + await expect(strategy.getStorageIdentifier({ path: 'http://example.com/whatever' })) + .resolves.toEqual({ path: 'http://example.com/' }); + }); +}); diff --git a/test/unit/server/description/RootStorageLocationStrategy.test.ts b/test/unit/server/description/RootStorageLocationStrategy.test.ts new file mode 100644 index 000000000..eb2fe7517 --- /dev/null +++ b/test/unit/server/description/RootStorageLocationStrategy.test.ts @@ -0,0 +1,10 @@ +import { RootStorageLocationStrategy } from '../../../../src/server/description/RootStorageLocationStrategy'; + +describe('A RootStorageLocationStrategy', (): void => { + const baseUrl = 'http://example.com/'; + const strategy = new RootStorageLocationStrategy(baseUrl); + + it('returns the base URL.', async(): Promise => { + await expect(strategy.getStorageIdentifier()).resolves.toEqual({ path: baseUrl }); + }); +});