diff --git a/.impt.test b/.impt.test new file mode 100644 index 0000000..b020954 --- /dev/null +++ b/.impt.test @@ -0,0 +1,12 @@ +{ + "deviceGroupId": "4e784e30-183f-a3fa-d9b9-6870c4749f6c", + "timeout": 30, + "stopOnFail": false, + "allowDisconnect": false, + "builderCache": false, + "testFiles": [ + "*.test.nut", + "tests/**/*.test.nut" + ], + "agentFile": "ThingWorx.agent.lib.nut" +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ee09ef5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: node_js + +node_js: +- 'node' + +before_script: +- npm i -g imp-central-impt@latest +- impt auth login --local --lk ${EI_LOGIN_KEY} + +script: +- impt test run diff --git a/Examples/DataSender.agent.nut b/Examples/DataSender.agent.nut new file mode 100644 index 0000000..273ec1b --- /dev/null +++ b/Examples/DataSender.agent.nut @@ -0,0 +1,138 @@ +// MIT License +// +// Copyright 2018 Electric Imp +// +// SPDX-License-Identifier: MIT +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +#require "ThingWorx.agent.lib.nut:1.0.0" + +// ThingWorx library example. +// Creates a ThingWorx Thing with two predefined Properties if it does not exist +// (thing name is specified as constructor argument) and periodically updates the properties values. +// Properties contain integer incremental value, converted to string and data measurement time +// in seconds since the epoch. +// Properties values are updated every 10 seconds. + +const UPDATE_DATA_PERIOD = 10.0; + +const THING_NAME = "test_thing"; +const THING_PROPERTY_DATA_NAME = "data"; +const THING_PROPERTY_DATA_TYPE = "STRING"; +const THING_PROPERTY_MEASURE_TIME_NAME = "measure_time"; +const THING_PROPERTY_MEASURE_TIME_TYPE = "INTEGER"; + +class DataSender { + _counter = 0; + _thingWorxClient = null; + _thingName = null; + + constructor(endpoint, appKey, thingName) { + _thingWorxClient = ThingWorx(endpoint, appKey); + _thingName = thingName; + } + + // Creates ThingWorx client, creates Thing if it doesn't exist and starts properties values update + function start() { + _thingWorxClient.existThing(_thingName, function (error, exist) { + if (error) { + server.error("ThingWorx existThing failed: " + error.details); + } else if (!exist) { + createThing(updatePropertiesValues); + } else { + updatePropertiesValues(); + } + }.bindenv(this)); + } + + // Creates ThingWorx Thing + function createThing(callback) { + _thingWorxClient.createThing(_thingName, null, function (error) { + if (error) { + server.error("ThingWorx createThing failed: " + error.details); + } else { + createThingProperties(callback); + } + }.bindenv(this)); + } + + // Creates ThingWorx Thing Properties + function createThingProperties(callback) { + createThingProperty(THING_PROPERTY_DATA_NAME, THING_PROPERTY_DATA_TYPE, function () { + createThingProperty(THING_PROPERTY_MEASURE_TIME_NAME, THING_PROPERTY_MEASURE_TIME_TYPE, callback); + }.bindenv(this)); + } + + function createThingProperty(propertyName, propertyType, callback) { + _thingWorxClient.createThingProperty(_thingName, propertyName, propertyType, function (error) { + if (error) { + server.error("ThingWorx createThingProperty failed: " + error.details); + } else { + callback(); + } + }.bindenv(this)); + } + + // Returns a data to be set for ThingWorx Properties + function getData() { + _counter++; + local result = {}; + result[THING_PROPERTY_DATA_NAME] <- _counter.tostring(); + result[THING_PROPERTY_MEASURE_TIME_NAME] <- time(); + return result; + } + + // Periodically updates Properties values + function updatePropertiesValues() { + local values = getData(); + setPropertyValue(THING_PROPERTY_DATA_NAME, values[THING_PROPERTY_DATA_NAME], function() { + setPropertyValue(THING_PROPERTY_MEASURE_TIME_NAME, values[THING_PROPERTY_MEASURE_TIME_NAME], function() { + server.log("Property values updated successfully: " + http.jsonencode(values)); + }.bindenv(this)); + }.bindenv(this)); + + imp.wakeup(UPDATE_DATA_PERIOD, function () { + updatePropertiesValues(); + }.bindenv(this)); + } + + // Updates value of the specified ThingWorx property + function setPropertyValue(propertyName, propertyValue, callback) { + _thingWorxClient.setPropertyValue(_thingName, propertyName, propertyValue, function (error) { + if (error) { + server.error("ThingWorx setPropertyValue failed: " + error.details); + } else { + callback(); + } + }.bindenv(this)); + } +} + +// RUNTIME +// --------------------------------------------------------------------------------- + +// ThingWorx constants +// --------------------------------------------------------------------------------- +const THING_WORX_ENDPOINT = ""; +const THING_WORX_APPLICATION_KEY = ""; + +// Start application +dataSender <- DataSender(THING_WORX_ENDPOINT, THING_WORX_APPLICATION_KEY, THING_NAME); +dataSender.start(); diff --git a/Examples/README.md b/Examples/README.md new file mode 100644 index 0000000..f2c54d2 --- /dev/null +++ b/Examples/README.md @@ -0,0 +1,61 @@ +# ThingWorx Examples # + +This document describes the example application provided with the [ThingWorx library](../README.md). + +## DataSender Example ## + +This example creates a ThingWorx Thing named `test_thing` (if it does not exist already) with two predefined Properties: + +- *data* — An integer value, converted to string, which starts at 1 and increases by 1 with every update. It restarts from 1 every time the example is restarted. +- *measure_time* — An integer value, which is the time in seconds since the epoch. + +The application updates the Properties values every 10 seconds. + +## Setup and Run ## + +### ThingWorx Evaluation Server Configuration ### + +- Login to the [ThingWorx Developer Portal](https://developer.thingworx.com/login) in your web browser. +- Click the **Evaluation Server** icon in the top right corner. **Note** The initial provisioning of Evaluation Server for a new account usually takes between 3-5 minutes. +- Copy the **Hostname** value from the pop-up and paste it into a plain text document or equivalent. This will be used as the value of the *THING_WORX_ENDPOINT* constant in the imp agent code: +![LaunchThingWorx](../png/LaunchThingWorx.png?raw=true) +- Click **Launch**. You will be redirected to the ThingWorx Composer page (usually a new browser tab) +- In the **ThingWorx Composer** page’s **Home** tab, click **+** under **Application Keys** in the **SECURITY** section: +![AddAppKey](../png/AddAppKey.png?raw=true) +- Enter any Application Key **Name**, eg. `testAppKey`. +- Click **Search** in the **User Name Reference** field under **General Information**, and choose the **Administrator** user: +![AppKeyUser](../png/AppKeyUser.png?raw=true) +- Choose a date and time for **Expiration Date** field. +- Click **Done** then click **Save**: +![AppKeyExpirationDate](../png/AppKeyExpirationDate.png?raw=true) +- On the **General Information** page select and copy the **keyId** field and paste into a plain text document or equivalent. This will be used as the value of the *THING_WORX_APPLICATION_KEY* constant in the imp agent code: +![AppKeyId](../png/AppKeyId.png?raw=true) + +### Setting Up and Running the Application ### + +- In [Electric Imp’s impCentral™](https://impcentral.electricimp.com) create a Product and Development Device Group. +- Assign a development device to the newly created Device Group. +- Open the code editor for the newly created Device Group. +- Copy the [DataSender source code](./DataSender.agent.nut) and paste it into the code editor as the agent code. +- Set the *THING_WORX_ENDPOINT* constant in the agent example code to the value of Evaluation Server Hostname you retrieved and saved above. Ensure it is prefixed with `https://`. The value should look like `"https://PP-1802281448E8.Devportal.Ptc.Io"`. +- Set the *THING_WORX_APPLICATION_KEY* constant in the agent example code to the value of the Application Key ID you retrieved and saved above: +![SetThingWorxConsts](../png/SetThingWorxConsts.png?raw=true) +- Click **Build and Force Restart**. +- Use the code editor’s log pane to confirm that data is being sent successfully: +![DataSenderLogs](../png/DataSenderLogs.png?raw=true) + +### Notes ### + +- The hosted ThingWorx Evaluation Server is stopped after three hours of inactivity and, typically, it takes about one minute to start it again. +- You need to ensure the Server is started before running the example. To start the Server: + - Click the **Evaluation Server** icon in the top right corner of the ThingWorx Developer Portal. + - Check the Server status in the pop-up. If it is **Stopped**, click **Start**. + +### Monitor the Properties Values in ThingWorx ### + +- In the **ThingWorx Composer** page’s **Home** tab, click **Things** in the **MODELING** section. +- Click *test_thing* in the **Things** table: +![ThingsTable](../png/Things.png?raw=true) +- In the **test_thing** tab, click **Properties** in the **ENTITY INFORMATION** section. +- Ensure the Properties table contains **data** and **measure_time** Properties, and that their values are updated periodically if **Values** column’s refresh button is clicked: +![ThingProperties](../png/ThingProperties.png?raw=true) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..044c214 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Electric Imp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index e69de29..134958a 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,158 @@ +# ThingWorx # + +This library allows your agent code to work with the [ThingWorx platform](https://developer.thingworx.com/) via the [ThingWorx REST API](https://developer.thingworx.com/resources/guides/thingworx-rest-api-quickstart). + +This version of the library supports the following functionality: + +- Access ThingWorx platform (verified with [PTC hosted instance](https://www.ptc.com/en/products/iot/thingworx-platform) only). +- Thing creation and deletion. +- Thing Property creation. +- Setting a value of Thing Property. + +**To add this library to your project, add** `#require "ThingWorx.agent.lib.nut:1.0.0"` **to the top of your agent code** + +## Library Usage ## + +### Prerequisites ### + +Before using the library you need to have: + +- An endpoint of your ThingWorx platform instance (it may look like `https://PP-1802281448E8.Devportal.Ptc.Io`). +- A [ThingWorx Application Key](https://developer.thingworx.com/resources/guides/thingworx-rest-api-quickstart/create-appkey)). + +### Callbacks ### + +All requests that are made to the ThingWorx platform occur asynchronously. Every method that sends a request has an optional parameter which takes a callback function that will be executed when the operation is completed, whether successfully or not. The callback’s parameters are listed in the corresponding method description, but every callback has at least one parameter, *error*. If *error* is `null`, the operation has been executed successfully. Otherwise, *error* is an instance of the [ThingWorx.Error](#thingworxerror-class) class and contains the details of the error. + +Some methods require callbacks to be specified, others need only be passed a callback if you wish. + +## ThingWorx Class ## + +### Constructor: ThingWorx(*endpoint, appKey*) ### + +This method returns a new ThingWorx instance. + +| Parameter | Data Type | Required? | Description | +| --- | --- | --- | --- | +| *endpoint* | String | Yes | ThingWorx platform endpoint. Must include the scheme, eg. `"https://PP-1802281448E8.Devportal.Ptc.Io"` | +| *appKey* | String | Yes | ThingWorx Application Key. For more information, please see [the ThingWorx documentation](https://developer.thingworx.com/resources/guides/thingworx-rest-api-quickstart/create-appkey) | + +#### Example #### + +``` +#require "ThingWorx.agent.lib.nut:1.0.0" + +const MY_PLATFORM_ENDPOINT = ""; +const MY_APP_KEY = ""; + +tw <- ThingWorx(MY_PLATFORM_ENDPOINT, MY_APP_KEY); +``` + +### createThing(*thingName[, thingTemplateName][, callback]*) ### + +This method creates a new Thing, enables and restarts it. For more information, please see [the ThingWorx documentation](https://developer.thingworx.com/resources/guides/thingworx-rest-api-quickstart/use-rest-api-create-new-thing). + +| Parameter | Data Type | Required? | Description | +| --- | --- | --- | --- | +| *thingName* | String | Yes | Name of the new Thing. Must be unique across the ThingWorx platform instance | +| *thingTemplateName* | String | Optional | A Thing Template which may be used for the Thing creation. If not specified, the standard ThingWorx `"GenericThing"` template is used. For more information, please see [the ThingWorx documentation](https://developer.thingworx.com/resources/guides/thingworx-foundation-quickstart/thing-template) | +| *callback* | Function | Optional | Executed once the operation is completed | + +This method returns nothing. The result of the operation may be obtained via the *callback* function, which has the following parameter: + +| Parameter | Data Type | Description | +| --- | --- | --- | +| *error* | [ThingWorx.Error](#thingworxerror-class) | Error details, or `null` if the operation succeeds | + +### existThing(*thingName, callback*) ### + +This method checks if Thing with the specified name exists. + +| Parameter | Data Type | Required? | Description | +| --- | --- | --- | --- | +| *thingName* | String | Yes | Name of the Thing | +| *callback* | Function | Yes | Executed once the operation is completed | + +This method returns nothing. The result of the operation may be obtained via the *callback* function, which has the following parameters: + +| Parameter | Data Type | Description | +| --- | --- | --- | +| *error* | [ThingWorx.Error](#thingworxerror-class) | Error details, or `null` if the operation succeeds | +| *exist* | Boolean | `true` if the Thing exists, or `false` if the Thing does not exist or the operation fails | + +### deleteThing(*thingName[, callback]*) ### + +This method deletes Thing with the specified name. + +| Parameter | Data Type | Required? | Description | +| --- | --- | --- | --- | +| *thingName* | String | Yes | Name of the Thing | +| *callback* | Function | Optional | Executed once the operation is completed | + +This method returns nothing. The result of the operation may be obtained via the *callback* function, which has the following parameter: + +| Parameter | Data Type | Description | +| --- | --- | --- | +| *error* | [ThingWorx.Error](#thingworxerror-class) | Error details, or `null` if the operation succeeds | + +### createThingProperty(*thingName, propertyName, propertyType[, callback]*) ### + +This method creates a new Property of the specified Thing and restarts the Thing. For more information, please see [the ThingWorx documentation](https://developer.thingworx.com/resources/guides/thingworx-rest-api-quickstart/use-rest-api-add-property-thing). + +| Parameter | Data Type | Required? | Description | +| --- | --- | --- | --- | +| *thingName* | String | Yes | Name of the Thing | +| *propertyName* | String | Yes | Name of the new Property. Must be unique across the specified Thing | +| *propertyType* | String | Yes | Type of the new Property. Must be one of the types described in [the ThingWorx documentation](https://support.ptc.com/cs/help/thingworx_hc/thingworx_7.0_hc/index.jspx?id=ThingProperties&action=show) | +| *callback* | Function | Optional | Executed once the operation is completed | + +This method returns nothing. The result of the operation may be obtained via the *callback* function, which has the following parameter: + +| Parameter | Data Type | Description | +| --- | --- | --- | +| *error* | [ThingWorx.Error](#thingworxerror-class) | Error details, or `null` if the operation succeeds | + +### setPropertyValue(*thingName, propertyName, propertyValue[, callback]*) ### + +This method sets a new value of the specified Property. For more information, please see [the ThingWorx documentation](https://developer.thingworx.com/resources/guides/thingworx-rest-api-quickstart/use-rest-api-set-property-value). + +| Parameter | Data Type | Required? | Description | +| --- | --- | --- | --- | +| *thingName* | String | Yes | Name of the Thing | +| *propertyName* | String | Yes | Name of the Property | +| *propertyValue* | Boolean, Integer, Float,
String, Key-Value Table,
Blob, Null | Yes | New value of the Property | +| *callback* | Function | Optional | Executed once the operation is completed | + +This method returns nothing. The result of the operation may be obtained via the *callback* function, which has the following parameter: + +| Parameter | Data Type | Description | +| --- | --- | --- | +| *error* | [ThingWorx.Error](#thingworxerror-class) | Error details, or `null` if the operation succeeds | + +### setDebug(*value*) ### + +This method enables (*value* is `true`) or disables (*value* is `false`) the library debug output (including error logging). It is disabled by default. The method returns nothing. + +## ThingWorx.Error Class ## + +This class represents an error returned by the library and has the following public properties: + +- *type* — The error type, which is one of the following *THING_WORX_ERROR* enum values: + - *LIBRARY_ERROR* — The library is wrongly initialized, a method is called with invalid argument(s), or an internal error has occurred. The error details can be found in the *details* property. Usually this indicates an issue during application development which should be fixed during debugging and therefore should not occur after the application has been deployed. + - *REQUEST_FAILED* — An HTTP request to the ThingWorx platform failed. The error details can be found in the *details*, *httpStatus* and *httpResponse* properties. This error may occur during the normal execution of an application. The application logic should process this error. + - *UNEXPECTED_RESPONSE* — An unexpected response from the ThingWorx platform. The error details can be found in the *details* and *httpResponse* properties. +- *details* — A string containing a human readable description of the error. +- *httpStatus* — An integer indicating the HTTP status code, or `null` if the *type* property is *LIBRARY_ERROR* +- *httpResponse* — A table of key-value strings holding the response body of the failed request, or `null` if the *type* property is *LIBRARY_ERROR*. + +## Examples ## + +Working examples are provided in the [Examples](./Examples) directory and described [here](./Examples/README.md). + +## Testing ## + +Tests for the library are provided in the [tests](./tests) directory and described [here](./tests/README.md). + +## License ## + +The ThingWorx library is licensed under the [MIT License](./LICENSE) diff --git a/ThingWorx.agent.lib.nut b/ThingWorx.agent.lib.nut new file mode 100644 index 0000000..cf86f0e --- /dev/null +++ b/ThingWorx.agent.lib.nut @@ -0,0 +1,390 @@ +// MIT License +// +// Copyright 2018 Electric Imp +// +// SPDX-License-Identifier: MIT +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +// This library allows your agent code to work with the ThingWorx platform (https://developer.thingworx.com/) +// via the ThingWorx REST API (https://developer.thingworx.com/resources/guides/thingworx-rest-api-quickstart). +// +// This version of the library supports the following functionality: +// - Access ThingWorx platform +// - Thing creation and deletion +// - Thing Property creation +// - Setting a value of Thing Property +// +// All requests that are made to the ThingWorx platform occur asynchronously. +// Every method that sends a request has an optional parameter which takes a callback function that will be +// executed when the operation is completed, whether successfully or not. +// Every callback has at least one parameter, error. If error is null, the operation has been executed successfully. +// Otherwise, error is an instance of the ThingWorx.Error class and contains the details of the error. + +// ThingWorx library operation error types +enum THING_WORX_ERROR { + // the library detects an error, e.g. the library is wrongly initialized or + // a method is called with invalid argument(s). The error details can be + // found in the error.details value + LIBRARY_ERROR, + // HTTP request to ThingWorx failed. The error details can be found in + // the error.httpStatus and error.httpResponse properties + REQUEST_FAILED, + // Unexpected response from ThingWorx. The error details can be found in + // the error.details and error.httpResponse properties + UNEXPECTED_RESPONSE +}; + +// Error details produced by the library +const THING_WORX_REQUEST_FAILED = "ThingWorx request failed with status code"; +const THING_WORX_NON_EMPTY_ARG = "Non empty argument required"; + +// Internal library constants +const _THING_WORX_CREATE_THING_PATH = "Resources/EntityServices/Services/CreateThing"; +const _THING_WORX_THING_PATH = "Things/%s"; +const _THING_WORX_ENABLE_THING_PATH = "Things/%s/Services/EnableThing"; +const _THING_WORX_RESTART_THING_PATH = "Things/%s/Services/RestartThing"; +const _THING_WORX_CREATE_THING_PROPERTY_PATH = "Things/%s/Services/AddPropertyDefinition"; +const _THING_WORX_SET_PROPERTY_VALUE_PATH = "Things/%s/Properties/%s"; +const _THING_WORX_THING_TEMPLATE_DEFAULT = "GenericThing"; + +class ThingWorx { + static VERSION = "1.0.0"; + + _endpoint = null; + _appKey = null; + _debug = null; + + // ThingWorx class constructor. + // + // Parameters: + // endpoint : string ThingWorx platform endpoint. Must include the scheme. + // Example: "https://PP-1802281448E8.Devportal.Ptc.Io". + // appKey : string ThingWorx Application Key. + // See https://developer.thingworx.com/resources/guides/thingworx-rest-api-quickstart/create-appkey + // + // Returns: ThingWorx instance created. + constructor(endpoint, appKey) { + _endpoint = endpoint; + _appKey = appKey; + } + + // Creates a new Thing, enables and restarts it. + // See https://developer.thingworx.com/resources/guides/thingworx-rest-api-quickstart/use-rest-api-create-new-thing + // + // Parameters: + // thingName : string Name of the new Thing. + // Must be unique across the ThingWorx platform instance. + // thingTemplateName : A Thing Template which may be used for the Thing creation. + // string See https://developer.thingworx.com/resources/guides/thingworx-foundation-quickstart/thing-template + // (optional) If not specified, the standard ThingWorx "GenericThing" + // template is used. + // callback : function Optional callback function executed once the operation + // (optional) is completed. + // The callback signature: + // callback(error), where + // error : Error details, + // ThingWorx.Error or null if the operation succeeds. + // + // Returns: Nothing + function createThing(thingName, thingTemplateName = null, callback = null) { + if (!_validateNonEmptyArg(thingName, "thingName", callback)) { + return; + } + if (typeof thingTemplateName == "function") { + callback = thingTemplateName; + thingTemplateName = null; + } + local body = { + "name" : thingName, + "thingTemplateName" : thingTemplateName ? thingTemplateName : _THING_WORX_THING_TEMPLATE_DEFAULT + }; + _processRequest("POST", _THING_WORX_CREATE_THING_PATH, body, function (error) { + if (error) { + _invokeDefaultCallback(callback, error); + } else { + _activateThing(thingName, callback); + } + }.bindenv(this)); + } + + // Checks if Thing with the specified name exists. + // + // Parameters: + // thingName : string Name of the Thing. + // callback : function Optional callback function executed once the operation + // (optional) is completed. + // The callback signature: + // callback(error), where + // error : Error details, + // ThingWorx.Error or null if the operation succeeds. + // exist : true if the Thing exists; + // boolean false if the Thing does not exist + // or the operation fails. + // + // Returns: Nothing + function existThing(thingName, callback) { + if (!_validateNonEmptyArg(thingName, "thingName", callback, _invokeExistCallback)) { + return; + } + _processRequest("GET", format(_THING_WORX_THING_PATH, thingName), null, function (error) { + local exist = error ? false : true; + if (error && error.type == THING_WORX_ERROR.REQUEST_FAILED && error.httpStatus == 404) { + error = null; + } + _invokeExistCallback(callback, error, exist); + }.bindenv(this)); + } + + // Deletes Thing with the specified name. + // + // Parameters: + // thingName : string Name of the Thing. + // callback : function Optional callback function executed once the operation + // (optional) is completed. + // The callback signature: + // callback(error), where + // error : Error details, + // ThingWorx.Error or null if the operation succeeds. + // + // Returns: Nothing + function deleteThing(thingName, callback = null) { + if (!_validateNonEmptyArg(thingName, "thingName", callback)) { + return; + } + _processRequest("DELETE", format(_THING_WORX_THING_PATH, thingName), null, callback); + } + + // This method creates a new Property of the specified Thing and restarts the Thing. + // See https://developer.thingworx.com/resources/guides/thingworx-rest-api-quickstart/use-rest-api-add-property-thing + // + // Parameters: + // thingName : string Name of the Thing. + // propertyName : string Name of the new Property. + // Must be unique across the specified Thing. + // propertyType : string Type of the new Property. + // One of the types described here: + // https://support.ptc.com/cs/help/thingworx_hc/thingworx_7.0_hc/index.jspx?id=ThingProperties&action=show + // callback : function Optional callback function executed once the operation + // (optional) is completed. + // The callback signature: + // callback(error), where + // error : Error details, + // ThingWorx.Error or null if the operation succeeds. + // + // Returns: Nothing + function createThingProperty(thingName, propertyName, propertyType, callback = null) { + if (!_validateNonEmptyArgs({ + "thingName" : thingName, + "propertyName" : propertyName, + "propertyType" : propertyType + }, callback)) { + return; + } + local body = { + "name" : propertyName, + "type": propertyType + }; + _processRequest("POST", format(_THING_WORX_CREATE_THING_PROPERTY_PATH, thingName), body, function (error) { + if (error) { + _invokeDefaultCallback(callback, error); + } else { + _restartThing(thingName, callback); + } + }.bindenv(this)); + } + + // This method sets a new value of the specified Property. + // See https://developer.thingworx.com/resources/guides/thingworx-rest-api-quickstart/use-rest-api-set-property-value + // + // Parameters: + // thingName : string Name of the Thing. + // propertyName : string Name of the Property. + // propertyValue : New value of the Property. + // boolean, integer, + // float, string, + // table or blob + // callback : function Optional callback function executed once the operation + // (optional) is completed. + // The callback signature: + // callback(error), where + // error : Error details, + // ThingWorx.Error or null if the operation succeeds. + // + // Returns: Nothing + function setPropertyValue(thingName, propertyName, propertyValue, callback = null) { + if (!_validateNonEmptyArgs({ "thingName" : thingName, "propertyName" : propertyName }, callback)) { + return; + } + local body = {}; + body[propertyName] <- typeof propertyValue == "blob" ? http.base64encode(propertyValue) : propertyValue; + _processRequest("PUT", format(_THING_WORX_SET_PROPERTY_VALUE_PATH, thingName, propertyName), body, callback); + } + + // Enables/disables the library debug output (including errors logging). + // Disabled by default. + // + // Parameters: + // value : boolean true to enable, false to disable + function setDebug(value) { + _debug = value; + } + + // -------------------- PRIVATE METHODS -------------------- // + + // Activates Thing: enables and restarts it. + function _activateThing(thingName, callback) { + _processRequest("POST", format(_THING_WORX_ENABLE_THING_PATH, thingName), null, function (error) { + if (error) { + _invokeDefaultCallback(callback, error); + } else { + _restartThing(thingName, callback); + } + }.bindenv(this)); + } + + // Restarts Thing. + function _restartThing(thingName, callback) { + _processRequest("POST", format(_THING_WORX_RESTART_THING_PATH, thingName), null, callback); + } + + // Sends an http request to ThingWorx. + function _processRequest(method, path, body, callback) { + if (!_validateNonEmptyArgs({ "endpoint" : _endpoint, "appKey" : _appKey }, callback)) { + return; + } + local url = format("%s/Thingworx/%s", _endpoint, path); + local encodedBody = http.jsonencode(body); + _logDebug(format("Doing the request: %s %s, body: %s", method, url, encodedBody)); + + local headers = { + "Content-Type" : "application/json", + "Accept" : "application/json", + "appKey" : _appKey + }; + local request = http.request(method, url, headers, encodedBody); + request.sendasync(function (response) { + _processResponse(response, callback); + }.bindenv(this)); + } + + // Processes http response from ThingWorx and executes callback if specified. + function _processResponse(response, callback) { + _logDebug(format("Response status: %d, body: %s", response.statuscode, response.body)); + local errType = null; + local errDetails = null; + local httpStatus = response.statuscode; + if (httpStatus < 200 || httpStatus >= 300) { + errType = THING_WORX_ERROR.REQUEST_FAILED; + errDetails = format("%s: %i", THING_WORX_REQUEST_FAILED, httpStatus); + try { + response.body = (response.body == "") ? {} : http.jsondecode(response.body); + } catch (e) { + _logError(e); + } + } + + local error = errType ? ThingWorx.Error(errType, errDetails, httpStatus, response.body) : null; + _invokeDefaultCallback(callback, error); + } + + // Validates the argument is not empty. Invokes callback with THING_WORX_ERROR.LIBRARY_ERROR if the check failed. + function _validateNonEmptyArg(arg, argName, callback, invokeCallback = null) { + if (arg == null || typeof arg == "string" && arg.len() == 0) { + local error = ThingWorx.Error( + THING_WORX_ERROR.LIBRARY_ERROR, + format("%s: %s", THING_WORX_NON_EMPTY_ARG, argName)); + if (!invokeCallback) { + invokeCallback = _invokeDefaultCallback; + } + invokeCallback(callback, error); + return false; + } + return true; + } + + // Validates the arguments are not empty. Invokes callback with THING_WORX_ERROR.LIBRARY_ERROR if the check failed. + function _validateNonEmptyArgs(args, callback, invokeCallback = null) { + foreach (argName, arg in args) { + if (!_validateNonEmptyArg(arg, argName, callback, invokeCallback)) { + return false; + } + } + return true; + } + + // Logs error occurred and invokes default callback with single error parameter. + function _invokeDefaultCallback(callback, error) { + if (error) { + _logError(error.details); + } + if (callback) { + imp.wakeup(0, function () { + callback(error); + }); + } + } + + // Invokes existThing method callback. + function _invokeExistCallback(callback, error, exist = false) { + if (callback) { + imp.wakeup(0, function () { + callback(error, exist); + }); + } + } + + // Logs error occurred during the library methods execution. + function _logError(message) { + if (_debug) { + server.error("[ThingWorx] " + message); + } + } + + // Logs debug messages occurred during the library methods execution. + function _logDebug(message) { + if (_debug) { + server.log("[ThingWorx] " + message); + } + } + + // Auxiliary class, represents error returned by the library. + Error = class { + // error type, one of the THING_WORX_ERROR enum values + type = null; + + // human readable details of the error (string) + details = null; + + // HTTP status code (integer), + // null if type is THING_WORX_ERROR.LIBRARY_ERROR + httpStatus = null; + + // Response body of the failed request (table), + // null if type is THING_WORX_ERROR.LIBRARY_ERROR + httpResponse = null; + + constructor(type, details, httpStatus = null, httpResponse = null) { + this.type = type; + this.details = details; + this.httpStatus = httpStatus; + this.httpResponse = httpResponse; + } + } +} diff --git a/png/AddAppKey.png b/png/AddAppKey.png new file mode 100644 index 0000000..38ea9a8 Binary files /dev/null and b/png/AddAppKey.png differ diff --git a/png/AppKeyExpirationDate.png b/png/AppKeyExpirationDate.png new file mode 100644 index 0000000..3bcf45e Binary files /dev/null and b/png/AppKeyExpirationDate.png differ diff --git a/png/AppKeyId.png b/png/AppKeyId.png new file mode 100644 index 0000000..db55541 Binary files /dev/null and b/png/AppKeyId.png differ diff --git a/png/AppKeyUser.png b/png/AppKeyUser.png new file mode 100644 index 0000000..6461e93 Binary files /dev/null and b/png/AppKeyUser.png differ diff --git a/png/DataSenderLogs.png b/png/DataSenderLogs.png new file mode 100644 index 0000000..5f5f260 Binary files /dev/null and b/png/DataSenderLogs.png differ diff --git a/png/LaunchThingWorx.png b/png/LaunchThingWorx.png new file mode 100644 index 0000000..e1f1e48 Binary files /dev/null and b/png/LaunchThingWorx.png differ diff --git a/png/SetThingWorxConsts.png b/png/SetThingWorxConsts.png new file mode 100644 index 0000000..c5ec2c1 Binary files /dev/null and b/png/SetThingWorxConsts.png differ diff --git a/png/ThingProperties.png b/png/ThingProperties.png new file mode 100644 index 0000000..6588a3c Binary files /dev/null and b/png/ThingProperties.png differ diff --git a/png/Things.png b/png/Things.png new file mode 100644 index 0000000..d81a4e8 Binary files /dev/null and b/png/Things.png differ diff --git a/tests/Properties.agent.test.nut b/tests/Properties.agent.test.nut new file mode 100644 index 0000000..b3f92cf --- /dev/null +++ b/tests/Properties.agent.test.nut @@ -0,0 +1,197 @@ +// MIT License +// +// Copyright 2018 Electric Imp +// +// SPDX-License-Identifier: MIT +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +const THING_WORX_ENDPOINT = "@{THING_WORX_ENDPOINT}"; +const THING_WORX_APPLICATION_KEY = "@{THING_WORX_APPLICATION_KEY}"; + +const TEST_THING_NAME = "__imptest_thing"; +const TEST_PROPERTY_STRING = "__imptest_property_string"; +const TEST_PROPERTY_INTEGER = "__imptest_property_integer"; +const TEST_PROPERTY_BOOLEAN = "__imptest_property_boolean"; +const TEST_PROPERTY_NUMBER = "__imptest_property_number"; +const TEST_PROPERTY_JSON = "__imptest_property_json"; +const TEST_PROPERTY_BLOB = "__imptest_property_blob"; + +// Test case for Thing property methods of ThingWorx library (createThingProperty, setPropertyValue) +class PropertiesTestCase extends ImpTestCase { + _thingWorxClient = null; + + function setUp() { + _thingWorxClient = ThingWorx(THING_WORX_ENDPOINT, THING_WORX_APPLICATION_KEY); + // clean up Things to be created, if exists + return tearDown(); + } + + function tearDown() { + return _deleteThing(); + } + + function testCreatePropertiesAndSetValues() { + return _createThing() + .then(function (value) { + return _createThingProperty(TEST_PROPERTY_STRING, "STRING"); + }.bindenv(this)) + .then(function (value) { + return _createThingProperty(TEST_PROPERTY_INTEGER, "INTEGER"); + }.bindenv(this)) + .then(function (value) { + return _createThingProperty(TEST_PROPERTY_BOOLEAN, "BOOLEAN"); + }.bindenv(this)) + .then(function (value) { + return _createThingProperty(TEST_PROPERTY_NUMBER, "NUMBER"); + }.bindenv(this)) + .then(function (value) { + return _createThingProperty(TEST_PROPERTY_JSON, "JSON"); + }.bindenv(this)) + .then(function (value) { + return _createThingProperty(TEST_PROPERTY_BLOB, "BLOB"); + }.bindenv(this)) + .then(function (value) { + return _setPropertyValue(TEST_PROPERTY_STRING, [ "abc", null, "" ]); + }.bindenv(this)) + .then(function (value) { + return _setPropertyValue(TEST_PROPERTY_INTEGER, [ 0, 10, -20 ]); + }.bindenv(this)) + .then(function (value) { + return _setPropertyValue(TEST_PROPERTY_BOOLEAN, [ true, false ]); + }.bindenv(this)) + .then(function (value) { + return _setPropertyValue(TEST_PROPERTY_NUMBER, [ 0, 2.7, -15.987 ]); + }.bindenv(this)) + .then(function (value) { + return _setPropertyValue(TEST_PROPERTY_JSON, [ { "p1" : "val1", "p2" : 123, "p3" : [ "a", "b" ], "p4" : true }, {} ]); + }.bindenv(this)) + .then(function (value) { + local blobVal = blob(); + blobVal.writestring("abcdefg"); + return _setPropertyValue(TEST_PROPERTY_BLOB, [ blobVal ]); + }.bindenv(this)) + .then(function (value) { + return _deleteThing(); + }.bindenv(this)) + .fail(function (reason) { + return Promise.reject(reason); + }.bindenv(this)); + } + + function testCreateSetPropertyWithWrongArgs() { + return _createThing() + .then(function (value) { + return _createPropertyWithWrongArgs("nonexistentThingName", TEST_PROPERTY_STRING, "STRING"); + }.bindenv(this)) + .then(function (value) { + return _createPropertyWithWrongArgs(TEST_THING_NAME, TEST_PROPERTY_STRING, "WRONG_PROPERTY_TYPE"); + }.bindenv(this)) + .then(function (value) { + return _createThingProperty(TEST_PROPERTY_INTEGER, "INTEGER"); + }.bindenv(this)) + .then(function (value) { + return _setPropertyValueWithWrongArgs("nonexistentThingName", TEST_PROPERTY_INTEGER, 123); + }.bindenv(this)) + .then(function (value) { + return _setPropertyValueWithWrongArgs(TEST_THING_NAME, "nonexistentPropertyName", 123); + }.bindenv(this)) + .then(function (value) { + return _setPropertyValueWithWrongArgs(TEST_THING_NAME, TEST_PROPERTY_INTEGER, "abc"); + }.bindenv(this)) + .then(function (value) { + return _deleteThing(); + }.bindenv(this)) + .fail(function (reason) { + return Promise.reject(reason); + }.bindenv(this)); + } + + function _createThing() { + return Promise(function (resolve, reject) { + _thingWorxClient.createThing(TEST_THING_NAME, function (error) { + if (error) { + return reject(error.details); + } + return resolve(); + }.bindenv(this)); + }.bindenv(this)); + } + + function _deleteThing() { + return Promise(function (resolve, reject) { + _thingWorxClient.deleteThing(TEST_THING_NAME, function (error) { + return resolve(); + }); + }.bindenv(this)); + } + + function _createThingProperty(propertyName, propertyType) { + return Promise(function (resolve, reject) { + _thingWorxClient.createThingProperty(TEST_THING_NAME, propertyName, propertyType, function (error) { + if (error) { + return reject("Unexpected createThingProperty error: " + error.details); + } + return resolve(); + }.bindenv(this)); + }.bindenv(this)); + } + + function _createPropertyWithWrongArgs(thingName, propertyName, propertyType) { + return Promise(function (resolve, reject) { + _thingWorxClient.createThingProperty(thingName, propertyName, propertyType, function (error) { + if (error && error.type == THING_WORX_ERROR.REQUEST_FAILED) { + return resolve(); + } + return reject("createThingProperty with wrong arguments executed successfully"); + }.bindenv(this)); + }.bindenv(this)); + } + + function _setPropertyValue(propertyName, propertyValues) { + local index = 0; + return Promise.loop( + function () { + return index++ < propertyValues.len(); + }.bindenv(this), + function () { + return Promise(function (resolve, reject) { + _thingWorxClient.setPropertyValue(TEST_THING_NAME, propertyName, propertyValues[index - 1], function (error) { + if (error) { + return reject(format("Unexpected setPropertyValue error for property %s: %s", + propertyName, error.details)); + } + return resolve(); + }.bindenv(this)); + }.bindenv(this)); + }.bindenv(this) + ); + } + + function _setPropertyValueWithWrongArgs(thingName, propertyName, propertyValue) { + return Promise(function (resolve, reject) { + _thingWorxClient.setPropertyValue(thingName, propertyName, propertyValue, function (error) { + if (error && error.type == THING_WORX_ERROR.REQUEST_FAILED) { + return resolve(); + } + return reject("setPropertyValue with wrong arguments executed successfully"); + }.bindenv(this)); + }.bindenv(this)); + } +} diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..22306cd --- /dev/null +++ b/tests/README.md @@ -0,0 +1,49 @@ +# Test Instructions + +The tests in the current directory are intended to check the behavior of the ThingWorx library. The current set of tests check: +- Thing manipulation methods of ThingWorx library: createThing(), deleteThing(), existThing(). +- Thing Property manipulation methods of ThingWorx library: createThingProperty(), setPropertyValue(). +- processing of wrong parameters passed into the library methods + +The tests are written for and should be used with [impt](https://github.com/electricimp/imp-central-impt). See [impt Testing Guide](https://github.com/electricimp/imp-central-impt/blob/master/TestingGuide.md) for the details of how to configure and run the tests. + +The tests for ThingWorx library require pre-setup described below. + +## Configure ThingWorx Evaluation Server + +- Login to [ThingWorx Developer Portal](https://developer.thingworx.com/login) in your web browser. +- Click **Evaluation Server** icon in the top right corner of the Developer Portal. **Note:** The initial provisioning of Evaluation Server for a new account usually takes between 3-5 minutes. +- Copy and save somewhere **Hostname** value from the pop up. This will be used as the value of the *THING_WORX_ENDPOINT* environment variable. +- Click **Launch**. You will be redirected to the ThingWorx Composer page (usually a new tab). +![LaunchThingWorx](../png/LaunchThingWorx.png?raw=true) +- In the **ThingWorx Composer** **Home** tab click **Application Keys "+"** button in the **SECURITY** menu. +![AddAppKey](../png/AddAppKey.png?raw=true) +- Enter any Application Key **Name**, eg. `testAppKey`. +- Click **Search** button in **User Name Reference** field and choose **Administrator** user. +![AppKeyUser](../png/AppKeyUser.png?raw=true) +- Choose date and time for **Expiration Date** field. +- Click **Done**. +- Click **Save**. +![AppKeyExpirationDate](../png/AppKeyExpirationDate.png?raw=true) +- In the **General Information** page of your Application Key select and copy and save somewhere **keyId** field. This will be used as the value of the *THING_WORX_APPLICATION_KEY* environment variable. +![AppKeyId](../png/AppKeyId.png?raw=true) + +**IMPORTANT:** the hosted ThingWorx Evaluation Server is stopped after 3 hours of inactivity and, typically, it takes about one minute to start it again. +You need to ensure the Server is started before running the tests. +To start the Server: +- Click **Evaluation Server** icon in the top right corner of the ThingWorx Developer Portal. +- Check the Server status in the pop up. If it is **Stopped**, click **Start** button. + +**IMPORTANT:** the hosted ThingWorx Evaluation Server is available for a new ordinary account for a limited time only (usually, for 30 days). + +## Set Environment Variables + +- Set *THING_WORX_ENDPOINT* environment variable to the value of Evaluation Server Hostname you retrieved and saved in the previous steps, prefixed by `https://`. The value should look like `https://PP-1802281448E8.Devportal.Ptc.Io`. +- Set *THING_WORX_APPLICATION_KEY* environment variable to the value of Application Key Id you retrieved and saved in the previous steps. +- For integration with [Travis](https://travis-ci.org) set *EI_LOGIN_KEY* environment variable to the valid impCentral login key. + +## Run Tests + +- See [impt Testing Guide](https://github.com/electricimp/imp-central-impt/blob/master/TestingGuide.md) for the details of how to configure and run the tests. +- Run [impt](https://github.com/electricimp/imp-central-impt) commands from the root directory of the lib. It contains a [default test configuration file](../.impt.test) which should be updated by *impt* commands for your testing environment (at least the Device Group must be updated). + diff --git a/tests/Things.agent.test.nut b/tests/Things.agent.test.nut new file mode 100644 index 0000000..c7bf5a0 --- /dev/null +++ b/tests/Things.agent.test.nut @@ -0,0 +1,143 @@ +// MIT License +// +// Copyright 2018 Electric Imp +// +// SPDX-License-Identifier: MIT +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +const THING_WORX_ENDPOINT = "@{THING_WORX_ENDPOINT}"; +const THING_WORX_APPLICATION_KEY = "@{THING_WORX_APPLICATION_KEY}"; + +const TEST_THING_NAME = "__imptest_thing"; +const TEST_THING_TEMPLATE_NAME = "__imptest_unknown_template"; + +// Test case for Thing methods of ThingWorx library (createThing, existThing, deleteThing) +class ThingsTestCase extends ImpTestCase { + _thingWorxClient = null; + + function setUp() { + _thingWorxClient = ThingWorx(THING_WORX_ENDPOINT, THING_WORX_APPLICATION_KEY); + // clean up Things to be created, if exists + return tearDown(); + } + + function tearDown() { + return Promise(function (resolve, reject) { + _thingWorxClient.deleteThing(TEST_THING_NAME, function (error) { + return resolve(); + }); + }.bindenv(this)); + } + + function testCreateExistDeleteThing() { + return Promise(function (resolve, reject) { + _thingWorxClient.createThing(TEST_THING_NAME, null, function (error) { + if (error) { + return reject("createThing failed: " + error.details); + } + _thingWorxClient.existThing(TEST_THING_NAME, function (error, exist) { + if (error) { + return reject("existThing failed: " + error.details); + } + if (!exist) { + return reject("Wrong exist value of existThing"); + } + _thingWorxClient.deleteThing(TEST_THING_NAME, function (error) { + if (error) { + return reject("deleteThing failed: " + error.details); + } + return resolve(); + }.bindenv(this)); + }.bindenv(this)); + }.bindenv(this)); + }.bindenv(this)); + } + + function testCreateDuplicatedThing() { + return Promise(function (resolve, reject) { + _thingWorxClient.createThing(TEST_THING_NAME, function (error) { + if (error) { + return reject("createThing failed: " + error.details); + } + _thingWorxClient.createThing(TEST_THING_NAME, function (error) { + if (!error) { + return reject("Duplicated things are created successfully"); + } + else if (error && error.type != THING_WORX_ERROR.REQUEST_FAILED) { + return reject("Wrong error type for create duplicated things"); + } + _thingWorxClient.existThing(TEST_THING_NAME, function (error, exist) { + if (error) { + return reject("existThing failed: " + error.details); + } + if (!exist) { + return reject("Wrong exist value of existThing"); + } + _thingWorxClient.deleteThing(TEST_THING_NAME, function (error) { + if (error) { + return reject("deleteThing failed: " + error.details); + } + return resolve(); + }.bindenv(this)); + }.bindenv(this)); + }.bindenv(this)); + }.bindenv(this)); + }.bindenv(this)); + } + + function testCreateThingWithUnknownTemplate() { + return Promise(function (resolve, reject) { + _thingWorxClient.createThing(TEST_THING_NAME, TEST_THING_TEMPLATE_NAME, function (error) { + if (!error) { + return reject("Thing with unknown template is created successfully"); + } + else if (error && error.type != THING_WORX_ERROR.REQUEST_FAILED) { + return reject("Wrong error type for create thing with unknown template"); + } + return resolve(); + }.bindenv(this)); + }.bindenv(this)); + } + + function testExistNonexistentThing() { + return Promise(function (resolve, reject) { + _thingWorxClient.existThing(TEST_THING_NAME, function (error, exist) { + if (error) { + return reject("Unexpected existThing error: " + error.details); + } + if (exist) { + return reject("Wrong exist value of existThing"); + } + return resolve(); + }.bindenv(this)); + }.bindenv(this)); + } + + function testDeleteNonexistentThing() { + return Promise(function (resolve, reject) { + _thingWorxClient.deleteThing(TEST_THING_NAME, function (error) { + if (!error || error.type != THING_WORX_ERROR.REQUEST_FAILED || error.httpStatus != 404) { + return reject("Wrong deleteThing error: " + (error ? error.details : "null")); + } + return resolve(); + }.bindenv(this)); + }.bindenv(this)); + } +} diff --git a/tests/WrongParams.agent.test.nut b/tests/WrongParams.agent.test.nut new file mode 100644 index 0000000..7069ec6 --- /dev/null +++ b/tests/WrongParams.agent.test.nut @@ -0,0 +1,236 @@ +// MIT License +// +// Copyright 2018 Electric Imp +// +// SPDX-License-Identifier: MIT +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. + +const THING_WORX_ENDPOINT = "@{THING_WORX_ENDPOINT}"; +const THING_WORX_APPLICATION_KEY = "@{THING_WORX_APPLICATION_KEY}"; + +const TEST_THING_NAME = "__imptest_thing"; +const TEST_PROPERTY_NAME = "__imptest_property"; + +// Test case for wrong parameters of ThingWorx library methods +class WrongParamsTestCase extends ImpTestCase { + + _thingWorxClient = null; + + function setUp() { + _thingWorxClient = ThingWorx(THING_WORX_ENDPOINT, THING_WORX_APPLICATION_KEY); + } + + function testWrongConstructorEndpoint() { + return Promise.all([ + _testWrongConstructorParams(null, THING_WORX_APPLICATION_KEY), + _testWrongConstructorParams("", THING_WORX_APPLICATION_KEY), + _testWrongConstructorParams(null, null), + _testWrongConstructorParams("", null), + ]); + } + + function testWrongConstructorAppKey() { + return Promise.all([ + _testWrongConstructorParams(THING_WORX_ENDPOINT, null), + _testWrongConstructorParams(THING_WORX_ENDPOINT, "") + ]); + } + + function testWrongCreateThingNameParams() { + return Promise.all([ + _testWrongCreateThing(null), + _testWrongCreateThing("") + ]); + } + + function testWrongExistThingNameParams() { + return Promise.all([ + _testWrongExistThing(null), + _testWrongExistThing("") + ]); + } + + function testWrongDeleteThingNameParams() { + return Promise.all([ + _testWrongDeleteThing(null), + _testWrongDeleteThing("") + ]); + } + + function testWrongCreateThingPropertyParams() { + return Promise.all([ + _testWrongCreateThingProperty(null, TEST_PROPERTY_NAME, "STRING"), + _testWrongCreateThingProperty("", TEST_PROPERTY_NAME, "INTEGER"), + _testWrongCreateThingProperty(TEST_THING_NAME, null, "BOOLEAN"), + _testWrongCreateThingProperty(TEST_THING_NAME, "", "NUMBER"), + _testWrongCreateThingProperty(TEST_THING_NAME, TEST_PROPERTY_NAME, null), + _testWrongCreateThingProperty(TEST_THING_NAME, TEST_PROPERTY_NAME, ""), + _testWrongCreateThingProperty(null, null, null), + _testWrongCreateThingProperty("", "", "") + ]); + } + + function testWrongSetPropertyValueParams() { + return Promise.all([ + _testWrongSetPropertyValue(null, TEST_PROPERTY_NAME), + _testWrongSetPropertyValue("", TEST_PROPERTY_NAME), + _testWrongSetPropertyValue(TEST_THING_NAME, null), + _testWrongSetPropertyValue(TEST_THING_NAME, ""), + _testWrongSetPropertyValue(null, null), + _testWrongSetPropertyValue("", "") + ]); + } + + function _testWrongConstructorParams(endpoint, appKey) { + local client = ThingWorx(endpoint, appKey); + local thingName = TEST_THING_NAME; + local propertyName = TEST_PROPERTY_NAME; + return Promise.all([ + Promise(function (resolve, reject) { + client.createThing( + thingName, + null, + function (error) { + if (!_isLibraryError(error)) { + return reject("Wrong initial param accepted in createThing"); + } + return resolve(""); + }.bindenv(this)); + }.bindenv(this)), + Promise(function (resolve, reject) { + client.existThing( + thingName, + function (error, exist) { + if (!_isLibraryError(error)) { + return reject("Wrong initial param accepted in existThing"); + } + return resolve(""); + }.bindenv(this)); + }.bindenv(this)), + Promise(function (resolve, reject) { + client.deleteThing( + thingName, + function (error) { + if (!_isLibraryError(error)) { + return reject("Wrong initial param accepted in deleteThing"); + } + return resolve(""); + }.bindenv(this)); + }.bindenv(this)), + Promise(function (resolve, reject) { + client.createThingProperty( + thingName, propertyName, "STRING", + function (error) { + if (!_isLibraryError(error)) { + return reject("Wrong initial param accepted in createThingProperty"); + } + return resolve(""); + }.bindenv(this)); + }.bindenv(this)), + Promise(function (resolve, reject) { + client.setPropertyValue( + thingName, propertyName, "abc", + function (error) { + if (!_isLibraryError(error)) { + return reject("Wrong initial param accepted in setPropertyValue"); + } + return resolve(""); + }.bindenv(this)); + }.bindenv(this)), + ]); + } + + function _testWrongCreateThing(thingName) { + return Promise(function (resolve, reject) { + _thingWorxClient.createThing( + thingName, + null, + function (error) { + if (!_isLibraryError(error)) { + return reject("Wrong param accepted in createThing"); + } + return resolve(""); + }.bindenv(this)); + }.bindenv(this)); + } + + function _testWrongExistThing(thingName) { + return Promise(function (resolve, reject) { + _thingWorxClient.existThing( + thingName, + function (error, exist) { + if (!_isLibraryError(error)) { + return reject("Wrong param accepted in existThing"); + } + if (exist) { + return reject("Wrong exist == true returned by existThing"); + } + return resolve(""); + }.bindenv(this)); + }.bindenv(this)); + } + + function _testWrongDeleteThing(thingName) { + return Promise(function (resolve, reject) { + _thingWorxClient.deleteThing( + thingName, + function (error) { + if (!_isLibraryError(error)) { + return reject("Wrong param accepted in deleteThing"); + } + return resolve(""); + }.bindenv(this)); + }.bindenv(this)); + } + + function _testWrongCreateThingProperty(thingName, propertyName, propertyType) { + return Promise(function (resolve, reject) { + _thingWorxClient.createThingProperty( + thingName, + propertyName, + propertyType, + function (error) { + if (!_isLibraryError(error)) { + return reject("Wrong param accepted in createThingProperty"); + } + return resolve(""); + }.bindenv(this)); + }.bindenv(this)); + } + + function _testWrongSetPropertyValue(thingName, propertyName) { + return Promise(function (resolve, reject) { + _thingWorxClient.setPropertyValue( + thingName, + propertyName, + "abc", + function (error) { + if (!_isLibraryError(error)) { + return reject("Wrong param accepted in setPropertyValue"); + } + return resolve(""); + }.bindenv(this)); + }.bindenv(this)); + } + + function _isLibraryError(error) { + return error && error.type == THING_WORX_ERROR.LIBRARY_ERROR; + } +}