UNPKG

@iobroker/testing

Version:

Shared utilities for adapter and module testing in ioBroker

565 lines (564 loc) 19.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createAdapterMock = createAdapterMock; 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', // @ts-expect-error This method was deprecated 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 = Object.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 = Object.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; }