copy-cat
Version:
A standalone server to mock various communications services for push messaging, email, and sms.
202 lines (173 loc) • 6.92 kB
JavaScript
'use strict';
var express = require('express'),
routeValidator = require('express-route-validator'),
db = require('../db');
require('./validators');
module.exports = function (config) {
var router = express.Router(),
router_v1 = express.Router();
// Add store for bandwidth to track messages
var store = db.addStore('bandwidth', {
messages: {}, // Store messages hashed by ids
reset: function () {
this.messages = {};
},
addMessage: function (message) {
this.messages[message.id] = message;
},
getMessage: function (id) {
return this.messages[id];
}
});
// If config includes a token and secret, check that header matches
if (config.apiToken && config.apiSecret) {
var authorizationHeader = 'Basic ' + new Buffer(config.apiToken + ':' + config.apiSecret).toString('base64');
}
function _authenticateRequest (req, res, next) {
if (!req.headers.authorization || authorizationHeader && req.headers.authorization !== authorizationHeader) {
return res.status(401).end();
}
if (config.userId && req.params.user !== config.userId) {
return res.status(401).end();
}
return next();
}
// router_v1.use(_authenticateRequest);
// Save a reference to the POST /messages validator config to reuse on case where body is an array of messages
var postMessagesValidationConfig = {
body: {
from: { isRequired: true, isBandwidthPhoneNumber: true, toE164PhoneNumber: true, note: 'Should be E.164 format, but API will try to format. Number must be configured in your application.' },
to: { isRequired: true, isBandwidthPhoneNumber: true, toE164PhoneNumber: true, note: 'Should be E.164 format, but API will try to format.' },
text: { isRequired: true, isLength: { min: 1, max: 2048 }, note: 'Text of message. Can\'t be greater than 2,048 characters' },
receiptRequested: { isRequired: false, isIn: ['none', 'all', 'error' ], note: 'Requested receipt option for outbound messages: none all error Default is none.' },
callbackUrl: { isRequired: false, isUrl: true, note: 'The server URL where the events related to the outgoing message will be sent to.' },
callbackHttpMethod: { isRequired: false, isIn: ['get', 'post'], note: 'Determine if the callback event should be sent via HTTP GET or HTTP POST. Values are get or post Default is post' },
callbackTimeout: { isRequired: false, isInt: true, note: 'Determine how long should the platform wait for callbackUrl\'s response before timing out (milliseconds).' },
fallbackUrl: { isRequired: false, isUrl: true, note: 'The server URL used to send the message events if the request to callbackUrl fails.' },
tag: { isRequired: false, note: 'Any string, it will be included in the callback events of the message.' }
},
headers: {
'content-type': { isRequired: true, equals: 'application/json', note: 'Must send JSON' },
},
params: {
user: { isRequired: true, note: 'User id for requestor' }
}
};
// Save validator closure to use in case where body is an array of messages and can be manually called
var postMessagesValidator = routeValidator.validate({
body: postMessagesValidationConfig.body,
callNext: true
});
router_v1.post('/users/:user/messages', _authenticateRequest, function (req, res, next) {
// Check if body is an array of messages. If so, pass to the next route handler
if (req.body instanceof Array) {
return next('route');
} else {
return next();
}
}, routeValidator.validate(postMessagesValidationConfig), function (req, res) {
var message = _generateMessage(req.body.from, req.body.to, req.body.text);
// Store message in memory to enable GET /messages/:message
store.addMessage(message);
// Set response headers to match Bandwidth
res.set('Cache-Control', 'no-cache');
res.set('Location', req.protocol + '://' + req.hostname + req.originalUrl + '/' + message.id);
return res.status(201).end();
});
// Handle case where body is an array of messages
router_v1.post('/users/:user/messages', _authenticateRequest, routeValidator.validate({
headers: postMessagesValidationConfig.headers,
params: postMessagesValidationConfig.params
}), function (req, res, next) {
var error;
for (var i = 0, length = req.body.length; i < length; i++) {
postMessagesValidator({
body: req.body[i]
}, {}, _setError);
}
if (error) {
return res.status(400).send({
error: error.message
});
}
return next();
function _setError (err) {
if (err) {
error = err;
}
}
}, function (req, res) {
var messages = [],
message,
data = [],
locationStub = req.protocol + '://' + req.hostname + req.originalUrl + '/';
for (var i = 0, length = req.body.length; i < length; i++) {
message = _generateMessage(req.body[i].from, req.body[i].to, req.body[i].text);
messages.push(message);
store.addMessage(message);
data.push({
result: 'accepted',
location: locationStub + message.id
});
}
// Set response headers to match Bandwidth
res.set('Cache-Control', 'no-cache');
return res.status(201).send(data);
});
router_v1.get('/users/:user/messages/:message', _authenticateRequest, routeValidator.validate({
params: {
message: { isRequired: true, note: 'ID of the message requesting' },
user: { isRequired: true, note: 'User id for requestor' }
}
}), function (req, res) {
var message = store.getMessage(req.params.message);
if (!message) {
return res.status(404).json({
category: "not-found",
code: "message-not-found",
message: "The message '" + req.params.message + "' could not be found",
details: [
{
name: "messageId",
value: req.params.message
},
{
name: "requestMethod",
value: "GET"
},
{
name: "remoteAddress",
value: req.ip
},
{
name: "requestPath",
value: "users/" + req.params.user + "/messages/" + req.params.message
}
]
});
}
// Set response header to match Bandwidth
res.set('Cache-Control', 'no-cache');
return res.status(200).json(message);
});
router.use('/v1', router_v1);
return router;
function _generateMessageId () {
return 'm-i2c5jvxizg6wz' + ('' + Math.random()).substr(2,10);
}
function _generateMessage (from, to, text) {
var id = _generateMessageId();
return {
direction: 'out',
from: from,
id: id,
messageId: id,
state: 'sent',
text: text,
media: [],
time: new Date(),
to: to,
skipMMSCarrierValidation: false
};
}
};