Skip to content

Added a bot test mode #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules/
node_modules/
.env*
72 changes: 69 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ Therefore, the emulator mimics Cisco Spark REST API behavior for /rooms, /messag
The following features are NOT implemented: Messages Attachements, Room moderation, People LastActivity & Status and Pagination, as well as Teams, Automatic Invitations (if non Spark users are added to rooms) and Administration APIs,

The emulator can be used for several purposes:
- testing: on a developer laptop machine or on a CI environment to run a battery of test with no connection to Cisco Spark, and without 409 (Rate Limitations),
- testing: on a developer laptop machine or on a CI environment to run a battery of test with no connection to Cisco Spark, and without 409 (Rate Limitations)
- QA: run your code against a stable version of Cisco Spark (as the Cloud Service is incrementally upgraded, some bugs can be hard to replay. The emulator complies with a version of the API at a specific date, and helps reproduce an issue, or test for an upcoming feature (not released yet or toggled on)
- Bot Regression testing: Create a set of regression tests to ensure that for given user inputs you will get expected bot responses. See [Bot Testing Mode](#Bot-Testing-Mode) below.
- Training: backup plan in case of low or no connectivity location
- QA: simulate specific behaviors or errors from CiscoSpark (429, 500, 503)


## Give it a try from Heroku

The emulator is accessible at "https://mini-spark.herokuapp.com" via Heroku free dynos.
Expand Down Expand Up @@ -50,13 +50,79 @@ DEBUG="emulator*" node server.js
- POST /rooms create a new room
- POST /rooms create another room
- GET /rooms shows your rooms (2)
- DELETE /rooms:id deletes a room
- POST /memberships add a bot to the room
- POST /memberships 409 (conflict)
- GET /memberships show all your memberships
- GET /memberships?room= fetch memberships for you and your bot in current room
- POST /messages create a new message
- DELETE /memberships
- POST /messages create a new message. If the payload includes a personId, will create a new one on one space if one does not already exist. (personEmail not yet supported)
- POST /webhooks register a new webhook pointing to a target URL on your local machine, or on the internet
- DELETE /webhooks/:id deletes a webhook

## Bot Testing Mode
The emultator can be run in a special mode for bot testing by running with the environment variable BOT_UNDER_TEST set to the email of a user specified in tokens.json. When this environment variable is set additional middleware is loaded the performs that following functions:
- Inspect all incoming requests for an X-Bot-Responses header. When found it will intercept the response that normally would be sent to the caller of the API
- Inspect all incoming requests from the BOT_UNDER_TEST. When found the system checks to see if the bot request is correlated to any prior requests with the X-Bot-Response header. Generally correlation is done using the roomID.
- When correlated Bot responses are found, the emulator will build a new response body that will contain the response body to the original request in an object called testFrameworkResponse, as well as an array of botResponse objects that occured in response to the test input. Each botResponse object will contain the request sent by the Bot. The number of botResponse objects collected is the value specified in the X-Bot-Responses header.

As an example if we may expect a bot to send a message in response to being added to a space, and then to leave the space. To validate this with a test we would write a test that adds the bot to a space using the /memberships endpoint, which also set an X-Bot-Response header to 2, ie:
```
POST http://localhost:3210/memberships
{
"headers" {
"x-bot-responses": "2",
...
},
"body" {
"roomId": "Y2lzY29zcGFyazovL2VtL1JPT00vMzUzN2Q2ZTAtMmY5My00N2M0LWIwODMtZDYxNTg3MWZiMzFj"
"personId": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hODYwYmFkZC0wNGZkLTQwYWEtYWFjNS05NmYyYWRhZDE3NTA"
"isModerator": "false"
},
...
}
```

A sample consolidated response to such a request will have the status for the original request resposne and a body which might look as follows:
```
{
"testFrameworkResponse": {
"id": "Y2lzY29zcGFyazovL2VtL01FTUJFUlNISVAvMDMzMjllODctMzA4Ni00OThiLTg4ZGMtMzM5ZTVhODdlZGEy",
"roomId": "Y2lzY29zcGFyazovL2VtL1JPT00vMzUzN2Q2ZTAtMmY5My00N2M0LWIwODMtZDYxNTg3MWZiMzFj",
"personId": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9hODYwYmFkZC0wNGZkLTQwYWEtYWFjNS05NmYyYWRhZDE3NTA",
"personEmail": "bot@sparkbot.io",
"personDisplayName": "Bot",
"personOrgId": "Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi8xZWI2NWZkZi05NjQzLTQxN2YtOTk3NC1hZDcyY2FlMGUxMGY",
"isModerator": false,
"isMonitor": false,
"created": "2018-03-15T03:12:59.220Z"
},
"botResponses": [
{
"method": "POST",
"route": "/messages",
"body": {
"markdown": "Hi! Sorry, I only work in one on one rooms at the moment. Goodbye.",
"roomId": "Y2lzY29zcGFyazovL2VtL1JPT00vMzUzN2Q2ZTAtMmY5My00N2M0LWIwODMtZDYxNTg3MWZiMzFj"
}
},
{
"method": "DELETE",
"route": "/memberships/Y2lzY29zcGFyazovL2VtL01FTUJFUlNISVAvMDMzMjllODctMzA4Ni00OThiLTg4ZGMtMzM5ZTVhODdlZGEy",
"body": {}
}
]
}
```
The developer can then use tools like Postman to build a collection of test request and write tests to validate the expected response.

An added benefit of the Bot Test reqression framework is that it can be run on a laptop that is completely offline. Its great for working on your bot while on an airplane! In general your bot code itself needs to change only one thing:namely it needs to direct its Cisco Spark API calls to the emulator. The way this is done varies depending on the framework you are using.

[ToDo] provide example



## Testing

- The emulator comes with a Postman collection companion to quickly run requests againt the Emulator, and easilly switch back and forth between the emulator and the Cisco Spark API. To install the postman collection:

Expand Down
33 changes: 33 additions & 0 deletions bot-test/bot-interceptor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// Copyright (c) 2017 Cisco Systems
// Licensed under the MIT License
//

// Middleware to correlate bot responses to bot-test input

const debug = require("debug")("emulator:botInterceptor");
const sendError = require('../utils').sendError;
const sendSuccess = require('../utils').sendSuccess;
var interceptor = require('express-interceptor');

let botInterceptor = interceptor(function(req, res){
return {
// Only JSON responses will be intercepted
isInterceptable: function(){
return /application\/json/.test(res.get('Content-Type'));
},
// See if this is a response that we've been waiting for
intercept: function(body, send) {
console.log(body);
const db = req.app.locals.datastore;
if (db.responses.isTrackedResponse(res, JSON.parse(body))) {
console.log('Holding the response while waiting for bot input...');
} else {
send(body);
}
}
};
})


module.exports = botInterceptor;
53 changes: 53 additions & 0 deletions bot-test/bot-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// Copyright (c) 2017 Cisco Systems
// Licensed under the MIT License
//

// Middleware to look for X-Bot-Test

const debug = require("debug")("emulator:botTest");
const sendError = require('../utils').sendError;
const sendSuccess = require('../utils').sendSuccess;

let botTest = {};
let testIdCounter = 1;

botTest.middleware = function (req, res, next) {

// Public resources
if ((req.path == "/") || (req.path == "/tokens")) {
next();
}

debug('New Request from: '+ res.locals.person.emails[0]);
debug(req.method + ': ' + req.url);
if (req.method == 'GET') {
debug(req.params);
} else {
debug(req.body);
}

let botUnderTestEmail = req.app.locals.botUnderTestEmail
// Check if this is a test request that should generate bot requests
const botTestHeader = req.get("X-Bot-Responses");
if (botTestHeader) {
console.log('Found X-Bot-Test header: ' + botTestHeader);
const db = req.app.locals.datastore;
db.responses.initResponseObj(req, res, testIdCounter++);
} else {
if(res.locals.person.emails[0] == botUnderTestEmail) {
//See if this is a bot request in response to a test input
const db = req.app.locals.datastore;
if (db.responses.isTrackedBotResponse(req)) {
console.log('Updated response to test framework with bot request');
}
}
}

// Check if this is a request from the bot under test

next();
}


module.exports = botTest;
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"dependencies": {
"base-64": "^0.1.0",
"body-parser": "^1.17.2",
"dotenv": "^5.0.1",
"express": "^4.15.3",
"express-interceptor": "^1.2.0",
"request": "^2.81.0",
"uuid": "^3.1.0"
},
Expand All @@ -24,4 +26,4 @@
],
"author": "Stève Sfartz (Cisco DevNet) <stsfartz@cisco.com>",
"license": "MIT"
}
}
11 changes: 7 additions & 4 deletions resources/memberships.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ const express = require("express");
// default routing properties to mimic Cisco Spark
const router = express.Router({ "caseSensitive": true, "strict": false });

// for parsing application/json
const bodyParser = require("body-parser");
router.use(bodyParser.json());

// Extra imports
const sendError = require('../utils').sendError;
const sendSuccess = require('../utils').sendSuccess;
Expand Down Expand Up @@ -203,6 +199,13 @@ router.get("/:id", function (req, res) {
});
});

// Update a membership
router.put("/:id", function (req, res) {
debug(`Update membership not implemented yet`);
return sendError(res, 501, "[EMULATOR] Update membership not implemented yet");
});



// Delete a membership
router.delete("/:id", function (req, res) {
Expand Down
76 changes: 76 additions & 0 deletions sample-bot-test/bot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// Copyright (c) 2016 Cisco Systems
// Licensed under the MIT License
//

/*
* a Cisco Spark webhook based on pure Express.js.
*
* goal here is to illustrate how to create a bot without any library
*
*/

var express = require("express");
var app = express();

var bodyParser = require("body-parser");
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());

var debug = require("debug")("samples");
var Utils = require("../sparkbot/utils");


var started = Date.now();
app.route("/")
// healthcheck
.get(function (req, res) {
res.json({
message: "Congrats, your Cisco Spark bot is up and running",
since: new Date(started).toISOString(),
code: "express-all-in-one.js",
tip: "Register your bot as a WebHook to start receiving events: https://developer.ciscospark.com/endpoint-webhooks-post.html"
});
})

// webhook endpoint
.post(function (req, res) {

// analyse incoming payload, should conform to Spark webhook trigger specifications
debug("DEBUG: webhook invoked");
if (!req.body || !Utils.checkWebhookEvent(req.body)) {
console.log("WARNING: Unexpected payload POSTed, aborting...");
res.status(400).json({message: "Bad payload for Webhook",
details: "either the bot is misconfigured or Cisco Spark is running a new API version"});
return;
}

// event is ready to be processed, let's send a response to Spark without waiting any longer
res.status(200).json({message: "message is being processed by webhook"});

// process incoming resource/event, see https://developer.ciscospark.com/webhooks-explained.html
processWebhookEvent(req.body);
});


// Starts the Bot service
//
// [WORKAROUND] in some container situation (ie, Cisco Shipped), we need to use an OVERRIDE_PORT to force our bot to start and listen to the port defined in the Dockerfile (ie, EXPOSE),
// and not the PORT dynamically assigned by the host or scheduler.
var port = process.env.OVERRIDE_PORT || process.env.PORT || 8080;
app.listen(port, function () {
console.log("Cisco Spark Bot started at http://localhost:" + port + "/");
console.log(" GET / for health checks");
console.log(" POST / to procress new Webhook events");
});


// Invoked when the Spark webhook is triggered
function processWebhookEvent(trigger) {

//
// YOUR CODE HERE
//
console.log("EVENT: " + trigger.resource + "/" + trigger.event + ", with data id: " + trigger.data.id + ", triggered by person id:" + trigger.actorId);

}
18 changes: 17 additions & 1 deletion server.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const debug = require("debug")("emulator");
const express = require("express");
const uuid = require('uuid/v4');


//
// Setting up common services
//
Expand Down Expand Up @@ -46,6 +45,23 @@ app.use(function (req, res, next) {
const authentication = require("./auth");
app.use(authentication.middleware);

// for parsing application/json in middleware
const bodyParser = require("body-parser");
app.use(bodyParser.json());

// Allow user to set the BOT_UNDER_TEST environmnet var in a .env file
require('dotenv').load();
if (process.env.BOT_UNDER_TEST) {
app.locals.botUnderTestEmail = process.env.BOT_UNDER_TEST;
// Middleware to catch requests with the X-Bot-Responses header
const botTest = require("./bot-test/bot-test");
app.use(botTest.middleware);

// Middleware to delay responses until we get the expected bot response
var botInterceptor = require('./bot-test/bot-interceptor');
app.use(botInterceptor);
}

// Load initial list of accounts
const accounts = Object.keys(authentication.tokens).map(function (item, index) {
return authentication.tokens[item];
Expand Down
2 changes: 2 additions & 0 deletions storage/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ const MessageStorage = require("./messages");
datastore.messages = new MessageStorage(datastore);
const WebhookStorage = require("./webhooks");
datastore.webhooks = new WebhookStorage(datastore);
const ResponseStorage = require("./response-builder")
datastore.responses = new ResponseStorage(datastore);

module.exports = datastore;
Loading