From b1601211769f56258f65943109413fa3f9bd3618 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 25 Feb 2021 17:04:52 +0100 Subject: [PATCH] feat: Add implementation for dynamically instantiating pods --- src/index.ts | 10 +++ src/pods/generate/BaseComponentsJsFactory.ts | 43 ++++++++++ src/pods/generate/ComponentsJsFactory.ts | 14 +++ src/pods/generate/TemplatedPodGenerator.ts | 86 +++++++++++++++++++ src/pods/generate/variables/BaseUrlHandler.ts | 16 ++++ .../generate/variables/RootFilePathHandler.ts | 37 ++++++++ .../generate/variables/VariableHandler.ts | 11 +++ src/pods/generate/variables/VariableSetter.ts | 25 ++++++ src/pods/generate/variables/Variables.ts | 20 +++++ .../generate/BaseComponentsJsFactory.test.ts | 39 +++++++++ .../generate/TemplatedPodGenerator.test.ts | 84 ++++++++++++++++++ .../generate/variables/BaseUrlHandler.test.ts | 14 +++ .../variables/RootFilePathHandler.test.ts | 45 ++++++++++ .../generate/variables/VariableSetter.test.ts | 33 +++++++ 14 files changed, 477 insertions(+) create mode 100644 src/pods/generate/BaseComponentsJsFactory.ts create mode 100644 src/pods/generate/ComponentsJsFactory.ts create mode 100644 src/pods/generate/TemplatedPodGenerator.ts create mode 100644 src/pods/generate/variables/BaseUrlHandler.ts create mode 100644 src/pods/generate/variables/RootFilePathHandler.ts create mode 100644 src/pods/generate/variables/VariableHandler.ts create mode 100644 src/pods/generate/variables/VariableSetter.ts create mode 100644 src/pods/generate/variables/Variables.ts create mode 100644 test/unit/pods/generate/BaseComponentsJsFactory.test.ts create mode 100644 test/unit/pods/generate/TemplatedPodGenerator.test.ts create mode 100644 test/unit/pods/generate/variables/BaseUrlHandler.test.ts create mode 100644 test/unit/pods/generate/variables/RootFilePathHandler.test.ts create mode 100644 test/unit/pods/generate/variables/VariableSetter.test.ts diff --git a/src/index.ts b/src/index.ts index 3c0be75cd..b88ad7565 100644 --- a/src/index.ts +++ b/src/index.ts @@ -108,7 +108,16 @@ export * from './logging/LogUtil'; export * from './logging/VoidLoggerFactory'; export * from './logging/WinstonLoggerFactory'; +// Pods/Generate/Variables +export * from './pods/generate/variables/BaseUrlHandler'; +export * from './pods/generate/variables/RootFilePathHandler'; +export * from './pods/generate/variables/VariableHandler'; +export * from './pods/generate/variables/Variables'; +export * from './pods/generate/variables/VariableSetter'; + // Pods/Generate +export * from './pods/generate/BaseComponentsJsFactory'; +export * from './pods/generate/ComponentsJsFactory'; export * from './pods/generate/GenerateUtil'; export * from './pods/generate/HandlebarsTemplateEngine'; export * from './pods/generate/IdentifierGenerator'; @@ -116,6 +125,7 @@ export * from './pods/generate/PodGenerator'; export * from './pods/generate/ResourcesGenerator'; export * from './pods/generate/SubdomainIdentifierGenerator'; export * from './pods/generate/SuffixIdentifierGenerator'; +export * from './pods/generate/TemplatedPodGenerator'; export * from './pods/generate/TemplateEngine'; export * from './pods/generate/TemplatedResourcesGenerator'; diff --git a/src/pods/generate/BaseComponentsJsFactory.ts b/src/pods/generate/BaseComponentsJsFactory.ts new file mode 100644 index 000000000..9bac8bc5e --- /dev/null +++ b/src/pods/generate/BaseComponentsJsFactory.ts @@ -0,0 +1,43 @@ +import type { IComponentsManagerBuilderOptions, LogLevel } from 'componentsjs'; +import { ComponentsManager } from 'componentsjs'; +import { joinFilePath } from '../../util/PathUtil'; +import type { ComponentsJsFactory } from './ComponentsJsFactory'; + +/** + * Can be used to instantiate objects using Components.js. + * Default main module path is the root folder of the project. + * For every generate call a new manager will be made, + * but moduleState will be stored in between calls. + */ +export class BaseComponentsJsFactory implements ComponentsJsFactory { + private readonly options: IComponentsManagerBuilderOptions; + + public constructor(relativeModulePath = '../../../', logLevel = 'error') { + this.options = { + mainModulePath: joinFilePath(__dirname, relativeModulePath), + logLevel: logLevel as LogLevel, + dumpErrorState: false, + }; + } + + private async buildManager(): Promise> { + const manager = await ComponentsManager.build(this.options); + this.options.moduleState = manager.moduleState; + return manager; + } + + /** + * Calls Components.js to instantiate a new object. + * @param configPath - Location of the config to instantiate. + * @param componentIri - Iri of the object in the config that will be the result. + * @param variables - Variables to send to Components.js + * + * @returns The resulting object, corresponding to the given component IRI. + */ + public async generate(configPath: string, componentIri: string, variables: Record): + Promise { + const manager = await this.buildManager(); + await manager.configRegistry.register(configPath); + return await manager.instantiate(componentIri, { variables }); + } +} diff --git a/src/pods/generate/ComponentsJsFactory.ts b/src/pods/generate/ComponentsJsFactory.ts new file mode 100644 index 000000000..318d6a258 --- /dev/null +++ b/src/pods/generate/ComponentsJsFactory.ts @@ -0,0 +1,14 @@ +/** + * Used for instantiating new object using Components.js configurations. + */ +export interface ComponentsJsFactory { + /** + * Instantiates a new object using Components.js. + * @param configPath - Location of the config to instantiate. + * @param componentIri - Iri of the object in the config that will be the result. + * @param variables - Variables to send to Components.js + * + * @returns The resulting object, corresponding to the given component IRI. + */ + generate: (configPath: string, componentIri: string, variables: Record) => Promise; +} diff --git a/src/pods/generate/TemplatedPodGenerator.ts b/src/pods/generate/TemplatedPodGenerator.ts new file mode 100644 index 000000000..7e5433b0d --- /dev/null +++ b/src/pods/generate/TemplatedPodGenerator.ts @@ -0,0 +1,86 @@ +import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; +import { getLoggerFor } from '../../logging/LogUtil'; +import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage'; +import type { ResourceStore } from '../../storage/ResourceStore'; +import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; +import { ConflictHttpError } from '../../util/errors/ConflictHttpError'; +import { joinFilePath } from '../../util/PathUtil'; +import type { PodSettings } from '../settings/PodSettings'; +import type { ComponentsJsFactory } from './ComponentsJsFactory'; +import type { PodGenerator } from './PodGenerator'; +import type { VariableHandler } from './variables/VariableHandler'; +import { isValidVariable, TEMPLATE, TEMPLATE_VARIABLE } from './variables/Variables'; + +const DEFAULT_CONFIG_PATH = joinFilePath(__dirname, '../../../config/templates'); + +/** + * Creates a new ResourceStore when creating a pod based on a Components.js configuration. + * + * Part of the dynamic pod creation. + * 1. It calls a VariableHandler to add necessary variable values. + * E.g. setting the base url variable for components.js to the pod identifier. + * 2. It filters/cleans the input agent values using {@link VariableHandler}s + * 3. It calls a ComponentsJsFactory with the variables and template location to instantiate a new ResourceStore. + * 4. It stores these values in the configuration storage, which is used as a permanent storage for pod configurations. + * + * @see {@link ConfigPodManager}, {@link ConfigPodInitializer}, {@link BaseUrlRouterRule} + */ +export class TemplatedPodGenerator implements PodGenerator { + protected readonly logger = getLoggerFor(this); + private readonly storeFactory: ComponentsJsFactory; + private readonly variableHandler: VariableHandler; + private readonly configStorage: KeyValueStorage; + private readonly configTemplatePath: string; + + /** + * @param storeFactory - Factory used for Components.js instantiation. + * @param variableHandler - Handler used for setting variable values. + * @param configStorage - Where to store the configuration values to instantiate the store for this pod. + * @param configTemplatePath - Where to find the configuration templates. + */ + public constructor(storeFactory: ComponentsJsFactory, variableHandler: VariableHandler, + configStorage: KeyValueStorage, configTemplatePath?: string) { + this.storeFactory = storeFactory; + this.variableHandler = variableHandler; + this.configStorage = configStorage; + this.configTemplatePath = configTemplatePath ?? DEFAULT_CONFIG_PATH; + } + + public async generate(identifier: ResourceIdentifier, settings: PodSettings): Promise { + if (!settings.template) { + throw new BadRequestHttpError('Settings require template field.'); + } + + if (await this.configStorage.has(identifier.path)) { + this.logger.warn(`There already is a pod at ${identifier.path}`); + throw new ConflictHttpError(`There already is a pod at ${identifier.path}`); + } + + await this.variableHandler.handleSafe({ identifier, settings }); + + // Filter out irrelevant data in the agent + const variables: NodeJS.Dict = {}; + for (const key of Object.keys(settings)) { + if (isValidVariable(key)) { + variables[key] = settings[key]; + } + } + + // Prevent unsafe template names + if (!/^[a-zA-Z0-9.-]+$/u.test(settings.template)) { + this.logger.warn(`Invalid template name ${settings.template}`); + throw new BadRequestHttpError(`Invalid template name ${settings.template}`); + } + // Storing the template in the variables so it also gets stored in the config for later re-use + variables[TEMPLATE_VARIABLE.templateConfig] = joinFilePath(this.configTemplatePath, settings.template); + + const store: ResourceStore = + await this.storeFactory.generate(variables[TEMPLATE_VARIABLE.templateConfig]!, TEMPLATE.ResourceStore, variables); + this.logger.debug(`Generating store ${identifier.path} with variables ${JSON.stringify(variables)}`); + + // Store the variables permanently + await this.configStorage.set(identifier.path, variables); + + return store; + } +} diff --git a/src/pods/generate/variables/BaseUrlHandler.ts b/src/pods/generate/variables/BaseUrlHandler.ts new file mode 100644 index 000000000..06d8bd9c5 --- /dev/null +++ b/src/pods/generate/variables/BaseUrlHandler.ts @@ -0,0 +1,16 @@ +import type { ResourceIdentifier } from '../../../ldp/representation/ResourceIdentifier'; +import type { PodSettings } from '../../settings/PodSettings'; +import { VariableHandler } from './VariableHandler'; +import { TEMPLATE_VARIABLE } from './Variables'; + +/** + * Adds the pod identifier as base url variable to the agent. + * This allows for config templates that require a value for TEMPLATE_BASE_URL_URN, + * which should equal the pod identifier. + */ +export class BaseUrlHandler extends VariableHandler { + public async handle({ identifier, settings }: { identifier: ResourceIdentifier; settings: PodSettings }): + Promise { + settings[TEMPLATE_VARIABLE.baseUrl] = identifier.path; + } +} diff --git a/src/pods/generate/variables/RootFilePathHandler.ts b/src/pods/generate/variables/RootFilePathHandler.ts new file mode 100644 index 000000000..e2b38fdfb --- /dev/null +++ b/src/pods/generate/variables/RootFilePathHandler.ts @@ -0,0 +1,37 @@ +import { promises as fsPromises } from 'fs'; +import type { ResourceIdentifier } from '../../../ldp/representation/ResourceIdentifier'; +import type { FileIdentifierMapper } from '../../../storage/mapping/FileIdentifierMapper'; +import { ConflictHttpError } from '../../../util/errors/ConflictHttpError'; +import { isSystemError } from '../../../util/errors/SystemError'; +import type { PodSettings } from '../../settings/PodSettings'; +import { VariableHandler } from './VariableHandler'; +import { TEMPLATE_VARIABLE } from './Variables'; + +/** + * Uses a FileIdentifierMapper to generate a root file path variable based on the identifier. + * Will throw an error if the resulting file path already exists. + */ +export class RootFilePathHandler extends VariableHandler { + private readonly fileMapper: FileIdentifierMapper; + + public constructor(fileMapper: FileIdentifierMapper) { + super(); + this.fileMapper = fileMapper; + } + + public async handle({ identifier, settings }: { identifier: ResourceIdentifier; settings: PodSettings }): + Promise { + const path = (await this.fileMapper.mapUrlToFilePath(identifier)).filePath; + try { + // Even though we check if it already exists, there is still a potential race condition + // in between this check and the store being created. + await fsPromises.access(path); + throw new ConflictHttpError(`There already is a folder that corresponds to ${identifier.path}`); + } catch (error: unknown) { + if (!(isSystemError(error) && error.code === 'ENOENT')) { + throw error; + } + settings[TEMPLATE_VARIABLE.rootFilePath] = path; + } + } +} diff --git a/src/pods/generate/variables/VariableHandler.ts b/src/pods/generate/variables/VariableHandler.ts new file mode 100644 index 000000000..9e13d929d --- /dev/null +++ b/src/pods/generate/variables/VariableHandler.ts @@ -0,0 +1,11 @@ +import type { ResourceIdentifier } from '../../../ldp/representation/ResourceIdentifier'; +import { AsyncHandler } from '../../../util/handlers/AsyncHandler'; +import type { PodSettings } from '../../settings/PodSettings'; + +/** + * Updates the variables stored in the given agent. + * Can be used to set variables that are required for the Components.js instantiation + * but which should not be provided by the request. + * E.g.: The exact file path (when required) should be determined by the server to prevent abuse. + */ +export abstract class VariableHandler extends AsyncHandler<{ identifier: ResourceIdentifier; settings: PodSettings }> {} diff --git a/src/pods/generate/variables/VariableSetter.ts b/src/pods/generate/variables/VariableSetter.ts new file mode 100644 index 000000000..c559fc63b --- /dev/null +++ b/src/pods/generate/variables/VariableSetter.ts @@ -0,0 +1,25 @@ +import type { PodSettings } from '../../settings/PodSettings'; +import { VariableHandler } from './VariableHandler'; + +/** + * A VariableHandler that will set the given variable to the given value, + * unless there already is a value for the variable and override is false. + */ +export class VariableSetter extends VariableHandler { + private readonly variable: string; + private readonly value: string; + private readonly override: boolean; + + public constructor(variable: string, value: string, override = false) { + super(); + this.variable = variable; + this.value = value; + this.override = override; + } + + public async handle({ settings }: { settings: PodSettings }): Promise { + if (this.override || !settings[this.variable]) { + settings[this.variable] = this.value; + } + } +} diff --git a/src/pods/generate/variables/Variables.ts b/src/pods/generate/variables/Variables.ts new file mode 100644 index 000000000..1440134bb --- /dev/null +++ b/src/pods/generate/variables/Variables.ts @@ -0,0 +1,20 @@ +import { createUriAndTermNamespace } from '../../../util/Vocabularies'; + +export const TEMPLATE = createUriAndTermNamespace('urn:solid-server:template:', + 'ResourceStore'); + +// Variables used for configuration templates +// This is not an exclusive list +export const TEMPLATE_VARIABLE = createUriAndTermNamespace(`${TEMPLATE.namespace}variable:`, + 'baseUrl', + 'rootFilePath', + 'sparqlEndpoint', + 'templateConfig'); + +/** + * Checks if the given variable is one that is supported. + * This can be used to weed out irrelevant parameters in an object. + */ +export function isValidVariable(variable: string): boolean { + return variable.startsWith(TEMPLATE_VARIABLE.namespace); +} diff --git a/test/unit/pods/generate/BaseComponentsJsFactory.test.ts b/test/unit/pods/generate/BaseComponentsJsFactory.test.ts new file mode 100644 index 000000000..cf48013a9 --- /dev/null +++ b/test/unit/pods/generate/BaseComponentsJsFactory.test.ts @@ -0,0 +1,39 @@ +import type { ComponentsManager } from 'componentsjs'; +import { BaseComponentsJsFactory } from '../../../../src/pods/generate/BaseComponentsJsFactory'; + +const manager: jest.Mocked> = { + instantiate: jest.fn(async(): Promise => 'store!'), + configRegistry: { + register: jest.fn(), + }, +} as any; + +jest.mock('componentsjs', (): any => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + ComponentsManager: { + build: jest.fn(async(): Promise> => manager), + }, +})); + +describe('A BaseComponentsJsFactory', (): void => { + let factory: BaseComponentsJsFactory; + const configPath = 'config!'; + const componentIri = 'componentIri!'; + const variables = { + aa: 'b', + cc: 'd', + }; + + beforeEach(async(): Promise => { + jest.clearAllMocks(); + factory = new BaseComponentsJsFactory(); + }); + + it('calls Components.js with the given values.', async(): Promise => { + await expect(factory.generate(configPath, componentIri, variables)).resolves.toBe('store!'); + expect(manager.configRegistry.register).toHaveBeenCalledTimes(1); + expect(manager.configRegistry.register).toHaveBeenLastCalledWith(configPath); + expect(manager.instantiate).toHaveBeenCalledTimes(1); + expect(manager.instantiate).toHaveBeenLastCalledWith(componentIri, { variables }); + }); +}); diff --git a/test/unit/pods/generate/TemplatedPodGenerator.test.ts b/test/unit/pods/generate/TemplatedPodGenerator.test.ts new file mode 100644 index 000000000..eba4c425f --- /dev/null +++ b/test/unit/pods/generate/TemplatedPodGenerator.test.ts @@ -0,0 +1,84 @@ +import type { ComponentsJsFactory } from '../../../../src/pods/generate/ComponentsJsFactory'; +import { TemplatedPodGenerator } from '../../../../src/pods/generate/TemplatedPodGenerator'; +import type { VariableHandler } from '../../../../src/pods/generate/variables/VariableHandler'; +import { TEMPLATE, TEMPLATE_VARIABLE } from '../../../../src/pods/generate/variables/Variables'; +import type { PodSettings } from '../../../../src/pods/settings/PodSettings'; +import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage'; +import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError'; +import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError'; +import { joinFilePath } from '../../../../src/util/PathUtil'; + +describe('A TemplatedPodGenerator', (): void => { + const configTemplatePath = 'templates/config/'; + const template = 'config-template.json'; + const templatePath = `${configTemplatePath}${template}`; + const identifier = { path: 'http://test.com/alice/' }; + let settings: PodSettings; + let storeFactory: ComponentsJsFactory; + let variableHandler: VariableHandler; + let configStorage: KeyValueStorage; + let generator: TemplatedPodGenerator; + + beforeEach(async(): Promise => { + settings = { template } as any; + + storeFactory = { + generate: jest.fn().mockResolvedValue('store'), + } as any; + + variableHandler = { + handleSafe: jest.fn(), + } as any; + + configStorage = new Map() as any; + + generator = new TemplatedPodGenerator(storeFactory, variableHandler, configStorage, configTemplatePath); + }); + + it('only supports settings with a template.', async(): Promise => { + (settings as any).template = undefined; + await expect(generator.generate(identifier, settings)).rejects.toThrow(BadRequestHttpError); + }); + + it('generates a store and stores relevant variables.', async(): Promise => { + await expect(generator.generate(identifier, settings)).resolves.toBe('store'); + expect(variableHandler.handleSafe).toHaveBeenCalledTimes(1); + expect(variableHandler.handleSafe).toHaveBeenLastCalledWith({ identifier, settings }); + expect(storeFactory.generate).toHaveBeenCalledTimes(1); + expect(storeFactory.generate).toHaveBeenLastCalledWith( + templatePath, TEMPLATE.ResourceStore, { [TEMPLATE_VARIABLE.templateConfig]: templatePath }, + ); + expect(configStorage.get(identifier.path)).toEqual({ [TEMPLATE_VARIABLE.templateConfig]: templatePath }); + }); + + it('rejects identifiers that already have a config.', async(): Promise => { + await configStorage.set(identifier.path, {}); + await expect(generator.generate(identifier, settings)).rejects.toThrow(ConflictHttpError); + }); + + it('rejects invalid config template names.', async(): Promise => { + settings.template = '../../secret-file.json'; + await expect(generator.generate(identifier, settings)).rejects.toThrow(BadRequestHttpError); + }); + + it('only stores relevant variables from an agent object.', async(): Promise => { + settings[TEMPLATE_VARIABLE.rootFilePath] = 'correctFilePath'; + settings.login = 'should not be stored'; + await expect(generator.generate(identifier, settings)).resolves.toBe('store'); + expect(configStorage.get(identifier.path)).toEqual({ + [TEMPLATE_VARIABLE.templateConfig]: templatePath, + [TEMPLATE_VARIABLE.rootFilePath]: 'correctFilePath', + }); + }); + + it('uses a default template folder if none is provided.', async(): Promise => { + generator = new TemplatedPodGenerator(storeFactory, variableHandler, configStorage); + const defaultPath = joinFilePath(__dirname, '../../../../config/templates/', template); + + await expect(generator.generate(identifier, settings)).resolves.toBe('store'); + expect(storeFactory.generate) + .toHaveBeenLastCalledWith(defaultPath, TEMPLATE.ResourceStore, { + [TEMPLATE_VARIABLE.templateConfig]: defaultPath, + }); + }); +}); diff --git a/test/unit/pods/generate/variables/BaseUrlHandler.test.ts b/test/unit/pods/generate/variables/BaseUrlHandler.test.ts new file mode 100644 index 000000000..4ea09efeb --- /dev/null +++ b/test/unit/pods/generate/variables/BaseUrlHandler.test.ts @@ -0,0 +1,14 @@ +import { BaseUrlHandler } from '../../../../../src/pods/generate/variables/BaseUrlHandler'; +import { TEMPLATE_VARIABLE } from '../../../../../src/pods/generate/variables/Variables'; +import type { PodSettings } from '../../../../../src/pods/settings/PodSettings'; + +describe('A BaseUrlHandler', (): void => { + const handler = new BaseUrlHandler(); + + it('adds the identifier as base URL variable.', async(): Promise => { + const identifier = { path: 'http://test.com/foo' }; + const settings = {} as PodSettings; + await expect(handler.handle({ identifier, settings })).resolves.toBeUndefined(); + expect(settings[TEMPLATE_VARIABLE.baseUrl]).toBe(identifier.path); + }); +}); diff --git a/test/unit/pods/generate/variables/RootFilePathHandler.test.ts b/test/unit/pods/generate/variables/RootFilePathHandler.test.ts new file mode 100644 index 000000000..94645698f --- /dev/null +++ b/test/unit/pods/generate/variables/RootFilePathHandler.test.ts @@ -0,0 +1,45 @@ +import fs from 'fs'; +import { RootFilePathHandler } from '../../../../../src/pods/generate/variables/RootFilePathHandler'; +import { TEMPLATE_VARIABLE } from '../../../../../src/pods/generate/variables/Variables'; +import type { PodSettings } from '../../../../../src/pods/settings/PodSettings'; +import type { ResourceLink } from '../../../../../src/storage/mapping/FileIdentifierMapper'; +import { ConflictHttpError } from '../../../../../src/util/errors/ConflictHttpError'; +import { joinFilePath } from '../../../../../src/util/PathUtil'; + +jest.mock('fs'); + +describe('A RootFilePathHandler', (): void => { + const rootFilePath = 'files/'; + const baseUrl = 'http://test.com/'; + let handler: RootFilePathHandler; + const identifier = { path: 'http://test.com/alice/' }; + let settings: PodSettings; + let fsPromises: Record; + + beforeEach(async(): Promise => { + settings = {} as any; + + handler = new RootFilePathHandler({ + mapUrlToFilePath: async(id): Promise => ({ + identifier: id, + filePath: joinFilePath(rootFilePath, id.path.slice(baseUrl.length)), + }), + mapFilePathToUrl: jest.fn(), + }); + + fs.promises = { + access: jest.fn(), + } as any; + fsPromises = fs.promises as any; + }); + + it('errors if the target folder already exists.', async(): Promise => { + await expect(handler.handle({ identifier, settings })).rejects.toThrow(ConflictHttpError); + }); + + it('adds the new file path as variable.', async(): Promise => { + fsPromises.access.mockRejectedValue({ code: 'ENOENT', syscall: 'access' }); + await expect(handler.handle({ identifier, settings })).resolves.toBeUndefined(); + expect(settings[TEMPLATE_VARIABLE.rootFilePath]).toBe(joinFilePath(rootFilePath, 'alice/')); + }); +}); diff --git a/test/unit/pods/generate/variables/VariableSetter.test.ts b/test/unit/pods/generate/variables/VariableSetter.test.ts new file mode 100644 index 000000000..d3ef0c6d2 --- /dev/null +++ b/test/unit/pods/generate/variables/VariableSetter.test.ts @@ -0,0 +1,33 @@ +import { VariableSetter } from '../../../../../src/pods/generate/variables/VariableSetter'; +import type { PodSettings } from '../../../../../src/pods/settings/PodSettings'; + +describe('A VariableSetter', (): void => { + const variable = 'variable'; + const value = 'http://test.com/sparql'; + let settings: PodSettings; + let handler: VariableSetter; + + beforeEach(async(): Promise => { + settings = {} as any; + }); + + it('does nothing if there already is a sparql endpoint value and override is false.', async(): Promise => { + handler = new VariableSetter(variable, value); + settings[variable] = 'sparql-endpoint'; + await expect(handler.handle({ settings })).resolves.toBeUndefined(); + expect(settings[variable]).toBe('sparql-endpoint'); + }); + + it('adds adds the value to the variable if there is none.', async(): Promise => { + handler = new VariableSetter(variable, value); + await expect(handler.handle({ settings })).resolves.toBeUndefined(); + expect(settings[variable]).toBe(value); + }); + + it('always sets the value if override is true.', async(): Promise => { + handler = new VariableSetter(variable, value, true); + settings[variable] = 'sparql-endpoint'; + await expect(handler.handle({ settings })).resolves.toBeUndefined(); + expect(settings[variable]).toBe(value); + }); +});