From 4c283b5c0c26344b13b60ed93f798330022b937e Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Thu, 21 Dec 2023 17:30:38 +0100 Subject: [PATCH 1/5] Test for label array added --- tests/components/endpoints.js | 12 ++++++++++-- tests/components/rooms.js | 6 +++++- tests/components/test.workflow.js | 19 ++++++++++++++++++- tests/components/users.js | 11 +++++++++-- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/tests/components/endpoints.js b/tests/components/endpoints.js index 9f02635..1f5f595 100644 --- a/tests/components/endpoints.js +++ b/tests/components/endpoints.js @@ -16,7 +16,11 @@ try { C_COMPONENT.add({ _id, name: "Endpoint #1", - device: _device + device: _device, + labels: [ + "key1=value1", + "key2=value2" + ] }, (err, item) => { try { @@ -59,7 +63,11 @@ try { workflow(C_COMPONENT, "update", (done) => { C_COMPONENT.update(_id, { - name: "Endpoint #2 - updated" + name: "Endpoint #2 - updated", + labels: [ + "overriden=true", + "key3=value3" + ] }, (err, item) => { try { diff --git a/tests/components/rooms.js b/tests/components/rooms.js index 2fe23b3..f137a6a 100644 --- a/tests/components/rooms.js +++ b/tests/components/rooms.js @@ -15,7 +15,11 @@ try { C_COMPONENT.add({ _id, name: "Room #1", - floor: 1 + floor: 1, + labels: [ + "foo=bar", + "baz=true" + ] }, (err, item) => { try { diff --git a/tests/components/test.workflow.js b/tests/components/test.workflow.js index 5d5587a..af1594c 100644 --- a/tests/components/test.workflow.js +++ b/tests/components/test.workflow.js @@ -5,6 +5,7 @@ const sinon = require("sinon"); const _iterate = require("../../helper/iterate.js"); const Item = require("../../system/component/class.item.js"); +const Label = require("../../system/component/class.label.js"); module.exports = (C_COMPONENT, method, desc, worker) => { @@ -58,6 +59,18 @@ module.exports = (C_COMPONENT, method, desc, worker) => { done(); }); + it("Item .labels array should contain only class.label.js instances", () => { + + let valid = C_COMPONENT.items.map(({ labels }) => { + return labels; + }).flat().every((label) => { + return label instanceof Label; + }); + + assert.ok(valid); + + }); + it("Component item _id property should be a string, not a ObjectId instance", (done) => { assert.ok(typeof (C_COMPONENT.items[0]._id) === "string"); done(); @@ -71,11 +84,15 @@ module.exports = (C_COMPONENT, method, desc, worker) => { if (type === "array") { let result = value.every((entry) => { - if (entry instanceof Object) { + + // check if only "plain" objects have _id property + // otherwise this check fails on the .labels array items + if (entry instanceof Object && Object.getPrototypeOf(entry) === null) { return Object.hasOwnProperty.call(entry, "_id"); } else { return true; } + }); assert.ok(result === true); diff --git a/tests/components/users.js b/tests/components/users.js index 40c946e..6d0a9e8 100644 --- a/tests/components/users.js +++ b/tests/components/users.js @@ -59,7 +59,10 @@ try { workflow(C_COMPONENT, "update", (done) => { C_COMPONENT.update(_id, { - enabled: true + enabled: true, + labels: [ + "expires=29991231" + ] }, (err, item) => { try { @@ -83,7 +86,11 @@ try { // update call 1 C_COMPONENT.update(_id, { - password: "12345678" + password: "12345678", + labels: [ + "expires=29991231", + "key=value" + ] }), // update call 2 From 7863bb21ebf485f56c0a97bb28be70f5b97fcd31 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Thu, 21 Dec 2023 21:10:16 +0100 Subject: [PATCH 2/5] unit test added --- tests/system/component/index.js | 2 + tests/system/component/test.label.js | 39 ++++++++++++++++ tests/system/component/test.labels.js | 66 +++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 tests/system/component/test.label.js create mode 100644 tests/system/component/test.labels.js diff --git a/tests/system/component/index.js b/tests/system/component/index.js index a717d51..5990e00 100644 --- a/tests/system/component/index.js +++ b/tests/system/component/index.js @@ -2,6 +2,8 @@ const { describe } = require("mocha"); describe("system/component", function () { + require("./test.label.js"); + require("./test.labels.js"); require("./test.base.js"); require("./test.common.js"); require("./test.component.js"); diff --git a/tests/system/component/test.label.js b/tests/system/component/test.label.js new file mode 100644 index 0000000..363b4fb --- /dev/null +++ b/tests/system/component/test.label.js @@ -0,0 +1,39 @@ +const { describe, it } = require("mocha"); +const assert = require("assert"); + +const Label = require("../../../system/component/class.label.js"); + +describe("label", function () { + + let instance = new Label("hello=world"); + + it("should have a key property", () => { + assert(instance.key); + }); + + it("should have a value property", () => { + assert(instance.key); + }); + + it("should have a label property", () => { + assert(instance.label); + }); + + it(".key = hello", () => { + assert.equal(instance.key, "hello"); + }); + + it(".value = world", () => { + assert.equal(instance.value, "world"); + }); + + ["toJSON", "toString"].forEach((fnc) => { + + it(`should have method "${fnc}"`, () => { + assert(instance[fnc] instanceof Function); + }); + + }); + + +}); \ No newline at end of file diff --git a/tests/system/component/test.labels.js b/tests/system/component/test.labels.js new file mode 100644 index 0000000..b6be5ca --- /dev/null +++ b/tests/system/component/test.labels.js @@ -0,0 +1,66 @@ +const { describe, it } = require("mocha"); +const assert = require("assert"); + +const Labels = require("../../../system/component/class.labels.js"); +const Label = require("../../../system/component/class.label.js"); + +describe("labels", function () { + + let labels = [ + "foo=bar", + "baz=true", + "gen=1", + 'json={"key": "value"}', + "gen=3" + ].map((txt) => { + return new Label(txt); + }); + + let instance = new Labels(...labels); + + it("should be a instance of Array", () => { + assert(instance instanceof Array); + }); + + it("should have 4 items", () => { + assert.equal(instance.length, 5); + }); + + it("every item should be a instance of class.label.js", () => { + + let valid = instance.every((label) => { + return label instanceof Label; + }); + + assert(valid); + + }); + + [ + "key", "value", "has", + "filter", "toJSON" + ].forEach((fnc) => { + + it(`should have method "${fnc}"`, () => { + assert(instance[fnc] instanceof Function); + }); + + }); + + it('labels.key("bar") should return "foo"', () => { + assert.equal(instance.key("bar"), "foo"); + }); + + it('labels.value("baz") should return "foo"', () => { + assert.equal(instance.value("baz"), "true"); + }); + + it('labels.has("gen") should return true', () => { + assert.equal(instance.has("gen"), true); + }); + + it('labels.filter("gen=*") should return 2 items', () => { + assert.equal(instance.filter("gen=*").length, 2); + }); + +}); \ No newline at end of file From ba2df9159a761c77858331f80f29fd5cbc81ceaa Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Fri, 22 Dec 2023 11:04:21 +0100 Subject: [PATCH 3/5] refactor label array, fix #352 --- system/component/class.component.js | 19 ++++++++++- system/component/class.item.js | 38 +++++++++++++++++++++ system/component/class.label.js | 52 +++++++++++++++++++++++++++++ system/component/class.labels.js | 51 ++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 system/component/class.label.js create mode 100644 system/component/class.labels.js diff --git a/system/component/class.component.js b/system/component/class.component.js index f6aa86f..3a393fa 100644 --- a/system/component/class.component.js +++ b/system/component/class.component.js @@ -5,6 +5,7 @@ const _extend = require("../../helper/extend"); const _merge = require("../../helper/merge"); const COMMON = require("./class.common.js"); +const Label = require("./class.label.js"); const PENDING_CHANGE_EVENTS = new Set(); @@ -98,7 +99,23 @@ module.exports = class COMPONENT extends COMMON { let baseSchema = Joi.object({ //labels: Joi.array().items(Joi.string().regex(/^[a-zA-Z0-9]+=[a-zA-Z0-9]+$/)).default([]) //labels: Joi.array().items(Joi.string().regex(/^[a-z0-9\.]+=[a-z0-9]+$/)).default([]), - labels: Joi.array().items(Joi.string().regex(/^.+?=.+|.+=.+$/i)).default([]), + //labels: Joi.array().items(Joi.string().regex(/^.+?=.+|.+=.+$/i)).default([]), + labels: Joi.array().items(Joi.alternatives().try( + Joi.string().regex(/^.+?=.+|.+=.+$/i), + Joi.object().custom((value, helpers) => { + if (value instanceof Label) { + + // convert to string + // otherwise the serialized object is saved into the database + return value.toString(); + + } else { + + return helpers.error("any.custom"); + + } + }, "Instance of class.label.js") + )).default([]), timestamps: Joi.object({ ...schema?.timestamps, created: Joi.number().allow(null).default(null), diff --git a/system/component/class.item.js b/system/component/class.item.js index efcba43..6fd8bee 100644 --- a/system/component/class.item.js +++ b/system/component/class.item.js @@ -1,3 +1,6 @@ +const Labels = require("./class.labels.js"); +const Label = require("./class.label.js"); + module.exports = class Item { constructor(obj) { @@ -11,6 +14,41 @@ module.exports = class Item { // see issue https://github.com/OpenHausIO/backend/issues/352 // this.labels = obj.labels.map(...); + obj.labels?.forEach((txt, i, arr) => { + if (!(txt instanceof Label)) { + arr[i] = new Label(txt); + } + }); + + let labels = new Labels(...obj.labels); + + Object.defineProperty(this, "labels", { + get() { + + //console.log("get called"); + return labels; + + }, + set(value) { + + // clear array + // needed when new array has fewer items than old one + // otherwise it contains old items & has wrong size + labels.splice(0, labels.length); + + value.forEach((txt, i) => { + if (!(txt instanceof Label)) { + labels[i] = new Label(txt); + } else { + labels[i] = txt; + } + }); + + }, + enumerable: true, + configurable: false + }); + } }; \ No newline at end of file diff --git a/system/component/class.label.js b/system/component/class.label.js new file mode 100644 index 0000000..cedc28e --- /dev/null +++ b/system/component/class.label.js @@ -0,0 +1,52 @@ +module.exports = class Label { + + constructor(label) { + + let [key, value] = label.split("="); + + Object.defineProperty(this, "key", { + set(val) { + key = val; + label = `${key}=${value}`; + }, + get() { + return key; + }, + enumerable: true + }); + + Object.defineProperty(this, "value", { + set(val) { + value = val; + label = `${key}=${value}`; + }, + get() { + return value; + }, + enumerable: true + }); + + Object.defineProperty(this, "label", { + set(val) { + let { k, v } = label.split("="); + label = val; + key = k; + value = v; + }, + get() { + return label; + }, + enumerable: true + }); + + } + + toJSON() { + return `${this.key}=${this.value}`; + } + + toString() { + return `${this.key}=${this.value}`; + } + +}; \ No newline at end of file diff --git a/system/component/class.labels.js b/system/component/class.labels.js new file mode 100644 index 0000000..51b5f05 --- /dev/null +++ b/system/component/class.labels.js @@ -0,0 +1,51 @@ +module.exports = class Labels extends Array { + + constructor(...args) { + super(...args); + } + + value(key) { + return this.find((label) => { + return label.key === key; + })?.value; + } + + key(value) { + return this.find((label) => { + return label.value === value; + })?.key; + } + + has(key) { + return !!this.find((label) => { + return label.key === key; + }); + } + + filter(query) { + + let [k, v] = query.split("="); + + return Array.prototype.filter.call(this, (label) => { + + if (k !== "*") { + return label.key === k; + } + + if (v !== "*") { + return label.value === v; + } + + return label.key === k && label.value === v; + + }); + + } + + toJSON() { + return this.map(({ key, value }) => { + return `${key}=${value}`; + }); + } + +}; \ No newline at end of file From fdced8039e15ac3f67e69f87dddbfdacffeb68e8 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Fri, 22 Dec 2023 11:08:56 +0100 Subject: [PATCH 4/5] fix #378 --- system/component/class.component.js | 7 +++++-- system/component/class.item.js | 31 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/system/component/class.component.js b/system/component/class.component.js index 3a393fa..e0bc134 100644 --- a/system/component/class.component.js +++ b/system/component/class.component.js @@ -1,11 +1,11 @@ const mongodb = require("mongodb"); -const Joi = require("joi"); +//const Joi = require("joi"); const _extend = require("../../helper/extend"); const _merge = require("../../helper/merge"); const COMMON = require("./class.common.js"); -const Label = require("./class.label.js"); +const Item = require("./class.item.js"); const PENDING_CHANGE_EVENTS = new Set(); @@ -95,7 +95,9 @@ module.exports = class COMPONENT extends COMMON { }); this.collection = mongodb.client.collection(name); + this.schema = Item.schema().concat(schema); + /* let baseSchema = Joi.object({ //labels: Joi.array().items(Joi.string().regex(/^[a-zA-Z0-9]+=[a-zA-Z0-9]+$/)).default([]) //labels: Joi.array().items(Joi.string().regex(/^[a-z0-9\.]+=[a-z0-9]+$/)).default([]), @@ -125,6 +127,7 @@ module.exports = class COMPONENT extends COMMON { // concat base schema with component specific this.schema = baseSchema.concat(schema); + */ if (process.env.DATABASE_WATCH_CHANGES === "true") { try { diff --git a/system/component/class.item.js b/system/component/class.item.js index 6fd8bee..3367abe 100644 --- a/system/component/class.item.js +++ b/system/component/class.item.js @@ -1,3 +1,5 @@ +const Joi = require("joi"); + const Labels = require("./class.labels.js"); const Label = require("./class.label.js"); @@ -51,4 +53,33 @@ module.exports = class Item { } + static schema() { + return Joi.object({ + labels: Joi.array().items(Joi.alternatives().try( + Joi.string().regex(/^.+?=.+|.+=.+$/i), + Joi.object().custom((value, helpers) => { + if (value instanceof Label) { + + // convert to string + // otherwise the serialized object is saved into the database + return value.toString(); + + } else { + + return helpers.error("any.custom"); + + } + }, "Instance of class.label.js") + )).default([]), + timestamps: Joi.object({ + created: Joi.number().allow(null).default(null), + updated: Joi.number().allow(null).default(null) + }) + }); + } + + static validate(data) { + return Item.schema().validate(data); + } + }; \ No newline at end of file From b6e6766f058475cfee11f0ca25c0c64d066c6b32 Mon Sep 17 00:00:00 2001 From: Marc Stirner Date: Fri, 22 Dec 2023 11:09:42 +0100 Subject: [PATCH 5/5] labels added --- postman.json | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/postman.json b/postman.json index e1a88f1..a619ad3 100644 --- a/postman.json +++ b/postman.json @@ -40,7 +40,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"_id\": \"65818753e275da05046aaa78\",\n \"name\": \"Livingroom\" \n}", + "raw": "{\n \"_id\": \"65818753e275da05046aaa78\",\n \"name\": \"Livingroom\",\n \"labels\": [\n \"foo=bar\",\n \"baz=true\"\n ]\n}", "options": { "raw": { "language": "json" @@ -854,7 +854,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"_id\": \"65818856464bebcf19ebec4c\",\n \"topic\": \"air-sensor/sensor/particulate_matter_25m_concentration\"\n}", + "raw": "{\n \"_id\": \"65818856464bebcf19ebec4c\",\n \"topic\": \"air-sensor/sensor/particulate_matter_25m_concentration\",\n \"labels\": [\n \"manufacturer=custom\",\n \"esp8266=true\"\n ]\n}", "options": { "raw": { "language": "json" @@ -982,7 +982,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"description\": \"Ikea VINDRIKTNING MQTT modd\"\n}", + "raw": "{\n \"description\": \"Ikea VINDRIKTNING MQTT mod\",\n \"labels\": [\n \"manufacturer=Ikea\",\n \"model=VINDRIKTNING\",\n \"esp8266=true\"\n ]\n}", "options": { "raw": { "language": "json" @@ -1342,7 +1342,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"_id\": \"658188a7aadfcc026a0e0131\",\n \"name\": \"Hans Hubert #3\",\n \"email\": \"hans.hubert3@example.com\",\n \"password\": \"Pa$$w0rd\"\n}", + "raw": "{\n \"_id\": \"658188a7aadfcc026a0e0131\",\n \"name\": \"Hans Hubert #3\",\n \"email\": \"hans.hubert3@example.com\",\n \"password\": \"Pa$$w0rd\",\n \"labels\": [\n \"expires=29991231\"\n ]\n}", "options": { "raw": { "language": "json" @@ -1571,7 +1571,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"_id\": \"658188e93cde9987c3228806\",\n \"name\": \"Plugin Boilerplate Demo\",\n \"enabled\": true,\n \"version\": 1,\n \"intents\": [\n \"devices\",\n \"endpoints\",\n \"plugins\",\n \"rooms\",\n \"ssdp\",\n \"store\",\n \"users\",\n \"vault\"\n ]\n}", + "raw": "{\n \"_id\": \"658188e93cde9987c3228806\",\n \"name\": \"Plugin Boilerplate Demo\",\n \"uuid\": \"6951dee2-8541-4a69-bd3e-629fdadf093a\",\n \"enabled\": true,\n \"version\": 1,\n \"intents\": [\n \"devices\",\n \"endpoints\",\n \"plugins\",\n \"rooms\",\n \"ssdp\",\n \"store\",\n \"users\",\n \"vault\"\n ]\n}", "options": { "raw": { "language": "json" @@ -1642,7 +1642,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"name\": \"New updated\",\n \"enabled\": true,\n \"autostart\": false\n}", + "raw": "{\n \"name\": \"New updated\",\n \"enabled\": true,\n \"autostart\": false,\n \"labels\": [\n \"worker_thread=false\",\n \"my_custom_label={\\\"json\\\":true, \\\"number\\\":0815420}\"\n ]\n}", "options": { "raw": { "language": "json" @@ -1667,6 +1667,26 @@ }, { "name": "Upload plugin *.tgz content", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], "request": { "method": "PUT", "header": [], @@ -1776,7 +1796,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"_id\": \"65818918d32dad8dab53e433\",\n \"name\": \"SmartMeter\",\n \"interfaces\": [{\n \"_id\": \"6581c55abc21a0a3122b9998\",\n \"type\": \"ETHERNET\",\n \"description\": \"WebSocket API\",\n \"settings\": {\n \"host\": \"192.168.2.155\",\n \"port\": 8080\n },\n \"adapter\": [\"raw\"]\n }],\n \"room\": \"62a4bc8bd9256b5e8d6988a0\",\n \"icon\": \"fa-solid fa-gauge-high\"\n}", + "raw": "{\n \"_id\": \"65818918d32dad8dab53e433\",\n \"name\": \"SmartMeter\",\n \"interfaces\": [{\n \"_id\": \"6581c55abc21a0a3122b9998\",\n \"type\": \"ETHERNET\",\n \"description\": \"WebSocket API\",\n \"settings\": {\n \"host\": \"192.168.2.155\",\n \"port\": 8080\n },\n \"adapter\": [\"raw\"]\n }],\n \"room\": \"62a4bc8bd9256b5e8d6988a0\",\n \"icon\": \"fa-solid fa-gauge-high\",\n \"labels\": [\n \"test=true\",\n \"protected=false\", \n \"foo=bar\"\n ]\n}", "options": { "raw": { "language": "json" @@ -1846,7 +1866,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"enabled\": true,\n \"name\": \"SaMsUnG FrIdGe\"\n}", + "raw": "{\n \"enabled\": true,\n \"name\": \"SaMsUnG FrIdGe\",\n \"labels\": [\n \"test=true\",\n \"protected=true\", \n \"foo=bar\"\n ]\n}", "options": { "raw": { "language": "json" @@ -2505,7 +2525,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"item\": \"6375343d0b555ccd42460a2e\"\n}", + "raw": "{\n \"item\": \"6375343d0b555ccd42460a2e\",\n \"labels\": [\n \"device=65818918d32dad8dab53e433\",\n \"endpoint=658189336fa19198939caa21\"\n ]\n}", "options": { "raw": { "language": "json" @@ -2599,11 +2619,6 @@ "}", "", "", - "pm.test(\"content-type = application/json\", () => {", - " pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');", - "});", - "", - "", "pm.test(\"Status code 200 || 202\", () => {", " pm.expect(pm.response.code).to.be.oneOf([", " 200,", @@ -2612,6 +2627,11 @@ "});", "", "", + "pm.test(\"content-type = application/json\", () => {", + " pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');", + "});", + "", + "", "pm.test(\"Response has no error field\", () => {", "", " let length = pm.response.headers.get(\"content-length\");",