diff --git a/lib/extension/externalExtension.js b/lib/extension/externalExtension.js index cb10a1e09..76512dced 100644 --- a/lib/extension/externalExtension.js +++ b/lib/extension/externalExtension.js @@ -6,7 +6,7 @@ const data = require('./../util/data'); const path = require('path'); const logger = require('./../util/logger'); const stringify = require('json-stable-stringify-without-jsonify'); -const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/extension/(.*)`); +const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/extension/(save|remove)`); class ExternalExtension extends Extension { constructor(zigbee, mqtt, state, publishEntityState, eventBus, addExtension, enableDisableExtension) { @@ -18,6 +18,7 @@ class ExternalExtension extends Extension { this.loadExtension = this.loadExtension.bind(this); this.requestLookup = { 'save': this.saveExtension.bind(this), + 'remove': this.removeExtension.bind(this), }; this.loadUserDefinedExtensions(); } @@ -36,7 +37,25 @@ class ExternalExtension extends Extension { return []; } } - saveExtension({name, code}) { + removeExtension(message) { + const {name} = message; + const extensions = this.getListOfUserDefinedExtensions(); + const extensionToBeRemoved = extensions.find((e) => e.name === name); + + if (extensionToBeRemoved) { + this.enableDisableExtension(false, extensionToBeRemoved.name); + const basePath = this.getExtensionsBasePath(); + const extensonFilePath = path.join(basePath, path.basename(name)); + fs.unlinkSync(extensonFilePath); + this.publishExtensions(); + logger.info(`Extension ${name} removed`); + return utils.getResponse(message, {}, null); + } else { + return utils.getResponse(message, {}, `Extension ${name} doesn't exists`); + } + } + saveExtension(message) { + const {name, code} = message; const ModuleConstructor = utils.loadModuleFromText(code); this.loadExtension(ModuleConstructor); const basePath = this.getExtensionsBasePath(); @@ -44,10 +63,11 @@ class ExternalExtension extends Extension { if (!fs.existsSync(basePath)) { fs.mkdirSync(basePath); } - const extensonFilePath = path.join(basePath, name); + const extensonFilePath = path.join(basePath, path.basename(name)); fs.writeFileSync(extensonFilePath, code); this.publishExtensions(); - return utils.getResponse(`Extension ${name} loaded`, {}, null); + logger.info(`Extension ${name} loaded`); + return utils.getResponse(message, {}, null); } diff --git a/test/externalExtension.test.js b/test/externalExtension.test.js index 7884e321f..92a49b432 100644 --- a/test/externalExtension.test.js +++ b/test/externalExtension.test.js @@ -16,6 +16,7 @@ const mocksClear = [ const fs = require('fs'); const mkdirSyncSpy = jest.spyOn(fs, 'mkdirSync'); +const unlinkSyncSpy = jest.spyOn(fs, 'unlinkSync'); describe('User extensions', () => { let controller; @@ -73,4 +74,29 @@ describe('User extensions', () => { expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/response/extension/save', stringify({"data":{},"error":"Unexpected identifier","status":"error"}), { retain: false, qos: 0 }, expect.any(Function)); }); + + it('Removes user extension', async () => { + const extensionPath = path.join(data.mockDir, 'extension'); + const extensionCode = fs.readFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), 'utf-8'); + fs.mkdirSync(extensionPath); + const extensionFilePath = path.join(extensionPath, 'exampleExtension.js') + fs.copyFileSync(path.join(__dirname, 'assets', 'exampleExtension.js'), extensionFilePath) + controller = new Controller(jest.fn(), jest.fn()); + await controller.start(); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'test', { retain: false, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([{"name": "exampleExtension.js", "code": extensionCode}]), { retain: true, qos: 0 }, expect.any(Function)); + + MQTT.events.message('zigbee2mqtt/bridge/request/extension/remove', stringify({"name": "exampleExtension.js"})); + await flushPromises(); + expect(unlinkSyncSpy).toHaveBeenCalledWith(extensionFilePath); + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/extension/remove', stringify({"name": "non existing.js"})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/extension/remove', + stringify({"data":{},"status":"error","error":"Extension non existing.js doesn't exists"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); });