UNPKG

@mcm4iob/testing

Version:

Shared utilities for adapter and module testing in ioBroker

532 lines (531 loc) 19.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createAdapterMock = void 0; const objects_1 = require("alcalzone-shared/objects"); const sinon_1 = require("sinon"); const mockLogger_1 = require("./mockLogger"); const tools_1 = require("./tools"); // Define here which methods were implemented manually, so we can hook them up with a real stub // The value describes if and how the async version of the callback is constructed const implementedMethods = { getObject: "normal", setObject: "normal", setObjectNotExists: "normal", extendObject: "normal", getForeignObject: "normal", getForeignObjects: "normal", setForeignObject: "normal", setForeignObjectNotExists: "normal", extendForeignObject: "normal", getState: "normal", getStates: "normal", setState: "normal", setStateChanged: "normal", delState: "normal", getForeignState: "normal", setForeignState: "normal", setForeignStateChanged: "normal", subscribeStates: "normal", subscribeForeignStates: "normal", subscribeObjects: "normal", subscribeForeignObjects: "normal", getAdapterObjects: "no error", getObjectView: "normal", getObjectList: "normal", on: "none", removeListener: "none", removeAllListeners: "none", terminate: "none", getPort: "no error", checkPassword: "no error", setPassword: "normal", checkGroup: "no error", calculatePermissions: "no error", getCertificates: "normal", sendTo: "no error", sendToHost: "no error", getHistory: "normal", setBinaryState: "normal", getBinaryState: "normal", getEnum: "normal", getEnums: "normal", addChannelToEnum: "normal", deleteChannelFromEnum: "normal", addStateToEnum: "normal", deleteStateFromEnum: "normal", createDevice: "normal", deleteDevice: "normal", createChannel: "normal", deleteChannel: "normal", createState: "normal", deleteState: "normal", getDevices: "normal", getChannelsOf: "normal", getStatesOf: "normal", readDir: "normal", mkDir: "normal", readFile: "normal", writeFile: "normal", delFile: "normal", unlink: "normal", rename: "normal", chmodFile: "normal", }; function getCallback(...args) { const lastArg = args[args.length - 1]; if (typeof lastArg === "function") return lastArg; } /** Stub implementation which can be promisified */ const asyncEnabledStub = ((...args) => { const callback = getCallback(...args); if (typeof callback === "function") callback(); }); /** * Creates an adapter mock that is connected to a given database mock */ function createAdapterMock(db, options = {}) { // In order to support ES6-style adapters with inheritance, we need to work on the instance directly const ret = this || {}; Object.assign(ret, { name: options.name || "test", host: "testhost", instance: options.instance || 0, namespace: `${options.name || "test"}.${options.instance || 0}`, config: options.config || {}, common: {}, systemConfig: null, adapterDir: "", ioPack: {}, pack: {}, log: (0, mockLogger_1.createLoggerMock)(), version: "any", connected: true, getPort: asyncEnabledStub, stop: (0, sinon_1.stub)(), checkPassword: asyncEnabledStub, setPassword: asyncEnabledStub, checkGroup: asyncEnabledStub, calculatePermissions: asyncEnabledStub, getCertificates: asyncEnabledStub, sendTo: asyncEnabledStub, sendToHost: asyncEnabledStub, idToDCS: (0, sinon_1.stub)(), getObject: ((id, ...args) => { if (!id.startsWith(ret.namespace)) id = ret.namespace + "." + id; const callback = getCallback(...args); if (callback) callback(null, db.getObject(id)); }), setObject: ((id, obj, ...args) => { if (!id.startsWith(ret.namespace)) id = ret.namespace + "." + id; obj._id = id; db.publishObject(obj); const callback = getCallback(...args); if (callback) callback(null, { id }); }), setObjectNotExists: ((id, obj, ...args) => { if (!id.startsWith(ret.namespace)) id = ret.namespace + "." + id; const callback = getCallback(...args); if (db.hasObject(id)) { if (callback) callback(null, { id }); } else { ret.setObject(id, obj, callback); } }), getAdapterObjects: ((callback) => { callback(db.getObjects(`${ret.namespace}.*`)); }), getObjectView: ((design, search, { startkey, endkey }, ...args) => { if (design !== "system") { throw new Error("If you want to use a custom design for getObjectView, you need to mock it yourself!"); } const callback = getCallback(...args); if (typeof callback === "function") { let objects = (0, objects_1.values)(db.getObjects("*")); objects = objects.filter((obj) => obj.type === search); if (startkey) objects = objects.filter((obj) => obj._id >= startkey); if (endkey) objects = objects.filter((obj) => obj._id <= endkey); callback(null, { rows: objects.map((obj) => ({ id: obj._id, value: obj, })), }); } }), getObjectList: (({ startkey, endkey, include_docs, }, ...args) => { const callback = getCallback(...args); if (typeof callback === "function") { let objects = (0, objects_1.values)(db.getObjects("*")); if (startkey) objects = objects.filter((obj) => obj._id >= startkey); if (endkey) objects = objects.filter((obj) => obj._id <= endkey); if (!include_docs) objects = objects.filter((obj) => !obj._id.startsWith("_")); callback(null, { rows: objects.map((obj) => ({ id: obj._id, value: obj, doc: obj, })), }); } }), extendObject: ((id, obj, ...args) => { if (!id.startsWith(ret.namespace)) id = ret.namespace + "." + id; const existing = db.getObject(id) || {}; const target = (0, objects_1.extend)({}, existing, obj); target._id = id; db.publishObject(target); const callback = getCallback(...args); if (callback) callback(null, { id: target._id, value: target }, id); }), delObject: ((id, ...args) => { if (!id.startsWith(ret.namespace)) id = ret.namespace + "." + id; db.deleteObject(id); const callback = getCallback(...args); if (callback) callback(undefined); }), getForeignObject: ((id, ...args) => { const callback = getCallback(...args); if (callback) callback(null, db.getObject(id)); }), getForeignObjects: ((pattern, ...args) => { const type = typeof args[0] === "string" ? args[0] : undefined; const callback = getCallback(...args); if (callback) callback(null, db.getObjects(pattern, type)); }), setForeignObject: ((id, obj, ...args) => { obj._id = id; db.publishObject(obj); const callback = getCallback(...args); if (callback) callback(null, { id }); }), setForeignObjectNotExists: ((id, obj, ...args) => { const callback = getCallback(...args); if (db.hasObject(id)) { if (callback) callback(null, { id }); } else { ret.setObject(id, obj, callback); } }), extendForeignObject: ((id, obj, ...args) => { const target = db.getObject(id) || {}; Object.assign(target, obj); target._id = id; db.publishObject(target); const callback = getCallback(...args); if (callback) callback(null, { id: target._id, value: target }, id); }), findForeignObject: (0, sinon_1.stub)(), delForeignObject: ((id, ...args) => { db.deleteObject(id); const callback = getCallback(...args); if (callback) callback(undefined); }), setState: ((id, state, ...args) => { const callback = getCallback(...args); if (!id.startsWith(ret.namespace)) id = ret.namespace + "." + id; let ack; if (state != null && typeof state === "object") { ack = !!state.ack; state = state.val; } else { ack = typeof args[0] === "boolean" ? args[0] : false; } db.publishState(id, { val: state, ack }); if (callback) callback(null, id); }), setStateChanged: ((id, state, ...args) => { const callback = getCallback(...args); let ack; if (state != null && typeof state === "object") { ack = !!state.ack; state = state.val; } else { ack = typeof args[0] === "boolean" ? args[0] : false; } if (!id.startsWith(ret.namespace)) id = ret.namespace + "." + id; if (!db.hasState(id) || db.getState(id).val !== state) { db.publishState(id, { val: state, ack }); } if (callback) callback(null, id); }), setForeignState: ((id, state, ...args) => { const callback = getCallback(...args); let ack; if (state != null && typeof state === "object") { ack = !!state.ack; state = state.val; } else { ack = typeof args[0] === "boolean" ? args[0] : false; } db.publishState(id, { val: state, ack }); if (callback) callback(null, id); }), setForeignStateChanged: ((id, state, ...args) => { const callback = getCallback(...args); let ack; if (state != null && typeof state === "object") { ack = !!state.ack; state = state.val; } else { ack = typeof args[0] === "boolean" ? args[0] : false; } if (!db.hasState(id) || db.getState(id).val !== state) { db.publishState(id, { val: state, ack }); } if (callback) callback(null, id); }), getState: ((id, ...args) => { if (!id.startsWith(ret.namespace)) id = ret.namespace + "." + id; const callback = getCallback(...args); if (callback) callback(null, db.getState(id)); }), getForeignState: ((id, ...args) => { const callback = getCallback(...args); if (callback) callback(null, db.getState(id)); }), getStates: ((pattern, ...args) => { if (!pattern.startsWith(ret.namespace)) pattern = ret.namespace + "." + pattern; const callback = getCallback(...args); if (callback) callback(null, db.getStates(pattern)); }), getForeignStates: ((pattern, ...args) => { const callback = getCallback(...args); if (callback) callback(null, db.getStates(pattern)); }), delState: ((id, ...args) => { if (!id.startsWith(ret.namespace)) id = ret.namespace + "." + id; db.deleteState(id); const callback = getCallback(...args); if (callback) callback(undefined); }), delForeignState: ((id, ...args) => { db.deleteState(id); const callback = getCallback(...args); if (callback) callback(undefined); }), getHistory: asyncEnabledStub, setBinaryState: asyncEnabledStub, getBinaryState: asyncEnabledStub, getEnum: asyncEnabledStub, getEnums: asyncEnabledStub, addChannelToEnum: asyncEnabledStub, deleteChannelFromEnum: asyncEnabledStub, addStateToEnum: asyncEnabledStub, deleteStateFromEnum: asyncEnabledStub, subscribeObjects: asyncEnabledStub, subscribeForeignObjects: asyncEnabledStub, unsubscribeObjects: asyncEnabledStub, unsubscribeForeignObjects: asyncEnabledStub, subscribeStates: asyncEnabledStub, subscribeForeignStates: asyncEnabledStub, unsubscribeStates: asyncEnabledStub, unsubscribeForeignStates: asyncEnabledStub, createDevice: asyncEnabledStub, deleteDevice: asyncEnabledStub, createChannel: asyncEnabledStub, deleteChannel: asyncEnabledStub, createState: asyncEnabledStub, deleteState: asyncEnabledStub, getDevices: asyncEnabledStub, getChannels: (0, sinon_1.stub)(), getChannelsOf: asyncEnabledStub, getStatesOf: asyncEnabledStub, readDir: asyncEnabledStub, mkDir: asyncEnabledStub, readFile: asyncEnabledStub, writeFile: asyncEnabledStub, delFile: asyncEnabledStub, unlink: asyncEnabledStub, rename: asyncEnabledStub, chmodFile: asyncEnabledStub, formatValue: (0, sinon_1.stub)(), formatDate: (0, sinon_1.stub)(), terminate: ((reason, exitCode) => { if (typeof reason === "number") { // Only the exit code was passed exitCode = reason; reason = undefined; } const errorMessage = `Adapter.terminate was called${typeof exitCode === "number" ? ` (exit code ${exitCode})` : ""}: ${reason ? reason : "Without reason"}`; // Terminates execution by const err = new Error(errorMessage); // @ts-expect-error I'm too lazy to add terminateReason to the error type err.terminateReason = reason || "no reason given!"; throw err; }), supportsFeature: (0, sinon_1.stub)(), getPluginInstance: (0, sinon_1.stub)(), getPluginConfig: (0, sinon_1.stub)(), // EventEmitter methods on: ((event, handler) => { // Remember the event handlers so we can call them on demand switch (event) { case "ready": ret.readyHandler = handler; break; case "message": ret.messageHandler = handler; break; case "objectChange": ret.objectChangeHandler = handler; break; case "stateChange": ret.stateChangeHandler = handler; break; case "unload": ret.unloadHandler = handler; break; } return ret; }), removeListener: ((event, _listener) => { // TODO This is not entirely correct switch (event) { case "ready": ret.readyHandler = undefined; break; case "message": ret.messageHandler = undefined; break; case "objectChange": ret.objectChangeHandler = undefined; break; case "stateChange": ret.stateChangeHandler = undefined; break; case "unload": ret.unloadHandler = undefined; break; } return ret; }), removeAllListeners: ((event) => { if (!event || event === "ready") { ret.readyHandler = undefined; } if (!event || event === "message") { ret.messageHandler = undefined; } if (!event || event === "objectChange") { ret.objectChangeHandler = undefined; } if (!event || event === "stateChange") { ret.stateChangeHandler = undefined; } if (!event || event === "unload") { ret.unloadHandler = undefined; } return ret; }), // Mock-specific methods resetMockHistory() { // reset Adapter (0, tools_1.doResetHistory)(ret); ret.log.resetMockHistory(); }, resetMockBehavior() { // reset Adapter (0, tools_1.doResetBehavior)(ret, implementedMethods); ret.log.resetMockBehavior(); }, resetMock() { ret.resetMockHistory(); ret.resetMockBehavior(); }, }); (0, tools_1.stubAndPromisifyImplementedMethods)(ret, implementedMethods, [ "getObjectView", "getObjectList", ]); // Access the options object directly, so we can react to later changes Object.defineProperties(ret, { readyHandler: { get() { return options.ready; }, set(handler) { options.ready = handler; }, }, messageHandler: { get() { return options.message; }, set(handler) { options.message = handler; }, }, objectChangeHandler: { get() { return options.objectChange; }, set(handler) { options.objectChange = handler; }, }, stateChangeHandler: { get() { return options.stateChange; }, set(handler) { options.stateChange = handler; }, }, unloadHandler: { get() { return options.unload; }, set(handler) { options.unload = handler; }, }, }); return ret; } exports.createAdapterMock = createAdapterMock;