From 6ed212a77633c01ef70c287e3e39185b5757960f Mon Sep 17 00:00:00 2001 From: Ben Francis Date: Thu, 19 Aug 2021 18:05:51 +0100 Subject: [PATCH] Implement W3C compliant properties protocol binding --- src/app.ts | 7 ++- src/controllers/things_controller.ts | 22 +++----- src/test/browser/test-utils.ts | 7 ++- src/test/integration/logs-test.ts | 3 +- src/test/integration/things-test.ts | 84 ++++++++++++++-------------- src/test/rules-engine/index-test.ts | 19 ++++--- static/js/models/thing-model.js | 9 ++- 7 files changed, 79 insertions(+), 72 deletions(-) diff --git a/src/app.ts b/src/app.ts index a77dd94cb..30b670b36 100644 --- a/src/app.ts +++ b/src/app.ts @@ -273,7 +273,12 @@ function createApp(isSecure: boolean): express.Application { extended: false, }) ); - app.use(bodyParser.json({ limit: '1mb' })); + app.use( + bodyParser.json({ + limit: '1mb', + strict: false, + }) + ); // Use fileUpload to handle multi-part uploads app.use(fileUpload()); diff --git a/src/controllers/things_controller.ts b/src/controllers/things_controller.ts index 6eba4a9d3..cca5f6339 100644 --- a/src/controllers/things_controller.ts +++ b/src/controllers/things_controller.ts @@ -324,9 +324,7 @@ function build(): express.Router { const propertyName = request.params.propertyName; try { const value = await Things.getThingProperty(thingId, propertyName); - const result: Record = {}; - result[propertyName] = value; - response.status(200).json(result); + response.status(200).json(value); } catch (err) { response.status(err.code).send(err.message); } @@ -338,20 +336,18 @@ function build(): express.Router { controller.put('/:thingId/properties/:propertyName', async (request, response) => { const thingId = request.params.thingId; const propertyName = request.params.propertyName; - if (!request.body || typeof request.body[propertyName] === 'undefined') { - response.status(400).send('Invalid property name'); + if (typeof request.body === 'undefined') { + response.sendStatus(400); return; } - const value = request.body[propertyName]; + const value = request.body; try { + // Note: updatedValue may differ from value const updatedValue = await Things.setThingProperty(thingId, propertyName, value); - const result = { - [propertyName]: updatedValue, - }; - response.status(200).json(result); - } catch (e) { - console.error('Error setting property:', e); - response.status(e.code || 500).send(e.message); + response.status(200).json(updatedValue); + } catch (err) { + console.error('Error setting property:', err); + response.status(err.code || 500).send(err.message); } }); diff --git a/src/test/browser/test-utils.ts b/src/test/browser/test-utils.ts index 058199b66..e1d0a33c7 100644 --- a/src/test/browser/test-utils.ts +++ b/src/test/browser/test-utils.ts @@ -63,7 +63,7 @@ export async function getProperty(id: string, property: string): Promise { .get(`${Constants.THINGS_PATH}/${id}/properties/${property}`) .set('Accept', 'application/json') .set(...headerAuth(jwt)); - return res.body[property]; + return res.body; } export async function setProperty(id: string, property: string, value: T): Promise { @@ -72,9 +72,10 @@ export async function setProperty(id: string, property: string, value: T): Pr .keepOpen() .put(`${Constants.THINGS_PATH}/${id}/properties/${property}`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ [property]: value }); - return res.body[property]; + .send(JSON.stringify(value)); + return res.body; } export function escapeHtmlForIdClass(text: string): string { diff --git a/src/test/integration/logs-test.ts b/src/test/integration/logs-test.ts index 713ec27ac..0aa52f6ea 100644 --- a/src/test/integration/logs-test.ts +++ b/src/test/integration/logs-test.ts @@ -79,8 +79,9 @@ describe('logs/', function () { .request(server) .put(`${Constants.THINGS_PATH}/${thingId}/properties/${propId}`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ [propId]: value }); + .send(JSON.stringify(value)); expect(res.status).toEqual(200); // sleep just a bit to allow events to fire in the gateway diff --git a/src/test/integration/things-test.ts b/src/test/integration/things-test.ts index c5a52a97e..3e2fe8ec9 100644 --- a/src/test/integration/things-test.ts +++ b/src/test/integration/things-test.ts @@ -355,8 +355,7 @@ describe('things/', function () { .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - expect(res.body).toHaveProperty('power'); - expect(res.body.power).toEqual(false); + expect(res.body).toEqual(false); }); it('fail to GET a nonexistent property of a thing', async () => { @@ -385,8 +384,9 @@ describe('things/', function () { .request(server) .put(`${Constants.THINGS_PATH}/test-1/properties/power`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({}); + .send(); expect(err.status).toEqual(400); }); @@ -395,8 +395,9 @@ describe('things/', function () { .request(server) .put(`${Constants.THINGS_PATH}/test-1/properties/power`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ abc: true }); + .send('foo'); expect(err.status).toEqual(400); }); @@ -406,24 +407,24 @@ describe('things/', function () { .request(server) .put(`${Constants.THINGS_PATH}/test-1/properties/power`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ power: true }); + .send(JSON.stringify(true)); expect(on.status).toEqual(200); - expect(on.body).toHaveProperty('power'); - expect(on.body.power).toEqual(true); + expect(on.body).toEqual(true); // Flip it back to off... const off = await chai .request(server) .put(`${Constants.THINGS_PATH}/test-1/properties/power`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ power: false }); + .send(JSON.stringify(false)); expect(off.status).toEqual(200); - expect(off.body).toHaveProperty('power'); - expect(off.body.power).toEqual(false); + expect(off.body).toEqual(false); }); it('fail to set x and y coordinates of a non-existent thing', async () => { @@ -754,8 +755,9 @@ describe('things/', function () { .request(server) .put(`${Constants.THINGS_PATH}/${TEST_THING.id}/properties/power`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ power: true }), + .send(JSON.stringify(true)), ]); expect(res.status).toEqual(200); expect(messages[2].messageType).toEqual(Constants.PROPERTY_STATUS); @@ -782,8 +784,7 @@ describe('things/', function () { .set(...headerAuth(jwt)); expect(on.status).toEqual(200); - expect(on.body).toHaveProperty('power'); - expect(on.body.power).toEqual(true); + expect(on.body).toEqual(true); await webSocketClose(ws); }); @@ -865,23 +866,26 @@ describe('things/', function () { .request(server) .put(`${Constants.THINGS_PATH}/${otherThingId}/properties/power`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ power: true }) + .send(JSON.stringify(true)) .then(() => { return chai .request(server) .put(`${Constants.THINGS_PATH}/${TEST_THING.id}/properties/power`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ power: true }); + .send(JSON.stringify(true)); }) .then(() => { return chai .request(server) .put(`${Constants.THINGS_PATH}/${TEST_THING.id}/properties/power`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ power: false }); + .send(JSON.stringify(false)); }), webSocketRead(ws, 4), ]); @@ -1445,15 +1449,15 @@ describe('things/', function () { .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - expect(res.body).toHaveProperty('readOnlyProp'); - expect(res.body.readOnlyProp).toEqual(true); + expect(res.body).toEqual(true); const err = await chai .request(server) .put(`${Constants.THINGS_PATH}/validation-1/properties/readOnlyProp`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ readOnlyProp: false }); + .send(JSON.stringify(false)); expect(err.status).toEqual(400); res = await chai @@ -1463,8 +1467,7 @@ describe('things/', function () { .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - expect(res.body).toHaveProperty('readOnlyProp'); - expect(res.body.readOnlyProp).toEqual(true); + expect(res.body).toEqual(true); }); it('fail to set invalid number property value', async () => { @@ -1477,15 +1480,15 @@ describe('things/', function () { .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - expect(res.body).toHaveProperty('minMaxProp'); - expect(res.body.minMaxProp).toEqual(15); + expect(res.body).toEqual(15); let err = await chai .request(server) .put(`${Constants.THINGS_PATH}/validation-1/properties/minMaxProp`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ minMaxProp: 0 }); + .send(JSON.stringify(0)); expect(err.status).toEqual(400); res = await chai @@ -1495,15 +1498,15 @@ describe('things/', function () { .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - expect(res.body).toHaveProperty('minMaxProp'); - expect(res.body.minMaxProp).toEqual(15); + expect(res.body).toEqual(15); err = await chai .request(server) .put(`${Constants.THINGS_PATH}/validation-1/properties/minMaxProp`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ minMaxProp: 30 }); + .send(JSON.stringify(30)); expect(err.status).toEqual(400); res = await chai @@ -1513,8 +1516,7 @@ describe('things/', function () { .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - expect(res.body).toHaveProperty('minMaxProp'); - expect(res.body.minMaxProp).toEqual(15); + expect(res.body).toEqual(15); res = await chai .request(server) @@ -1523,15 +1525,15 @@ describe('things/', function () { .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - expect(res.body).toHaveProperty('multipleProp'); - expect(res.body.multipleProp).toEqual(10); + expect(res.body).toEqual(10); err = await chai .request(server) .put(`${Constants.THINGS_PATH}/validation-1/properties/multipleProp`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ multipleProp: 3 }); + .send(JSON.stringify(3)); expect(err.status).toEqual(400); res = await chai @@ -1541,15 +1543,15 @@ describe('things/', function () { .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - expect(res.body).toHaveProperty('multipleProp'); - expect(res.body.multipleProp).toEqual(10); + expect(res.body).toEqual(10); res = await chai .request(server) .put(`${Constants.THINGS_PATH}/validation-1/properties/multipleProp`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ multipleProp: 30 }); + .send(JSON.stringify(30)); expect(res.status).toEqual(200); res = await chai @@ -1559,8 +1561,7 @@ describe('things/', function () { .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - expect(res.body).toHaveProperty('multipleProp'); - expect(res.body.multipleProp).toEqual(30); + expect(res.body).toEqual(30); }); it('fail to set invalid enum property value', async () => { @@ -1573,15 +1574,15 @@ describe('things/', function () { .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - expect(res.body).toHaveProperty('enumProp'); - expect(res.body.enumProp).toEqual('val2'); + expect(res.body).toEqual('val2'); const err = await chai .request(server) .put(`${Constants.THINGS_PATH}/validation-1/properties/enumProp`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ enumProp: 'val0' }); + .send(JSON.stringify('val0')); expect(err.status).toEqual(400); res = await chai @@ -1591,7 +1592,6 @@ describe('things/', function () { .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - expect(res.body).toHaveProperty('enumProp'); - expect(res.body.enumProp).toEqual('val2'); + expect(res.body).toEqual('val2'); }); }); diff --git a/src/test/rules-engine/index-test.ts b/src/test/rules-engine/index-test.ts index a398f55dd..29237fa02 100644 --- a/src/test/rules-engine/index-test.ts +++ b/src/test/rules-engine/index-test.ts @@ -453,8 +453,9 @@ describe('rules engine', () => { .request(server) .put(`${Constants.THINGS_PATH}/${thingLight1.id}/properties/on`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ on: true }); + .send(JSON.stringify(true)); res = await chai .request(server) @@ -463,7 +464,7 @@ describe('rules engine', () => { .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - expect(res.body.on).toEqual(false); + expect(res.body).toEqual(false); await deleteRule(ruleId); }); @@ -511,8 +512,9 @@ describe('rules engine', () => { .request(server) .put(`${Constants.THINGS_PATH}/${thingLight2.id}/properties/hue`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ hue: 150 }), + .send(JSON.stringify(150)), webSocketRead(ws, 7), ]); expect(resPut.status).toEqual(200); @@ -533,8 +535,9 @@ describe('rules engine', () => { .request(server) .put(`${Constants.THINGS_PATH}/${thingLight2.id}/properties/hue`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ hue: 0 }), + .send(JSON.stringify(0)), webSocketRead(ws, 1), ]); expect(resPut.status).toEqual(200); @@ -558,7 +561,7 @@ describe('rules engine', () => { .set('Accept', 'application/json') .set(...headerAuth(jwt)); expect(res.status).toEqual(200); - return res.body.on; + return res.body; } async function setOn(lightId: string, on: boolean): Promise { @@ -566,8 +569,9 @@ describe('rules engine', () => { .request(server) .put(`${Constants.THINGS_PATH}/${lightId}/properties/on`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ on }); + .send(JSON.stringify(on)); expect(res.status).toEqual(200); } @@ -676,8 +680,9 @@ describe('rules engine', () => { .request(server) .put(`${Constants.THINGS_PATH}/light3/properties/color`) .set('Accept', 'application/json') + .type('json') .set(...headerAuth(jwt)) - .send({ color: '#00ff77' }); + .send(JSON.stringify('#00ff77')); expect(res.status).toEqual(200); await waitForExpect(async () => { diff --git a/static/js/models/thing-model.js b/static/js/models/thing-model.js index 9fc54d742..de08c0fca 100644 --- a/static/js/models/thing-model.js +++ b/static/js/models/thing-model.js @@ -193,9 +193,6 @@ class ThingModel extends Model { } const property = this.propertyDescriptions[name]; - const payload = { - [name]: value, - }; let href; for (const link of property.links) { @@ -205,9 +202,11 @@ class ThingModel extends Model { } } - return API.putJson(href, payload) + return API.putJson(href, value) .then((json) => { - this.onPropertyStatus(json); + const result = {}; + result[name] = json; + this.onPropertyStatus(result); }) .catch((error) => { console.error(error);