mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
feat(xo6): add page plugins (#10593)
* feat(xo6): add page plugins * improve error summary component * plugin configuration form review * fix(xo6): adapt plugins to master after rebase - FrontXoPlugin keeps the plugin types of @vates/types (strict union since #10588) - tests follow the state checks, task params and loading state of the master backend * chore(xo6): add plugins page to the changelog * fix(xo6): pluralize the examples count and unprefix generic plugin i18n keys * fix(xo6): drop JSDoc from the json-schema form * test(xo6): align the plugin tests with the existing test patterns Component tests mock the plugin jobs and assert their run and arguments instead of the REST requests, job tests mock fetch.util directly with run/validate blocks, composable tests use a local mockJob, and createPlugin gets the factory JSDoc. * refactor(xo6): name the json-schema form conditions and share the object schema guard * refactor(xo6): expose the summary errors from the json-schema form * refactor(xo6): name the plugin save and test steps * fix(xo6): use the delete action icon in the plugin delete buttons * revert(xo6): drop the capitalize prop of UiToggle * refactor(xo6): remove the go-to-first-error button of the plugin configuration * refactor(xo6): use VtsDeleteButton in the plugin configuration deletion * revert(web-core): keep useValidatedForm and useFormSelect as on master The plugin forms focus their first missing field through useJsonSchemaForm, so the error summary and focus-on-validate of useValidatedForm had no consumer and changed the behaviour of the existing forms. * feat(web-core): use the puzzle icon for plugins and the window icon for templates * feat(xo6): plugin load and startup actions in the plugins table, panel and header - Side panel: Load/Unload button + "Enable/Disable load at startup" in more actions, loaded / load at startup as statuses, copiable version and description, no bottom link button - Table: action column (select + quick actions) replaces the select column - Detail head bar: same actions, PluginStateToggles removed - One plugin.change-state job replaces the load and unload jobs; updatePluginAutoload and isPluginLoaded move to xo-plugin.util.ts - useXoPluginUtils no longer exposes description - Shared test helpers: createJobResult, createActionItem, findTooltipContent * feat(xo6): read-only plugin detail page, configuration and test in drawers - Detail page: general info + schema-driven read-only configuration card (VtsColumns) - Configure: primary action in head bar, side panel and table row, opens a drawer - Test: drawer from the more-actions menus; delete configuration in the same menus - Errors follow the app pattern: the drawer stays open on job failure - Delete configuration is blocked while the plugin is loaded (the API rejects it) - Secrets: masked by xo-server's name rule or by the value obfuscated by the API - web-core: VtsActionItems extracted from VtsActionsMenu for menu-slot reuse * feat(xo6): show the plugin Configure action first, next to Load * refactor(xo6): remove the unused ApiErrorAlert, useActionError and api-error utils * refactor(xo6): de-duplicate the plugin drawer composables and test fixtures - one useXoPluginDrawerJob replaces the near-identical configure and test composables - useXoPluginUtils exposes the description and loaded state used by the templates - shared plugin schema fixture and JSON schema item-label test helpers * refactor(xo6): de-duplicate the JSON schema fields and the plugin tests - JsonSchemaInputWrapper holds the input wrapper, message and validation of the input, select and XO object fields - JsonSchemaField wraps the object and array fields in their fieldset, which owns the optional value - shared test helpers for drawers, plugin actions, titles and JSON schema forms * refactor(xo6): reuse @vates/obfuscate and cover the JSON schema select fields - @vates/obfuscate exports isSensitiveParam and ships its types; XO 6 imports it and OBFUSCATED_VALUE instead of copies - tests for the select, XO object field and useXoObjectOptions; drop the select guard that could never run - JsonSchemaValue titles its sections in a computed and checks the secret mask * feat(xo6): show the arrays of a plugin configuration as a list * refactor(xo6): share the plugin route and the item label type, remove the unused required key * docs(lite): add a story for VtsActionItems * feat(xo6): validate the numeric limits, integers and list sizes of the plugin forms The JSON schema form only checked the required fields, so a value under minimum, over maximum, a decimal number for an integer or a list shorter than minItems was only rejected by the server. oneOf is still left to the server. * test(xo6): share the drawer, button and tag test helpers, test the preset application Replaces the local submit, button and tag lookups of the backup repository, plugin, host and VM tests with the src/test helpers, makes PluginMoreActions assert its wiring instead of its mock, and documents the new helpers. * test(xo6): test the plugin form lists, sections, messages and links, extract the property grouping * fix(xo6): do not test a plugin while it is being loaded or unloaded * refactor(xo6): give fetchPut and fetchPatch no default type, like fetchPost * refactor(xo6): identify a missing plugin in the plugin job argument instead of in defineJob * refactor(xo6): remove the unused disabled prop from the JSON schema form * refactor(xo6): detect the plugin secrets in the front only, leave @vates/obfuscate as on master - the secret property names and the obfuscated value are local to XO 6 again, no @vates/obfuscate export, types or release - the backup repository and plugin tests import the obfuscated value from the shared constants * refactor(xo6): use checkboxes instead of toggles in the plugin forms * test(xo6): get a checkbox by its label with a shared test helper * test(xo6): reset the state of the array items when the list is replaced from the outside * refactor(xo6): split the JSON schema form dir into the plugin components, utils, composables and form dirs --------- Co-authored-by: Olivier Floch <olivier.floch@vates.fr>
This commit is contained in:
443
packages/xo-server/src/xo-mixins/_plugins.test.mjs
Normal file
443
packages/xo-server/src/xo-mixins/_plugins.test.mjs
Normal file
@@ -0,0 +1,443 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { describe, it } from 'node:test'
|
||||
import { configure } from '@xen-orchestra/log/configure'
|
||||
import { invalidParameters, noSuchObject } from 'xo-common/api-errors.js'
|
||||
import { OBFUSCATED_VALUE } from '@vates/obfuscate'
|
||||
|
||||
import Plugins from './plugins.mjs'
|
||||
|
||||
configure({ level: 'FATAL', transport: () => {} })
|
||||
|
||||
// an empty configuration is invalid
|
||||
const REQUIRED_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: { host: { type: 'string' }, password: { type: 'string' } },
|
||||
required: ['host'],
|
||||
}
|
||||
|
||||
// an empty configuration is valid
|
||||
const OPTIONAL_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: { host: { type: 'string' } },
|
||||
}
|
||||
|
||||
const createInstance = ({ testable = false, unloadable = true } = {}) => {
|
||||
const calls = []
|
||||
const instance = {
|
||||
configure: async (configuration, context) => calls.push(['configure', configuration, context]),
|
||||
load: async () => calls.push(['load']),
|
||||
}
|
||||
if (testable) {
|
||||
instance.test = async data => calls.push(['test', data])
|
||||
}
|
||||
if (unloadable) {
|
||||
instance.unload = async () => calls.push(['unload'])
|
||||
}
|
||||
return { calls, instance }
|
||||
}
|
||||
|
||||
// `_pluginsMetadata` is normally created on the `core started` hook, backed by Redis
|
||||
const createPlugins = (initialMetadata = {}) => {
|
||||
const touched = []
|
||||
const app = {
|
||||
hooks: new EventEmitter(),
|
||||
touchXoObject: async (type, id) => {
|
||||
assert.equal(type, 'plugin')
|
||||
touched.push(id)
|
||||
},
|
||||
}
|
||||
const plugins = new Plugins(app)
|
||||
|
||||
const stored = { __proto__: null }
|
||||
for (const [id, metadata] of Object.entries(initialMetadata)) {
|
||||
stored[id] = { id, ...metadata }
|
||||
}
|
||||
|
||||
plugins._pluginsMetadata = {
|
||||
first: async id => (stored[id] === undefined ? undefined : { ...stored[id] }),
|
||||
update: async metadata => {
|
||||
stored[metadata.id] = { ...metadata }
|
||||
},
|
||||
merge: async (id, data) => {
|
||||
if (stored[id] === undefined) {
|
||||
throw new Error('no such plugin metadata')
|
||||
}
|
||||
stored[id] = { ...stored[id], ...data }
|
||||
},
|
||||
}
|
||||
|
||||
return { app, plugins, stored, touched }
|
||||
}
|
||||
|
||||
const register = (plugins, name, { configurationSchema, instance, testSchema } = {}) =>
|
||||
plugins.registerPlugin(
|
||||
name,
|
||||
instance,
|
||||
configurationSchema,
|
||||
undefined,
|
||||
'description',
|
||||
['keyword'],
|
||||
testSchema,
|
||||
'1.0.0'
|
||||
)
|
||||
|
||||
const isInvalidParameters = error => invalidParameters.is(error)
|
||||
|
||||
describe('Plugins', () => {
|
||||
describe('registerPlugin()', () => {
|
||||
it('loads a non configurable plugin without configuring it', async () => {
|
||||
const { plugins, stored } = createPlugins()
|
||||
const { calls, instance } = createInstance()
|
||||
|
||||
await register(plugins, 'foo', { instance })
|
||||
|
||||
const plugin = await plugins.getPlugin('foo')
|
||||
assert.equal(plugin.configured, true)
|
||||
assert.equal(plugin.loaded, true)
|
||||
assert.deepEqual(
|
||||
calls.map(([name]) => name),
|
||||
['load']
|
||||
)
|
||||
// first registration: the metadata are created with autoload enabled
|
||||
assert.equal(stored.foo.autoload, true)
|
||||
})
|
||||
|
||||
it('configures with an empty configuration when it is valid', async () => {
|
||||
const { plugins } = createPlugins()
|
||||
const { calls, instance } = createInstance()
|
||||
|
||||
await register(plugins, 'foo', { configurationSchema: OPTIONAL_SCHEMA, instance })
|
||||
|
||||
const plugin = await plugins.getPlugin('foo')
|
||||
assert.equal(plugin.configured, true)
|
||||
assert.equal(plugin.loaded, true)
|
||||
assert.equal(plugin.configuration, undefined)
|
||||
assert.deepEqual(calls[0], ['configure', {}, { loaded: undefined }])
|
||||
})
|
||||
|
||||
it('neither configures nor loads when the empty configuration is invalid', async () => {
|
||||
const { plugins } = createPlugins()
|
||||
const { calls, instance } = createInstance()
|
||||
|
||||
await register(plugins, 'foo', { configurationSchema: REQUIRED_SCHEMA, instance })
|
||||
|
||||
const plugin = await plugins.getPlugin('foo')
|
||||
assert.equal(plugin.configured, false)
|
||||
assert.equal(plugin.loaded, undefined)
|
||||
assert.deepEqual(calls, [])
|
||||
})
|
||||
|
||||
it('configures with the saved configuration', async () => {
|
||||
const { plugins } = createPlugins({ foo: { autoload: true, configuration: { host: 'example.org' } } })
|
||||
const { calls, instance } = createInstance()
|
||||
|
||||
await register(plugins, 'foo', { configurationSchema: REQUIRED_SCHEMA, instance })
|
||||
|
||||
assert.equal((await plugins.getPlugin('foo')).configured, true)
|
||||
assert.deepEqual(calls[0], ['configure', { host: 'example.org' }, { loaded: undefined }])
|
||||
})
|
||||
|
||||
it('throws when the saved configuration is invalid', async () => {
|
||||
const { plugins } = createPlugins({ foo: { autoload: true, configuration: { host: 42 } } })
|
||||
const { instance } = createInstance()
|
||||
|
||||
await assert.rejects(
|
||||
register(plugins, 'foo', { configurationSchema: REQUIRED_SCHEMA, instance }),
|
||||
isInvalidParameters
|
||||
)
|
||||
})
|
||||
|
||||
it('does not load when autoload is disabled', async () => {
|
||||
const { plugins } = createPlugins({ foo: { autoload: false } })
|
||||
const { calls, instance } = createInstance()
|
||||
|
||||
await register(plugins, 'foo', { instance })
|
||||
|
||||
const plugin = await plugins.getPlugin('foo')
|
||||
assert.equal(plugin.autoload, false)
|
||||
assert.equal(plugin.loaded, undefined)
|
||||
assert.deepEqual(calls, [])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPlugin()', () => {
|
||||
it('exposes the plugin with an obfuscated configuration', async () => {
|
||||
const { plugins } = createPlugins({
|
||||
foo: { autoload: false, configuration: { host: 'example.org', password: 'secret' } },
|
||||
})
|
||||
const { instance } = createInstance({ testable: true })
|
||||
|
||||
await register(plugins, 'foo', { configurationSchema: REQUIRED_SCHEMA, instance, testSchema: OPTIONAL_SCHEMA })
|
||||
|
||||
assert.deepEqual(await plugins.getPlugin('foo'), {
|
||||
id: 'foo',
|
||||
name: 'foo',
|
||||
autoload: false,
|
||||
description: 'description',
|
||||
loaded: undefined,
|
||||
loading: undefined,
|
||||
keywords: ['keyword'],
|
||||
unloadable: true,
|
||||
version: '1.0.0',
|
||||
configuration: { host: 'example.org', password: OBFUSCATED_VALUE },
|
||||
configurationPresets: undefined,
|
||||
configurationSchema: REQUIRED_SCHEMA,
|
||||
configured: true,
|
||||
testable: true,
|
||||
testSchema: OPTIONAL_SCHEMA,
|
||||
})
|
||||
})
|
||||
|
||||
it('throws on an unknown plugin', async () => {
|
||||
const { plugins } = createPlugins()
|
||||
|
||||
await assert.rejects(plugins.getPlugin('foo'), error => noSuchObject.is(error, { id: 'foo', type: 'plugin' }))
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadPlugin()', () => {
|
||||
it('refuses to load a plugin which is not configured', async () => {
|
||||
const { plugins } = createPlugins({ foo: { autoload: false } })
|
||||
const { instance } = createInstance()
|
||||
await register(plugins, 'foo', { configurationSchema: REQUIRED_SCHEMA, instance })
|
||||
|
||||
await assert.rejects(plugins.loadPlugin('foo'), isInvalidParameters)
|
||||
})
|
||||
|
||||
it('refuses to load a plugin which is already loaded', async () => {
|
||||
const { plugins } = createPlugins()
|
||||
const { instance } = createInstance()
|
||||
await register(plugins, 'foo', { instance })
|
||||
|
||||
await assert.rejects(plugins.loadPlugin('foo'), isInvalidParameters)
|
||||
})
|
||||
|
||||
it('loads a configured plugin', async () => {
|
||||
const { plugins } = createPlugins({ foo: { autoload: false } })
|
||||
const { calls, instance } = createInstance()
|
||||
await register(plugins, 'foo', { instance })
|
||||
|
||||
await plugins.loadPlugin('foo')
|
||||
|
||||
assert.equal((await plugins.getPlugin('foo')).loaded, true)
|
||||
assert.deepEqual(calls, [['load']])
|
||||
})
|
||||
})
|
||||
|
||||
describe('unloadPlugin()', () => {
|
||||
it('refuses to unload a plugin which cannot be unloaded', async () => {
|
||||
const { plugins } = createPlugins()
|
||||
const { instance } = createInstance({ unloadable: false })
|
||||
await register(plugins, 'foo', { instance })
|
||||
|
||||
await assert.rejects(plugins.unloadPlugin('foo'), isInvalidParameters)
|
||||
})
|
||||
})
|
||||
|
||||
describe('configurePlugin()', () => {
|
||||
it('validates, applies and saves the configuration', async () => {
|
||||
const { plugins, stored } = createPlugins({ foo: { autoload: false } })
|
||||
const { calls, instance } = createInstance()
|
||||
await register(plugins, 'foo', { configurationSchema: REQUIRED_SCHEMA, instance })
|
||||
|
||||
await plugins.configurePlugin('foo', { host: 'example.org' })
|
||||
|
||||
assert.equal((await plugins.getPlugin('foo')).configured, true)
|
||||
assert.deepEqual(stored.foo.configuration, { host: 'example.org' })
|
||||
assert.deepEqual(calls.at(-1), ['configure', { host: 'example.org' }, { loaded: undefined }])
|
||||
})
|
||||
|
||||
it('neither applies nor saves an invalid configuration', async () => {
|
||||
const { plugins, stored } = createPlugins({ foo: { autoload: false } })
|
||||
const { calls, instance } = createInstance()
|
||||
await register(plugins, 'foo', { configurationSchema: REQUIRED_SCHEMA, instance })
|
||||
|
||||
await assert.rejects(plugins.configurePlugin('foo', {}), isInvalidParameters)
|
||||
|
||||
assert.equal(stored.foo.configuration, undefined)
|
||||
assert.deepEqual(calls, [])
|
||||
})
|
||||
|
||||
it('keeps the saved values of the obfuscated ones', async () => {
|
||||
const { plugins, stored } = createPlugins({
|
||||
foo: { autoload: false, configuration: { host: 'example.org', password: 'secret' } },
|
||||
})
|
||||
const { instance } = createInstance()
|
||||
await register(plugins, 'foo', { configurationSchema: REQUIRED_SCHEMA, instance })
|
||||
|
||||
await plugins.configurePlugin('foo', { host: 'example.net', password: OBFUSCATED_VALUE })
|
||||
|
||||
assert.deepEqual(stored.foo.configuration, { host: 'example.net', password: 'secret' })
|
||||
})
|
||||
|
||||
it('merges with the saved configuration when asked', async () => {
|
||||
const { plugins, stored } = createPlugins({
|
||||
foo: { autoload: false, configuration: { host: 'example.org', password: 'secret' } },
|
||||
})
|
||||
const { instance } = createInstance()
|
||||
await register(plugins, 'foo', { configurationSchema: REQUIRED_SCHEMA, instance })
|
||||
|
||||
await plugins.configurePlugin('foo', { host: 'example.net' }, true)
|
||||
|
||||
assert.deepEqual(stored.foo.configuration, { host: 'example.net', password: 'secret' })
|
||||
})
|
||||
|
||||
it('refuses to configure a non configurable plugin', async () => {
|
||||
const { plugins } = createPlugins({ foo: { autoload: false } })
|
||||
const { instance } = createInstance()
|
||||
await register(plugins, 'foo', { instance })
|
||||
|
||||
await assert.rejects(plugins.configurePlugin('foo', {}), isInvalidParameters)
|
||||
})
|
||||
})
|
||||
|
||||
describe('purgePluginConfiguration()', () => {
|
||||
it('unloads the plugin, removes its configuration and makes it not configured', async () => {
|
||||
const { plugins, stored } = createPlugins({ foo: { autoload: true, configuration: { host: 'example.org' } } })
|
||||
const { calls, instance } = createInstance()
|
||||
await register(plugins, 'foo', { configurationSchema: REQUIRED_SCHEMA, instance })
|
||||
|
||||
await plugins.purgePluginConfiguration('foo')
|
||||
|
||||
const plugin = await plugins.getPlugin('foo')
|
||||
assert.equal(plugin.configured, false)
|
||||
assert.equal(plugin.loaded, false)
|
||||
assert.equal(stored.foo.configuration, undefined)
|
||||
assert.deepEqual(calls.at(-1), ['unload'])
|
||||
await assert.rejects(plugins.loadPlugin('foo'), isInvalidParameters)
|
||||
})
|
||||
|
||||
it('keeps the plugin configured when an empty configuration is valid', async () => {
|
||||
const { plugins } = createPlugins({ foo: { autoload: false, configuration: { host: 'example.org' } } })
|
||||
const { calls, instance } = createInstance()
|
||||
await register(plugins, 'foo', { configurationSchema: OPTIONAL_SCHEMA, instance })
|
||||
|
||||
await plugins.purgePluginConfiguration('foo')
|
||||
|
||||
assert.equal((await plugins.getPlugin('foo')).configured, true)
|
||||
// the instance no longer uses the deleted configuration
|
||||
assert.deepEqual(calls.at(-1), ['configure', {}, { loaded: undefined }])
|
||||
})
|
||||
|
||||
it('keeps a non configurable plugin configured', async () => {
|
||||
const { plugins } = createPlugins()
|
||||
const { instance } = createInstance()
|
||||
await register(plugins, 'foo', { instance })
|
||||
|
||||
await plugins.purgePluginConfiguration('foo')
|
||||
|
||||
assert.equal((await plugins.getPlugin('foo')).configured, true)
|
||||
})
|
||||
|
||||
it('throws on an unknown plugin', async () => {
|
||||
const { plugins } = createPlugins()
|
||||
|
||||
await assert.rejects(plugins.purgePluginConfiguration('foo'), error =>
|
||||
noSuchObject.is(error, { id: 'foo', type: 'plugin' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('testPlugin()', () => {
|
||||
it('refuses to test a plugin which is not testable', async () => {
|
||||
const { plugins } = createPlugins()
|
||||
const { instance } = createInstance()
|
||||
await register(plugins, 'foo', { instance })
|
||||
|
||||
await assert.rejects(plugins.testPlugin('foo', {}), isInvalidParameters)
|
||||
})
|
||||
|
||||
it('refuses to test a plugin which is not loaded', async () => {
|
||||
const { plugins } = createPlugins({ foo: { autoload: false } })
|
||||
const { instance } = createInstance({ testable: true })
|
||||
await register(plugins, 'foo', { instance })
|
||||
|
||||
await assert.rejects(plugins.testPlugin('foo', {}), isInvalidParameters)
|
||||
})
|
||||
|
||||
it('refuses data which do not match the test schema', async () => {
|
||||
const { plugins } = createPlugins()
|
||||
const { instance } = createInstance({ testable: true })
|
||||
await register(plugins, 'foo', { instance, testSchema: REQUIRED_SCHEMA })
|
||||
|
||||
await assert.rejects(plugins.testPlugin('foo', undefined), isInvalidParameters)
|
||||
await assert.rejects(plugins.testPlugin('foo', {}), isInvalidParameters)
|
||||
})
|
||||
|
||||
it('tests the plugin with the data', async () => {
|
||||
const { plugins } = createPlugins()
|
||||
const { calls, instance } = createInstance({ testable: true })
|
||||
await register(plugins, 'foo', { instance, testSchema: REQUIRED_SCHEMA })
|
||||
|
||||
await plugins.testPlugin('foo', { host: 'example.org' })
|
||||
|
||||
assert.deepEqual(calls.at(-1), ['test', { host: 'example.org' }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('plugin collection', () => {
|
||||
it('registers the plugins metadata as a collection when the core is started', async () => {
|
||||
const { app, plugins } = createPlugins()
|
||||
const registered = []
|
||||
app.hooks.on('registerCollection', options => registered.push(options))
|
||||
app.addConfigManager = () => {}
|
||||
// just enough of Redis to create the plugins metadata
|
||||
app._redis = { sAdd: async () => {}, get: async () => '20170905' }
|
||||
|
||||
app.hooks.emit('core started')
|
||||
|
||||
assert.equal(registered.length, 1)
|
||||
const { collection, decorate, type } = registered[0]
|
||||
assert.equal(type, 'plugin')
|
||||
assert.equal(collection, plugins._pluginsMetadata)
|
||||
|
||||
// the plugins metadata are replaced by the ones of the test to register a plugin
|
||||
plugins._pluginsMetadata = createPlugins().plugins._pluginsMetadata
|
||||
await register(plugins, 'foo', { instance: createInstance().instance })
|
||||
|
||||
assert.equal((await decorate({ id: 'foo' })).id, 'foo')
|
||||
// metadata of plugins that are not registered are ignored
|
||||
assert.equal(await decorate({ id: 'unknown' }), undefined)
|
||||
})
|
||||
|
||||
it('signals a registered plugin, even when it could not be configured', async () => {
|
||||
const { plugins, touched } = createPlugins()
|
||||
|
||||
await register(plugins, 'foo', { instance: createInstance().instance, configurationSchema: REQUIRED_SCHEMA })
|
||||
|
||||
assert.deepEqual(touched, ['foo'])
|
||||
})
|
||||
|
||||
// changes of the metadata (configuration, autoload) are signaled by the collection itself
|
||||
it('signals each change of the plugin state which is not stored in its metadata', async () => {
|
||||
const { plugins, touched } = createPlugins({ foo: { autoload: false } })
|
||||
await register(plugins, 'foo', { instance: createInstance().instance, configurationSchema: REQUIRED_SCHEMA })
|
||||
await plugins.configurePlugin('foo', { host: 'example.org' })
|
||||
|
||||
touched.length = 0
|
||||
await plugins.loadPlugin('foo')
|
||||
// once when the loading starts, once when it ends
|
||||
assert.deepEqual(touched, ['foo', 'foo'], 'load')
|
||||
|
||||
touched.length = 0
|
||||
await plugins.unloadPlugin('foo')
|
||||
assert.deepEqual(touched, ['foo'], 'unload')
|
||||
|
||||
touched.length = 0
|
||||
await plugins.purgePluginConfiguration('foo')
|
||||
assert.deepEqual(touched, ['foo'], 'purge')
|
||||
})
|
||||
|
||||
it('does not signal a failed change', async () => {
|
||||
const { plugins, touched } = createPlugins({ foo: { autoload: false } })
|
||||
await register(plugins, 'foo', { instance: createInstance().instance, configurationSchema: REQUIRED_SCHEMA })
|
||||
touched.length = 0
|
||||
|
||||
await assert.rejects(plugins.configurePlugin('foo', {}), isInvalidParameters)
|
||||
await assert.rejects(plugins.loadPlugin('foo'), isInvalidParameters)
|
||||
|
||||
assert.deepEqual(touched, [])
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user