From 85c8ec1f843109c9798ecdcf2bda4245f5c436b2 Mon Sep 17 00:00:00 2001 From: Marin Karamihalev <marin.karamihalev@iopsys.eu> Date: Thu, 3 Aug 2023 13:57:52 +0200 Subject: [PATCH] added errors --- package.json | 2 +- src/commands/common/add.ts | 12 ++- src/commands/common/set.ts | 4 +- src/configurations/build.ts | 11 ++- src/errors.ts | 41 ++++++++ src/types.ts | 56 ++++++++++- src/util.ts | 38 ++++++-- src/verify.ts | 148 +++++++++++++++++++++++++++++ tests/integration/config.json | 2 +- tests/integration/errors.test.ts | 157 +++++++++++++++++++++++++++++++ tests/integration/index.test.ts | 17 ++++ 11 files changed, 466 insertions(+), 22 deletions(-) create mode 100644 src/errors.ts create mode 100644 src/verify.ts create mode 100644 tests/integration/errors.test.ts diff --git a/package.json b/package.json index e83305e..c1201a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "usp-js", - "version": "0.4.4", + "version": "0.4.5", "description": "Library for easy usp(TR-369) communication using mqtt or ws.", "main": "node/index.js", "browser": "web/index.js", diff --git a/src/commands/common/add.ts b/src/commands/common/add.ts index 96a18df..9b410a7 100644 --- a/src/commands/common/add.ts +++ b/src/commands/common/add.ts @@ -1,6 +1,10 @@ import { DecodeFn, EncodeFn } from "../../types"; import * as util from "../util"; -import { AddLookupCreateObject, AddLookupCreateParamSetting } from "../../types"; +import { + AddLookupCreateObject, + AddLookupCreateParamSetting, +} from "../../types"; +import { parseSetArgs } from "../../util"; const decode: DecodeFn = (msg) => { const paths: string[] | undefined = util.searchAll(msg, "instantiatedPath"); @@ -21,9 +25,9 @@ const makePair = (value): [string, any, boolean][] => : []; const encode: EncodeFn = ({ value, path }) => { - const paths = Array.isArray(path) ? path : [path]; - const values = value ? (Array.isArray(value) ? value : [value]) : []; - const allowPartial = values && values.some((it) => it.allowPartial) || false; + const [paths, values] = parseSetArgs(value, path); + const allowPartial = + (values && values.some((it) => it.allowPartial)) || false; const createObjs = paths.map((path, i) => ({ lookup: "Add.CreateObject" as AddLookupCreateObject, diff --git a/src/commands/common/set.ts b/src/commands/common/set.ts index 31fede9..dd63b3e 100644 --- a/src/commands/common/set.ts +++ b/src/commands/common/set.ts @@ -6,6 +6,7 @@ import { SetLookupUpdateParamSetting, } from "../../types"; import * as util from "../util"; +import { parseSetArgs } from "../../util"; const decode: DecodeFn = (msg, decodeOptions) => { if (decodeOptions?.raw) return [msg]; @@ -25,8 +26,7 @@ const makePairs = (path: string, value): [string, any, boolean][] => : [[path.split(".").pop() || "", (value || "").toString(), true]]; const encode: EncodeFn = ({ value, path: initialPath }) => { - const paths = Array.isArray(initialPath) ? initialPath : [initialPath]; - const values = value ? (Array.isArray(value) ? value : [value]) : []; + const [paths, values] = parseSetArgs(value, initialPath); const allowPartial = (values && values.some((it) => it.allowPartial)) || false; diff --git a/src/configurations/build.ts b/src/configurations/build.ts index 39f864d..5098a5f 100644 --- a/src/configurations/build.ts +++ b/src/configurations/build.ts @@ -28,6 +28,8 @@ import { makeRouter, parseCallbackOptions, } from "../util"; +import makeVerify from "../verify"; +import defaultErrors from "../errors"; const defaultPublishEndpoint = "/usp/endpoint"; const defaultSubscribeEndpoint = "/usp/controller"; @@ -175,7 +177,7 @@ const initializeSession = ({ }; const buildConnect: BuildConnectionFn = - ({ connectClient, decodeID, loadProtobuf }) => + ({ connectClient, decodeID, loadProtobuf, errors = defaultErrors }) => async (options, events) => { const subscribeEndpoint = options.subscribeEndpoint || defaultSubscribeEndpoint; @@ -310,8 +312,15 @@ const buildConnect: BuildConnectionFn = }; }; + const verify = makeVerify(errors); const call: CallFn = (command, args, callOpts) => new Promise((resolve, reject) => { + const error = verify(command, args); + if (error) { + reject(error); + return; + } + // todo make looky nice sessionOptions.sequenceId++; sessionOptions.expectedId = sessionOptions.sequenceId; diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..70ee712 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,41 @@ +import { VerifyResult } from "./types"; + +export const defaultMissingErrorMessage = "Unknown error occurred"; + +export default { + [VerifyResult.NoError]: "No error", + + // GET + [VerifyResult.PathsMissing]: "'paths' argument is missing", + [VerifyResult.PathsNotStringOrStringArray]: + "'paths' argument has to be a string or a string array", + [VerifyResult.RawOptionNotBool]: "'raw' option has to be a boolean", + [VerifyResult.RetainPathOptionNotBool]: + "'retainPath' option has to be a boolean", + [VerifyResult.MaxDepthOptionNotNumber]: + "'max_depth' option has to be a positive number", + [VerifyResult.MaxDepthOptionIsNegative]: + "'max_depth' option has to be a positive number", + + // SET / ADD + [VerifyResult.ValueIsMissing]: "'value' argument is required", + [VerifyResult.ValueMustBeArray]: + "'value' argument must be an array, when 'path' is an array", + [VerifyResult.ValueAndPathArraysMustBeSameLength]: + "'path' and 'value' arrays must have the same length", + [VerifyResult.ValueIsNotTupleArray]: + "'path' argument must be an array of tuples", + + // DELETE + [VerifyResult.AllowPartialOptionNotBool]: + "'allowPartial' option has to be a boolean", + + // SUPPORTED_DM + [VerifyResult.OptionsMustBeBool]: "all options have to be boolean", + + // INSTANCES + [VerifyResult.FirstLevelOnlyNotBool]: "'firstLevelOnly' has to be a boolean", + + // PROTO + [VerifyResult.ProtoNotString]: "'proto' argument has to be a string", +} as Record<VerifyResult, string>; diff --git a/src/types.ts b/src/types.ts index fb14a30..321d32d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -130,6 +130,38 @@ export type SetRawResponse = { }; }; +export type VerifierFunc = (args: Record<string, any>) => VerifyResult; + +export enum VerifyResult { + NoError = "VerifyResult.NoError", + + // GET + PathsMissing = "VerifyResult.PathsMissing", + PathsNotStringOrStringArray = "VerifyResult.PathsNotStringOrStringArray", + RawOptionNotBool = "VerifyResult.RawOptionNotBool", + RetainPathOptionNotBool = "VerifyResult.RetainPathOptionNotBool", + MaxDepthOptionNotNumber = "VerifyResult.MaxDepthOptionNotNumber", + MaxDepthOptionIsNegative = "VerifyResult.MaxDepthOptionIsNegative", + + // SET / ADD + ValueIsMissing = "VerifyResult.ValueIsMissing", + ValueMustBeArray = "VerifyResult.ValueMustBeArray", + ValueAndPathArraysMustBeSameLength = "VerifyResult.ValueAndPathArraysMustBeSameLength", + ValueIsNotTupleArray = "VerifyResult.ValueIsNotTupleArray", + + // DELETE + AllowPartialOptionNotBool = "VerifyResult.AllowPartialOptionNotBool", + + // SUPPORTED_DM + OptionsMustBeBool = "VerifyResult.OptionsMustBeBool", + + // INSTANCES + FirstLevelOnlyNotBool = "VerifyResult.FirstLevelOnlyNotBool", + + // PROTO + ProtoNotString = "VerifyResult.ProtoNotString", +} + export type GetNestedReturn = | UspProperty | UspPropertyList @@ -162,7 +194,11 @@ export type SetCommand = (( | (JSValue | JSValue[] | InputRecord) | (JSValue | JSValue[] | InputRecord)[], options: { raw: true } & SetCommandOptions - ) => Promise<SetRawResponse>); + ) => Promise<SetRawResponse>) & + (( + values: [string, JSValue | JSValue[] | InputRecord][], + options?: SetCommandOptions + ) => Promise<SetResponse>); export type AddCommand = (( path: string | string[], value?: @@ -180,7 +216,11 @@ export type AddCommand = (( | null | undefined, options: { raw: true } - ) => Promise<AddRawResponse>); + ) => Promise<AddRawResponse>) & + (( + values: [string, JSValue | JSValue[] | InputRecord][], + options?: { raw?: boolean } + ) => Promise<AddResponse>); export type DelCommand = ( path: string | string[], allowPartial?: boolean @@ -452,6 +492,11 @@ export interface USP { ["Device.NAT.PortMapping.1.", "Device.NAT.PortMapping.2."], [{ Description: "cat-1" }, { Description: "cat-2" }] ) + * // or + * await device.set([ + ["Device.NAT.PortMapping.1.Description", "desc 1"], + ["Device.NAT.PortMapping.2.Description", "desc 2"], + ]); * ``` */ set: SetCommand; @@ -502,6 +547,10 @@ export interface USP { ["Device.NAT.PortMapping.", "Device.NAT.PortMapping."], [{ Description: "cpe-1" }, { Description: "cpe-2" }] ); + * await device.add([ + ["Device.NAT.PortMapping.", { Description: "bbbb" }], + ["Device.NAT.PortMapping.", { Description: "bbbb" }], + ]); * ``` */ add: AddCommand; @@ -688,7 +737,7 @@ export type HostConnectionOptions = { export type CertType = string | string[] | Buffer | Buffer[]; -export type USPVersion = typeof knownUSPVersions[number]; +export type USPVersion = (typeof knownUSPVersions)[number]; export interface MainConnectionOptions { username: string; @@ -831,6 +880,7 @@ export type BuildConnectionFn = (connectConfig: { connectClient: ConnectClientFn; decodeID: DecodeIDFn; loadProtobuf: LoadProtobufFn; + errors?: Record<VerifyResult, string>; }) => Connect; export type USPSession = { diff --git a/src/util.ts b/src/util.ts index 318d87e..a0e2584 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,10 @@ import { search } from "./commands/util"; -import { CallbackOptions, CommandType, OnCallback, OnIdent, SubscriptionCallback } from "./types"; +import { + CallbackOptions, + CommandType, + OnCallback, + OnIdent, +} from "./types"; /** * Makes a router for storing resolve/reject for a message @@ -23,10 +28,7 @@ const isRegExp = (v: OnIdent): v is RegExp => typeof v === "object"; const satisfies = (id: OnIdent, matches: string): boolean => isRegExp(id) ? matches.match(id) !== null : id === matches; export const makeCallbackRouter = () => { - const routes = new Map< - string, - { callback: OnCallback; ident: OnIdent } - >(); + const routes = new Map<string, { callback: OnCallback; ident: OnIdent }>(); return { get: (id: string): OnCallback[] => { return Array.from(routes.values()) @@ -40,10 +42,26 @@ export const makeCallbackRouter = () => { }; }; -export const parseCallbackOptions = (msg: Record<string, any>): CallbackOptions | null => { - const sendResp = search(msg, "sendResp") - if (typeof sendResp === "boolean") return { sendResp } - return null -} +export const parseCallbackOptions = ( + msg: Record<string, any> +): CallbackOptions | null => { + const sendResp = search(msg, "sendResp"); + if (typeof sendResp === "boolean") return { sendResp }; + return null; +}; export const knownUSPVersions = ["1.0", "1.1", "1.2"] as const; + +export const parseSetArgs = (value, initialPath) => { + if ( + Array.isArray(initialPath) && + initialPath.every((v) => Array.isArray(v) && v.length === 2) && + value === undefined + ) { + return [initialPath.map((v) => v[0]), initialPath.map((v) => v[1])]; + } + + const paths = Array.isArray(initialPath) ? initialPath : [initialPath]; + const values = value ? (Array.isArray(value) ? value : [value]) : []; + return [paths, values]; +}; diff --git a/src/verify.ts b/src/verify.ts new file mode 100644 index 0000000..b3363d6 --- /dev/null +++ b/src/verify.ts @@ -0,0 +1,148 @@ +import { defaultMissingErrorMessage } from "./errors"; +import { VerifyResult, CommandType, VerifierFunc } from "./types"; + +const isPresent = (item) => item !== undefined; +const isBool = (item) => typeof item === "boolean"; +const isNumber = (item) => typeof item === "number"; +const isString = (item) => typeof item === "string"; +const isArray = (item) => Array.isArray(item); +const isTupleArray = (item) => + Array.isArray(item) && + item.length > 0 && + item.every((it) => isArray(it) && it.length === 2); + +const checkGetOptions: VerifierFunc = ({ options }) => { + if (!isPresent(options)) return VerifyResult.NoError; + if (isPresent(options.raw) && !isBool(options.raw)) + return VerifyResult.RawOptionNotBool; + if (isPresent(options.retainPath) && !isBool(options.retainPath)) + return VerifyResult.RetainPathOptionNotBool; + if (isPresent(options.max_depth)) { + if (!isNumber(options.max_depth)) + return VerifyResult.MaxDepthOptionNotNumber; + if (options.max_depth < 0) return VerifyResult.MaxDepthOptionIsNegative; + } + + return VerifyResult.NoError; +}; + +const checkSetValue: VerifierFunc = ({ path, value }) => { + if (isTupleArray(path)) return VerifyResult.NoError + if (isArray(path)) { + if (!isPresent(value) && !isTupleArray(path)) + return VerifyResult.ValueIsNotTupleArray; + if (!isArray(value)) return VerifyResult.ValueMustBeArray; + if (path.length !== value.length) + return VerifyResult.ValueAndPathArraysMustBeSameLength; + } + return value !== undefined && value !== null + ? VerifyResult.NoError + : VerifyResult.ValueIsMissing; +}; + +const checkAddValue: VerifierFunc = ({ path, value }) => { + if (isTupleArray(path)) return VerifyResult.NoError + if (isArray(path)) { + if (!isPresent(value)) { + if (!isTupleArray(path)) return VerifyResult.ValueIsNotTupleArray; + else return VerifyResult.NoError; + } + if (!isArray(value)) return VerifyResult.ValueMustBeArray; + if (path.length !== value.length) + return VerifyResult.ValueAndPathArraysMustBeSameLength; + } + return VerifyResult.NoError; +}; + +const checkSetOptions: VerifierFunc = ({ options }) => { + if (!isPresent(options)) return VerifyResult.NoError; + if (isPresent(options.raw) && !isBool(options.raw)) + return VerifyResult.RawOptionNotBool; + + return VerifyResult.NoError; +}; + +const checkDelOptions: VerifierFunc = ({ allowPartial }) => { + if (!isPresent(allowPartial)) return VerifyResult.NoError; + if (!isBool(allowPartial)) return VerifyResult.AllowPartialOptionNotBool; + + return VerifyResult.NoError; +}; + +const checkSupportedDMOptions: VerifierFunc = ({ opts }) => { + if (!isPresent(opts)) return VerifyResult.NoError; + const allOptsAreBool = Object.values(opts).every(isBool); + return allOptsAreBool ? VerifyResult.NoError : VerifyResult.OptionsMustBeBool; +}; + +const checkInstancesOptions: VerifierFunc = ({ opts }) => { + if (!isPresent(opts)) return VerifyResult.NoError; + if (isPresent(opts.firstLevelOnly) && !isBool(opts.firstLevelOnly)) + return VerifyResult.FirstLevelOnlyNotBool; + return VerifyResult.NoError; +}; + +const checkProto: VerifierFunc = ({ proto }) => { + if (isPresent(proto) && !isString(proto)) return VerifyResult.ProtoNotString; + return VerifyResult.NoError; +}; + +const checkSetPaths: VerifierFunc = ({ path, value }) => { + if (!isPresent(path)) return VerifyResult.PathsMissing + if (!isPresent(value) && isArray(path) && !isTupleArray(path)) + return VerifyResult.ValueIsNotTupleArray; + return isString(path) || + (isArray(path) && + path.length > 0 && + path.every((path) => typeof path === "string" || isArray(path))) + ? VerifyResult.NoError + : VerifyResult.PathsNotStringOrStringArray; +}; + +const checkPaths: VerifierFunc = ({ + paths: initalPaths, + path: initialPath, +}) => { + const paths = initalPaths || initialPath; + return !isPresent(paths) + ? VerifyResult.PathsMissing + : isString(paths) || + (isArray(paths) && + paths.length > 0 && + paths.every((path) => typeof path === "string")) + ? VerifyResult.NoError + : VerifyResult.PathsNotStringOrStringArray; +}; + +const argsVerifiers: Partial<Record<CommandType, VerifierFunc[]>> = { + GET: [checkPaths, checkGetOptions], + SET: [checkSetPaths, checkSetValue, checkSetOptions], + ADD: [checkSetPaths, checkAddValue, checkSetOptions], + DELETE: [checkPaths, checkDelOptions], + OPERATE: [checkPaths], + GET_SUPPORTED_DM: [checkPaths, checkSupportedDMOptions], + GET_INSTANCES: [checkPaths, checkInstancesOptions], + GET_SUPPORTED_PROTO: [checkProto], +}; + +const translateError = ( + errors: Record<VerifyResult, string>, + verifyResult: VerifyResult +): Error => new Error(errors?.[verifyResult] || defaultMissingErrorMessage); + +export const makeVerify = + (errors: Record<VerifyResult, string>) => + (command: CommandType, args: Record<string, any>): Error | null => { + const verifiers = argsVerifiers[command]; + if (!verifiers) return null; + + const results = verifiers.map((verifier) => verifier(args)); + const finalResult = + results.find((value) => value !== VerifyResult.NoError) || + VerifyResult.NoError; + return finalResult === VerifyResult.NoError + ? null + : translateError(errors, finalResult); + }; + +export default makeVerify; diff --git a/tests/integration/config.json b/tests/integration/config.json index 43bab89..163c186 100644 --- a/tests/integration/config.json +++ b/tests/integration/config.json @@ -1,6 +1,6 @@ { "ws": { - "host": "localhost", + "host": "192.168.2.1", "username": "admin", "password": "admin", "port": 9001, diff --git a/tests/integration/errors.test.ts b/tests/integration/errors.test.ts new file mode 100644 index 0000000..1c70ce1 --- /dev/null +++ b/tests/integration/errors.test.ts @@ -0,0 +1,157 @@ +import assert from "assert"; +import connect from "../../src"; +import { ConnectionOptions } from "../../src/types"; +import config from "./config.json"; + +const expectError = async (func, errorMessage) => { + try { + await func(); + } catch (error) { + assert.strictEqual(typeof error, "object"); + assert.strictEqual(error.message, errorMessage); + } +}; + +describe("Test errors", function () { + this.timeout(10000); + + let device: any = null; + + before(async () => { + device = await connect(config.ws as ConnectionOptions); + }); + + after(async () => { + await device.disconnect(); + }); + + it("no error", async () => { + const resp = await device.get([ + "Device.Time.CurrentLocalTime", + "Device.Time.LocalTimeZone", + ]); + assert.strictEqual(Array.isArray(resp), true); + }); + + it("get errors", async () => { + await expectError(() => device.get(), "'paths' argument is missing"); + + await expectError( + () => device.get(3), + "'paths' argument has to be a string or a string array" + ); + await expectError( + () => device.get([]), + "'paths' argument has to be a string or a string array" + ); + await expectError( + () => device.get([1, 2, 3]), + "'paths' argument has to be a string or a string array" + ); + + await expectError( + () => device.get("Device.", { raw: 13 }), + "'raw' option has to be a boolean" + ); + + await expectError( + () => device.get("Device.", { retainPath: 13 }), + "'retainPath' option has to be a boolean" + ); + + await expectError( + () => device.get("Device.", { max_depth: false }), + "'max_depth' option has to be a positive number" + ); + await expectError( + () => device.get("Device.", { max_depth: -3 }), + "'max_depth' option has to be a positive number" + ); + }); + + it("set/add errors", async () => { + await expectError( + () => device.set("Device.NAT.PortMapping.1.Description"), + "'value' argument is required" + ); + + await expectError( + () => device.set(["Device.NAT.PortMapping.1.Description"], "Desc"), + "'value' argument must be an array, when 'path' is an array" + ); + await expectError( + () => device.add(["Device.NAT.PortMapping."], { Description: "Desc" }), + "'value' argument must be an array, when 'path' is an array" + ); + + await expectError( + () => + device.set( + ["Device.NAT.PortMapping.1.Description"], + ["Desc 1", "Desc 2"] + ), + "'path' and 'value' arrays must have the same length" + ); + await expectError( + () => + device.add( + ["Device.NAT.PortMapping."], + [{ Description: "Desc 1" }, { Description: "Desc 2" }] + ), + "'path' and 'value' arrays must have the same length" + ); + + await expectError( + () => + device.set([ + ["Device.NAT.PortMapping.1.Description", "desc 1", "desc 2"], + ]), + "'path' argument must be an array of tuples" + ); + await expectError( + () => + device.add([ + [ + "Device.NAT.PortMapping.", + { Description: "Desc 1" }, + { Description: "Desc 2" }, + ], + ]), + "'path' argument must be an array of tuples" + ); + }); + + it("delete errors", async () => { + await expectError( + () => device.del("Device.NAT.PortMapping.1.", 13), + "'allowPartial' option has to be a boolean" + ); + }); + + it("supportedDM errors", async () => { + await expectError( + () => + device.supportedDM("Device.NAT.PortMapping.1.", { + firstLevelOnly: true, + returnCommands: true, + returnEvents: true, + returnParams: 13, + }), + "all options have to be boolean" + ); + }); + + it("instances errors", async () => { + await expectError( + () => device.instances("Device.NAT.PortMapping.", 13), + "'firstLevelOnly' has to be a boolean" + ); + }); + + it("proto errors", async () => { + await expectError( + () => device.supportedProto(13), + "'proto' argument has to be a string" + ); + }); +}); diff --git a/tests/integration/index.test.ts b/tests/integration/index.test.ts index ff87274..36b2cfc 100644 --- a/tests/integration/index.test.ts +++ b/tests/integration/index.test.ts @@ -97,6 +97,23 @@ describe("Test general API", function () { await device.set("Device.WiFi.Radio.1.Alias", alias); }); + it("set multiple with value", async () => { + await device.set( + [ + "Device.NAT.PortMapping.1.Description", + "Device.NAT.PortMapping.2.Description", + ], + ["desc 1", "desc 2"] + ); + }); + + it("set multiple with value (alternate usage)", async () => { + await device.set([ + ["Device.NAT.PortMapping.1.Description", "desc 1"], + ["Device.NAT.PortMapping.2.Description", "desc 2"], + ]); + }); + it("set with object", async () => { const Alias = await device.get("Device.WiFi.Radio.1.Alias"); await device.set("Device.WiFi.Radio.1.", { Alias }); -- GitLab