From 066f725e7a7b071ac88274293a0f7c202dfb2210 Mon Sep 17 00:00:00 2001 From: danielpeintner Date: Mon, 29 Apr 2024 15:35:52 +0200 Subject: [PATCH 1/6] test: add testcase for async action --- packages/core/src/interaction-output.ts | 1 + packages/core/test/ClientTest.ts | 34 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/packages/core/src/interaction-output.ts b/packages/core/src/interaction-output.ts index 3a2d1768f..98362eba9 100644 --- a/packages/core/src/interaction-output.ts +++ b/packages/core/src/interaction-output.ts @@ -122,6 +122,7 @@ export class InteractionOutput implements WoT.InteractionOutput { // validate the schema const validate = ajv.compile(this.schema); + // Note: validation for action output should take place only if action is synchronous! if (!validate(json)) { debug(`schema = ${util.inspect(this.schema, { depth: 10, colors: true })}`); debug(`value: ${json}`); diff --git a/packages/core/test/ClientTest.ts b/packages/core/test/ClientTest.ts index f13e5f829..8c6353d87 100644 --- a/packages/core/test/ClientTest.ts +++ b/packages/core/test/ClientTest.ts @@ -108,6 +108,18 @@ const myThingDesc = { }, ], }, + anAsyncAction: { + input: { type: "integer" }, + output: { type: "integer" }, + synchronous: false, + forms: [ + { + href: "testdata://host/athing/actions/anasyncaction", + mediaType: "application/json", + response: { contentType: "application/json" }, + }, + ], + }, }, events: { anEvent: { @@ -510,6 +522,28 @@ class WoTClientTest { } } + @test async "call an async action"() { + // should not throw Error: Invalid value according to DataSchema + WoTClientTest.clientFactory.setTrap(async (form: Form, content: Content) => { + const valueData = await content.toBuffer(); + expect(valueData.toString()).to.equal("23"); + return new Content("application/json", Readable.from(Buffer.from("{'status': 'pending'"))); + }); + const td = (await WoTClientTest.WoTHelpers.fetch("td://foo")) as ThingDescription; + + const thing = await WoTClientTest.WoT.consume(td); + + expect(thing).to.have.property("title").that.equals("aThing"); + expect(thing).to.have.property("actions").that.has.property("anAction"); + + // deal with ActionStatus object + const result = await thing.invokeAction("anAsyncAction", 23); + // eslint-disable-next-line no-unused-expressions + expect(result).not.to.be.null; + const value = await result?.value(); + expect(value).to.have.property("status"); + } + @test async "subscribe to event"() { WoTClientTest.clientFactory.setTrap(() => { return new Content("application/json", Readable.from(Buffer.from("triggered"))); From 5f8a5c0ff108d04eb7e091fd5ff7f2639138c08f Mon Sep 17 00:00:00 2001 From: danielpeintner Date: Mon, 29 Apr 2024 16:19:11 +0200 Subject: [PATCH 2/6] refactor: allow to specify whether we want to have DataSchema validation --- packages/core/src/consumed-thing.ts | 14 +++++++----- packages/core/src/interaction-output.ts | 8 ++++--- packages/core/test/ClientTest.ts | 2 +- packages/core/test/InteractionOutputTest.ts | 25 +++++++++++++++++++++ 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/core/src/consumed-thing.ts b/packages/core/src/consumed-thing.ts index 5fafe9653..5d462120e 100644 --- a/packages/core/src/consumed-thing.ts +++ b/packages/core/src/consumed-thing.ts @@ -558,7 +558,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { const content = await client.readResource(form); try { - return this.handleInteractionOutput(content, form, tp); + return this.handleInteractionOutput(content, form, tp, false); } catch (e) { const error = e instanceof Error ? e : new Error(JSON.stringify(e)); throw new Error(`Error while processing property for ${tp.title}. ${error.message}`); @@ -568,7 +568,8 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { private handleInteractionOutput( content: Content, form: TD.Form, - outputDataSchema: WoT.DataSchema | undefined + outputDataSchema: WoT.DataSchema | undefined, + ignoreValidation: boolean | undefined ): InteractionOutput { // infer media type from form if not in response metadata content.type ??= form.contentType ?? "application/json"; @@ -583,7 +584,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { ); } } - return new InteractionOutput(content, form, outputDataSchema); + return new InteractionOutput(content, form, outputDataSchema, ignoreValidation); } async _readProperties(propertyNames: string[]): Promise { @@ -703,7 +704,8 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { const content = await client.invokeResource(form, input); try { - return this.handleInteractionOutput(content, form, ta.output); + const ignoreValidation = ta.synchronous === undefined ? true : !ta.synchronous; + return this.handleInteractionOutput(content, form, ta.output, ignoreValidation); } catch (e) { const error = e instanceof Error ? e : new Error(JSON.stringify(e)); throw new Error(`Error while processing action for ${ta.title}. ${error.message}`); @@ -746,7 +748,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { // next (content) => { try { - listener(this.handleInteractionOutput(content, form, tp)); + listener(this.handleInteractionOutput(content, form, tp, false)); } catch (e) { const error = e instanceof Error ? e : new Error(JSON.stringify(e)); warn(`Error while processing observe property for ${tp.title}. ${error.message}`); @@ -802,7 +804,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { formWithoutURITemplates, (content) => { try { - listener(this.handleInteractionOutput(content, form, te.data)); + listener(this.handleInteractionOutput(content, form, te.data, false)); } catch (e) { const error = e instanceof Error ? e : new Error(JSON.stringify(e)); warn(`Error while processing event for ${te.title}. ${error.message}`); diff --git a/packages/core/src/interaction-output.ts b/packages/core/src/interaction-output.ts index 98362eba9..ae75786cf 100644 --- a/packages/core/src/interaction-output.ts +++ b/packages/core/src/interaction-output.ts @@ -42,6 +42,7 @@ export class InteractionOutput implements WoT.InteractionOutput { dataUsed: boolean; form?: WoT.Form; schema?: WoT.DataSchema; + enforceValidation: boolean; // by default set to true public get data(): ReadableStream { if (this.#stream) { @@ -57,10 +58,11 @@ export class InteractionOutput implements WoT.InteractionOutput { return (this.#stream = ProtocolHelpers.toWoTStream(this.#content.body) as ReadableStream); } - constructor(content: Content, form?: WoT.Form, schema?: WoT.DataSchema) { + constructor(content: Content, form?: WoT.Form, schema?: WoT.DataSchema, ignoreValidation?: boolean) { this.#content = content; this.form = form; this.schema = schema; + this.enforceValidation = ignoreValidation === undefined ? true : !ignoreValidation; this.dataUsed = false; } @@ -123,7 +125,7 @@ export class InteractionOutput implements WoT.InteractionOutput { const validate = ajv.compile(this.schema); // Note: validation for action output should take place only if action is synchronous! - if (!validate(json)) { + if (this.enforceValidation && !validate(json)) { debug(`schema = ${util.inspect(this.schema, { depth: 10, colors: true })}`); debug(`value: ${json}`); debug(`Errror: ${validate.errors}`); @@ -131,6 +133,6 @@ export class InteractionOutput implements WoT.InteractionOutput { } this.#value = json; - return json; + return json as T; } } diff --git a/packages/core/test/ClientTest.ts b/packages/core/test/ClientTest.ts index 8c6353d87..ec7bfecac 100644 --- a/packages/core/test/ClientTest.ts +++ b/packages/core/test/ClientTest.ts @@ -527,7 +527,7 @@ class WoTClientTest { WoTClientTest.clientFactory.setTrap(async (form: Form, content: Content) => { const valueData = await content.toBuffer(); expect(valueData.toString()).to.equal("23"); - return new Content("application/json", Readable.from(Buffer.from("{'status': 'pending'"))); + return new Content("application/json", Readable.from(Buffer.from(JSON.stringify({ status: "pending" })))); }); const td = (await WoTClientTest.WoTHelpers.fetch("td://foo")) as ThingDescription; diff --git a/packages/core/test/InteractionOutputTest.ts b/packages/core/test/InteractionOutputTest.ts index 0d66b39dd..3c57e60de 100644 --- a/packages/core/test/InteractionOutputTest.ts +++ b/packages/core/test/InteractionOutputTest.ts @@ -20,6 +20,7 @@ import { expect, use } from "chai"; import { Readable } from "stream"; import { InteractionOutput } from "../src/interaction-output"; import { Content } from ".."; +import { fail } from "assert"; use(promised); const delay = (ms: number) => { @@ -106,6 +107,30 @@ class InteractionOutputTests { expect(result).be.true; } + @test async "should fail returning unexpected value with no validation"() { + const stream = Readable.from(Buffer.from("not boolean", "utf-8")); + const content = new Content("application/json", stream); + + const out = new InteractionOutput(content, {}, { type: "boolean" }); // ignoreValidation false by default + try { + const result = await out.value(); + expect(result).be.true; + fail("Wrongly allows invalid value"); + } catch { + // expected to throw + } + } + + @test async "should accept returning unexpected value with no validation"() { + // type boolean should not throw since we set ignoreValidation to true + const stream = Readable.from(Buffer.from("not boolean", "utf-8")); + const content = new Content("application/json", stream); + + const out = new InteractionOutput(content, {}, { type: "boolean" }, true); + const result = await out.value(); + expect(result).to.eql("not boolean"); + } + @test async "should data be used after arrayBuffer"() { const stream = Readable.from(Buffer.from("true", "utf-8")); const content = new Content("application/json", stream); From 54e887aa83b578a5e18e320b40f43e3ec3b39f8a Mon Sep 17 00:00:00 2001 From: danielpeintner Date: Mon, 29 Apr 2024 16:23:57 +0200 Subject: [PATCH 3/6] docs: remove now useless comment --- packages/core/src/interaction-output.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/interaction-output.ts b/packages/core/src/interaction-output.ts index ae75786cf..594b1a446 100644 --- a/packages/core/src/interaction-output.ts +++ b/packages/core/src/interaction-output.ts @@ -124,7 +124,6 @@ export class InteractionOutput implements WoT.InteractionOutput { // validate the schema const validate = ajv.compile(this.schema); - // Note: validation for action output should take place only if action is synchronous! if (this.enforceValidation && !validate(json)) { debug(`schema = ${util.inspect(this.schema, { depth: 10, colors: true })}`); debug(`value: ${json}`); From 4708fe7a5de7be62bd58032fec3e1212b3857bf9 Mon Sep 17 00:00:00 2001 From: danielpeintner Date: Tue, 30 Apr 2024 09:47:09 +0200 Subject: [PATCH 4/6] refactor: align flag using ignoreValidation everywhere --- packages/core/src/interaction-output.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/interaction-output.ts b/packages/core/src/interaction-output.ts index 594b1a446..fd757f1c2 100644 --- a/packages/core/src/interaction-output.ts +++ b/packages/core/src/interaction-output.ts @@ -42,7 +42,7 @@ export class InteractionOutput implements WoT.InteractionOutput { dataUsed: boolean; form?: WoT.Form; schema?: WoT.DataSchema; - enforceValidation: boolean; // by default set to true + ignoreValidation: boolean; // by default set to false public get data(): ReadableStream { if (this.#stream) { @@ -62,7 +62,7 @@ export class InteractionOutput implements WoT.InteractionOutput { this.#content = content; this.form = form; this.schema = schema; - this.enforceValidation = ignoreValidation === undefined ? true : !ignoreValidation; + this.ignoreValidation = ignoreValidation ?? false; this.dataUsed = false; } @@ -124,7 +124,7 @@ export class InteractionOutput implements WoT.InteractionOutput { // validate the schema const validate = ajv.compile(this.schema); - if (this.enforceValidation && !validate(json)) { + if (!this.ignoreValidation && !validate(json)) { debug(`schema = ${util.inspect(this.schema, { depth: 10, colors: true })}`); debug(`value: ${json}`); debug(`Errror: ${validate.errors}`); From 8a05b9066e9b0c1e3fcdc5213051f152be42f70d Mon Sep 17 00:00:00 2001 From: danielpeintner Date: Thu, 9 May 2024 14:50:36 +0200 Subject: [PATCH 5/6] refactor: introduce anonymous InteractionOutput options parameter --- packages/core/src/consumed-thing.ts | 2 +- packages/core/src/interaction-output.ts | 4 ++-- packages/core/test/InteractionOutputTest.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/consumed-thing.ts b/packages/core/src/consumed-thing.ts index 5d462120e..6b77e3a7c 100644 --- a/packages/core/src/consumed-thing.ts +++ b/packages/core/src/consumed-thing.ts @@ -584,7 +584,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { ); } } - return new InteractionOutput(content, form, outputDataSchema, ignoreValidation); + return new InteractionOutput(content, form, outputDataSchema, { ignoreValidation: ignoreValidation ?? false }); } async _readProperties(propertyNames: string[]): Promise { diff --git a/packages/core/src/interaction-output.ts b/packages/core/src/interaction-output.ts index fd757f1c2..9f36c258a 100644 --- a/packages/core/src/interaction-output.ts +++ b/packages/core/src/interaction-output.ts @@ -58,11 +58,11 @@ export class InteractionOutput implements WoT.InteractionOutput { return (this.#stream = ProtocolHelpers.toWoTStream(this.#content.body) as ReadableStream); } - constructor(content: Content, form?: WoT.Form, schema?: WoT.DataSchema, ignoreValidation?: boolean) { + constructor(content: Content, form?: WoT.Form, schema?: WoT.DataSchema, options = { ignoreValidation: false }) { this.#content = content; this.form = form; this.schema = schema; - this.ignoreValidation = ignoreValidation ?? false; + this.ignoreValidation = options.ignoreValidation ?? false; this.dataUsed = false; } diff --git a/packages/core/test/InteractionOutputTest.ts b/packages/core/test/InteractionOutputTest.ts index 3c57e60de..931317cbe 100644 --- a/packages/core/test/InteractionOutputTest.ts +++ b/packages/core/test/InteractionOutputTest.ts @@ -126,7 +126,7 @@ class InteractionOutputTests { const stream = Readable.from(Buffer.from("not boolean", "utf-8")); const content = new Content("application/json", stream); - const out = new InteractionOutput(content, {}, { type: "boolean" }, true); + const out = new InteractionOutput(content, {}, { type: "boolean" }, { ignoreValidation: true }); const result = await out.value(); expect(result).to.eql("not boolean"); } From f14ae6b500c2c98d4d4aaecf4e0b2254c83a3040 Mon Sep 17 00:00:00 2001 From: danielpeintner Date: Thu, 9 May 2024 14:53:31 +0200 Subject: [PATCH 6/6] refactor: make ignoreValidation required in private method only --- packages/core/src/consumed-thing.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/consumed-thing.ts b/packages/core/src/consumed-thing.ts index 6b77e3a7c..8c03f3938 100644 --- a/packages/core/src/consumed-thing.ts +++ b/packages/core/src/consumed-thing.ts @@ -569,7 +569,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { content: Content, form: TD.Form, outputDataSchema: WoT.DataSchema | undefined, - ignoreValidation: boolean | undefined + ignoreValidation: boolean ): InteractionOutput { // infer media type from form if not in response metadata content.type ??= form.contentType ?? "application/json"; @@ -584,7 +584,7 @@ export default class ConsumedThing extends TD.Thing implements IConsumedThing { ); } } - return new InteractionOutput(content, form, outputDataSchema, { ignoreValidation: ignoreValidation ?? false }); + return new InteractionOutput(content, form, outputDataSchema, { ignoreValidation }); } async _readProperties(propertyNames: string[]): Promise {