major updates and test suite

This commit is contained in:
Dave Horton
2019-12-04 21:43:27 -05:00
parent 47bb642854
commit 0f06c44168
35 changed files with 1624 additions and 500 deletions

8
.travis.yml Normal file
View File

@@ -0,0 +1,8 @@
sudo: required
language: node_js
node_js:
- "lts/*"
services:
- docker
script:
- npm test

5
app.js
View File

@@ -20,6 +20,9 @@ app.use(bodyParser.json());
app.use('/v1', passport.authenticate('bearer', { session: false }));
app.use('/', routes);
app.use((err, req, res, next) => {
req.status(500).json({msg: err.message});
logger.error(err, 'burped error');
res.status(err.status || 500).json({msg: err.message});
});
app.listen(PORT);
module.exports = app;

12
config/test.json Normal file
View File

@@ -0,0 +1,12 @@
{
"logging": {
"level": "error"
},
"mysql": {
"host": "localhost",
"user": "jambones",
"password": "jambones",
"database": "jambones",
"port": 3406
}
}

View File

@@ -0,0 +1,2 @@
insert into api_keys (api_key_sid, token)
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');

View File

@@ -27,8 +27,6 @@ DROP TABLE IF EXISTS `accounts`;
DROP TABLE IF EXISTS `service_providers`;
DROP TABLE IF EXISTS `phone_number_inventory`;
DROP TABLE IF EXISTS `voip_carriers`;
CREATE TABLE IF NOT EXISTS `applications`
@@ -66,16 +64,6 @@ CREATE TABLE IF NOT EXISTS `conference_participants`
PRIMARY KEY (`conference_participant_sid`)
) ENGINE=InnoDB COMMENT='A relationship between a call and a conference that it is co';
CREATE TABLE IF NOT EXISTS `phone_numbers`
(
`phone_number_sid` CHAR(36) NOT NULL UNIQUE ,
`number` VARCHAR(255) NOT NULL UNIQUE ,
`account_sid` CHAR(36) NOT NULL,
`application_sid` CHAR(36),
`phone_number_inventory_id` INTEGER(10) UNSIGNED NOT NULL,
PRIMARY KEY (`phone_number_sid`)
) ENGINE=InnoDB COMMENT='A phone number that has been assigned to an account';
CREATE TABLE IF NOT EXISTS `queues`
(
`id` INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE ,
@@ -182,13 +170,15 @@ CREATE TABLE IF NOT EXISTS `voip_carriers`
PRIMARY KEY (`voip_carrier_sid`)
) ENGINE=InnoDB COMMENT='An external organization that can provide sip trunking and D';
CREATE TABLE IF NOT EXISTS `phone_number_inventory`
CREATE TABLE IF NOT EXISTS `phone_numbers`
(
`phone_number_inventory_sid` CHAR(36) NOT NULL UNIQUE ,
`phone_number_sid` CHAR(36) NOT NULL UNIQUE ,
`number` VARCHAR(255) NOT NULL UNIQUE ,
`voip_carrier_sid` CHAR(36) NOT NULL,
PRIMARY KEY (`phone_number_inventory_sid`)
) ENGINE=InnoDB COMMENT='Telephone numbers (DIDs) that have been procured from a voip';
`account_sid` CHAR(36),
`application_sid` CHAR(36),
PRIMARY KEY (`phone_number_sid`)
) ENGINE=InnoDB COMMENT='A phone number that has been assigned to an account';
CREATE INDEX `applications_application_sid_idx` ON `applications` (`application_sid`);
CREATE INDEX `applications_name_idx` ON `applications` (`name`);
@@ -205,11 +195,6 @@ ALTER TABLE `conference_participants` ADD FOREIGN KEY call_sid_idxfk (`call_sid`
ALTER TABLE `conference_participants` ADD FOREIGN KEY conference_sid_idxfk (`conference_sid`) REFERENCES `conferences` (`conference_sid`);
CREATE INDEX `phone_numbers_phone_number_sid_idx` ON `phone_numbers` (`phone_number_sid`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY account_sid_idxfk_2 (`account_sid`) REFERENCES `accounts` (`account_sid`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY application_sid_idxfk_1 (`application_sid`) REFERENCES `applications` (`application_sid`);
CREATE INDEX `queues_queue_sid_idx` ON `queues` (`queue_sid`);
CREATE INDEX `registrations_registration_sid_idx` ON `registrations` (`registration_sid`);
CREATE INDEX `queue_members_queue_member_sid_idx` ON `queue_members` (`queue_member_sid`);
@@ -220,7 +205,7 @@ ALTER TABLE `queue_members` ADD FOREIGN KEY queue_sid_idxfk (`queue_sid`) REFERE
CREATE INDEX `calls_call_sid_idx` ON `calls` (`call_sid`);
ALTER TABLE `calls` ADD FOREIGN KEY parent_call_sid_idxfk (`parent_call_sid`) REFERENCES `calls` (`call_sid`);
ALTER TABLE `calls` ADD FOREIGN KEY application_sid_idxfk_2 (`application_sid`) REFERENCES `applications` (`application_sid`);
ALTER TABLE `calls` ADD FOREIGN KEY application_sid_idxfk_1 (`application_sid`) REFERENCES `applications` (`application_sid`);
ALTER TABLE `calls` ADD FOREIGN KEY phone_number_sd_idxfk (`phone_number_sd`) REFERENCES `phone_numbers` (`phone_number_sid`);
@@ -231,7 +216,7 @@ ALTER TABLE `calls` ADD FOREIGN KEY outbound_user_sid_idxfk (`outbound_user_sid`
CREATE INDEX `service_providers_service_provider_sid_idx` ON `service_providers` (`service_provider_sid`);
CREATE INDEX `service_providers_name_idx` ON `service_providers` (`name`);
CREATE INDEX `api_keys_api_key_sid_idx` ON `api_keys` (`api_key_sid`);
ALTER TABLE `api_keys` ADD FOREIGN KEY account_sid_idxfk_3 (`account_sid`) REFERENCES `accounts` (`account_sid`);
ALTER TABLE `api_keys` ADD FOREIGN KEY account_sid_idxfk_2 (`account_sid`) REFERENCES `accounts` (`account_sid`);
ALTER TABLE `api_keys` ADD FOREIGN KEY service_provider_sid_idxfk (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`);
@@ -242,5 +227,9 @@ CREATE INDEX `accounts_name_idx` ON `accounts` (`name`);
ALTER TABLE `accounts` ADD FOREIGN KEY service_provider_sid_idxfk_1 (`service_provider_sid`) REFERENCES `service_providers` (`service_provider_sid`);
CREATE INDEX `voip_carriers_voip_carrier_sid_idx` ON `voip_carriers` (`voip_carrier_sid`);
CREATE INDEX `phone_number_inventory_sid_idx` ON `phone_number_inventory` (`phone_number_inventory_sid`);
ALTER TABLE `phone_number_inventory` ADD FOREIGN KEY voip_carrier_sid_idxfk (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`);
CREATE INDEX `phone_numbers_phone_number_sid_idx` ON `phone_numbers` (`phone_number_sid`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY voip_carrier_sid_idxfk (`voip_carrier_sid`) REFERENCES `voip_carriers` (`voip_carrier_sid`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY account_sid_idxfk_3 (`account_sid`) REFERENCES `accounts` (`account_sid`);
ALTER TABLE `phone_numbers` ADD FOREIGN KEY application_sid_idxfk_2 (`application_sid`) REFERENCES `applications` (`application_sid`);

View File

@@ -111,7 +111,7 @@
<width>266.00</width>
<height>80.00</height>
</size>
<zorder>14</zorder>
<zorder>13</zorder>
<SQLField>
<name><![CDATA[voip_carrier_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
@@ -204,58 +204,6 @@
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[3EDF89A0-FD38-4DF9-BB65-E0FCD0A678BE]]></uid>
</SQLTable>
<SQLTable>
<name><![CDATA[phone_number_inventory]]></name>
<schema><![CDATA[]]></schema>
<comment><![CDATA[Telephone numbers (DIDs) that have been procured from a voip carrier for our use]]></comment>
<tableType><![CDATA[InnoDB]]></tableType>
<location>
<x>78.00</x>
<y>178.00</y>
</location>
<size>
<width>344.00</width>
<height>80.00</height>
</size>
<zorder>13</zorder>
<SQLField>
<name><![CDATA[phone_number_inventory_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<primaryKey>1</primaryKey>
<autoIncrement><![CDATA[0]]></autoIncrement>
<indexed><![CDATA[1]]></indexed>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[0F123268-6D7F-438D-A285-14B7183F6BF9]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<SQLField>
<name><![CDATA[number]]></name>
<type><![CDATA[VARCHAR(255)]]></type>
<autoIncrement><![CDATA[0]]></autoIncrement>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[74808B94-D472-4243-A5F6-48150FCFC712]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<SQLField>
<name><![CDATA[voip_carrier_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<referencesField>voip_carrier_sid</referencesField>
<referencesTable>voip_carriers</referencesTable>
<referencesField><![CDATA[voip_carrier_sid]]></referencesField>
<referencesTable><![CDATA[voip_carriers]]></referencesTable>
<sourceCardinality>4</sourceCardinality>
<destinationCardinality>1</destinationCardinality>
<referencesFieldUID><![CDATA[E34851EF-3C5F-4118-8425-F88EE16E38AA]]></referencesFieldUID>
<referencesTableUID><![CDATA[3D3136A7-AFC0-4A70-AEC3-68577955CA2E]]></referencesTableUID>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[4271E944-7163-407D-A2BC-71F85F6A2E91]]></uid>
<unsigned><![CDATA[0]]></unsigned>
</SQLField>
<labelWindowIndex><![CDATA[1]]></labelWindowIndex>
<objectComment><![CDATA[Telephone numbers (DIDs) that have been procured from a voip carrier for our use]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
<uid><![CDATA[5497021F-675D-406B-A378-104E0F125823]]></uid>
</SQLTable>
<SQLTable>
<name><![CDATA[call_routes]]></name>
<schema><![CDATA[]]></schema>
@@ -746,7 +694,7 @@
<tableType><![CDATA[InnoDB]]></tableType>
<location>
<x>83.00</x>
<y>322.00</y>
<y>191.00</y>
</location>
<size>
<width>331.00</width>
@@ -771,6 +719,20 @@
<uid><![CDATA[159B82ED-C6B0-4FC6-957B-5C354AF9E783]]></uid>
<unique><![CDATA[1]]></unique>
</SQLField>
<SQLField>
<name><![CDATA[voip_carrier_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
<referencesField>voip_carrier_sid</referencesField>
<referencesTable>voip_carriers</referencesTable>
<referencesField><![CDATA[voip_carrier_sid]]></referencesField>
<referencesTable><![CDATA[voip_carriers]]></referencesTable>
<sourceCardinality>4</sourceCardinality>
<destinationCardinality>1</destinationCardinality>
<referencesFieldUID><![CDATA[E34851EF-3C5F-4118-8425-F88EE16E38AA]]></referencesFieldUID>
<referencesTableUID><![CDATA[3D3136A7-AFC0-4A70-AEC3-68577955CA2E]]></referencesTableUID>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[58A047B9-C1C9-4697-9FE8-08E3BFF91660]]></uid>
</SQLField>
<SQLField>
<name><![CDATA[account_sid]]></name>
<type><![CDATA[CHAR(36)]]></type>
@@ -779,10 +741,10 @@
<referencesField><![CDATA[account_sid]]></referencesField>
<referencesTable><![CDATA[accounts]]></referencesTable>
<sourceCardinality>4</sourceCardinality>
<destinationCardinality>1</destinationCardinality>
<destinationCardinality>2</destinationCardinality>
<referencesFieldUID><![CDATA[1342FAFA-C15C-429B-809B-C6C55F9FA5B6]]></referencesFieldUID>
<referencesTableUID><![CDATA[985D6997-B1A7-4AB3-80F4-4D59B45480C8]]></referencesTableUID>
<notNull><![CDATA[1]]></notNull>
<notNull><![CDATA[0]]></notNull>
<uid><![CDATA[66690304-2370-4480-99E5-52C73A20F597]]></uid>
<unique><![CDATA[0]]></unique>
<unsigned><![CDATA[0]]></unsigned>
@@ -801,13 +763,6 @@
<uid><![CDATA[962CB80A-54CB-4C6A-9591-9BFC644CF80F]]></uid>
<unsigned><![CDATA[0]]></unsigned>
</SQLField>
<SQLField>
<name><![CDATA[phone_number_inventory_id]]></name>
<type><![CDATA[INTEGER(10)]]></type>
<notNull><![CDATA[1]]></notNull>
<uid><![CDATA[5CE29FC3-CB49-4370-B985-2D948500A71A]]></uid>
<unsigned><![CDATA[1]]></unsigned>
</SQLField>
<labelWindowIndex><![CDATA[6]]></labelWindowIndex>
<objectComment><![CDATA[A phone number that has been assigned to an account]]></objectComment>
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
@@ -1036,17 +991,17 @@
<overviewPanelHidden><![CDATA[0]]></overviewPanelHidden>
<pageBoundariesVisible><![CDATA[0]]></pageBoundariesVisible>
<PageGridVisible><![CDATA[0]]></PageGridVisible>
<RightSidebarWidth><![CDATA[1841.000000]]></RightSidebarWidth>
<RightSidebarWidth><![CDATA[1403.000000]]></RightSidebarWidth>
<sidebarIndex><![CDATA[2]]></sidebarIndex>
<snapToGrid><![CDATA[0]]></snapToGrid>
<SourceSidebarWidth><![CDATA[0.000000]]></SourceSidebarWidth>
<SQLEditorFileFormatVersion><![CDATA[4]]></SQLEditorFileFormatVersion>
<uid><![CDATA[58C99A00-06C9-478C-A667-C63842E088F3]]></uid>
<windowHeight><![CDATA[1613.000000]]></windowHeight>
<windowLocationX><![CDATA[62.000000]]></windowLocationX>
<windowLocationY><![CDATA[31.000000]]></windowLocationY>
<windowHeight><![CDATA[1027.000000]]></windowHeight>
<windowLocationX><![CDATA[0.000000]]></windowLocationX>
<windowLocationY><![CDATA[0.000000]]></windowLocationY>
<windowScrollOrigin><![CDATA[{0, 0}]]></windowScrollOrigin>
<windowWidth><![CDATA[2118.000000]]></windowWidth>
<windowWidth><![CDATA[1680.000000]]></windowWidth>
</SQLDocumentInfo>
<AllowsIndexRenamingOnInsert><![CDATA[1]]></AllowsIndexRenamingOnInsert>
<defaultLabelExpanded><![CDATA[1]]></defaultLabelExpanded>

View File

@@ -27,18 +27,22 @@ function makeStrategy(logger) {
}
// found api key
logger.info(results, 'api key lookup');
const scope = [];
if (results[0].account_sid === null && results[0].service_provider_sid === null) {
scope.push.apply(scope, ['admin', 'service_provider', 'account']);
}
else if (results[0].service_provider_sid) {
scope.push.apply(scope, ['service_provider', 'account']);
}
else {
scope.push('account');
}
const user = {
account_sid: results[0].account_sid,
service_provider_sid: results[0].service_provider_sid,
isAdmin: results[0].account_sid === null && results[0].service_provider_sid === null,
isServiceProvider: results[0].service_provider_sid !== null,
isUser: results[0].account_sid != null
hasScope: (s) => scope.includes(s)
};
const scope = [];
if (user.isAdmin) scope.push('admin');
else if (user.isServiceProvider) scope.push('service_provider');
else scope.push('user');
logger.info(user, `successfully validated with scope ${scope}`);
return done(null, user, {scope});
});

View File

@@ -6,7 +6,6 @@ pool.getConnection((err, conn) => {
if (err) return console.error(err, 'Error testing pool');
conn.ping((err) => {
if (err) return console.error(err, `Error pinging mysql at ${JSON.stringify(config.get('mysql'))}`);
console.log('successfully pinged mysql database');
});
});

36
lib/models/account.js Normal file
View File

@@ -0,0 +1,36 @@
const Model = require('./model');
class Account extends Model {
constructor() {
super();
}
}
Account.table = 'accounts';
Account.fields = [
{
name: 'account_sid',
type: 'string',
primaryKey: true
},
{
name: 'name',
type: 'string',
required: true
},
{
name: 'service_provider_sid',
type: 'string',
required: true
},
{
name: 'sip_realm',
type: 'string',
},
{
name: 'registration_hook',
type: 'string',
}
];
module.exports = Account;

77
lib/models/application.js Normal file
View File

@@ -0,0 +1,77 @@
const Model = require('./model');
const {getMysqlConnection} = require('../db');
const serviceProviderSql = `
SELECT * from ${this.table}
WHERE account_sid in (
SELECT account_sid from accounts
WHERE service_provider_sid = ?
)`;
class Application extends Model {
constructor() {
super();
}
/**
* retrieve all applications for an account
*/
static retrieveAllForAccount(account_sid) {
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(`SELECT * from ${this.table} WHERE account_sid = ?`, [account_sid], (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results);
});
});
});
}
/**
* retrieve all applications for a service provider
*/
static retrieveAllForServiceProvider(service_provider_sid) {
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(serviceProviderSql, [service_provider_sid], (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results);
});
});
});
}
}
Application.table = 'applications';
Application.fields = [
{
name: 'application_sid',
type: 'string',
primaryKey: true
},
{
name: 'name',
type: 'string',
required: true
},
{
name: 'account_sid',
type: 'string',
required: true
},
{
name: 'call_hook',
type: 'string',
required: true
},
{
name: 'call_status_hook',
type: 'string',
required: true
}
];
module.exports = Application;

View File

@@ -30,11 +30,11 @@ class Model extends Emitter {
static make(obj) {
return new Promise((resolve, reject) => {
const pk = this.getPrimaryKey();
const uuid = uuidv4();
obj[pk.name] = uuid;
this.checkIsInsertable(obj);
getMysqlConnection((err, conn) => {
if (err) return reject(err);
const uuid = uuidv4();
obj[pk.name] = uuid;
conn.query(`INSERT into ${this.table} SET ?`,
obj,
(err, results, fields) => {

View File

@@ -0,0 +1,36 @@
const Model = require('./model');
class PhoneNumber extends Model {
constructor() {
super();
}
}
PhoneNumber.table = 'phone_numbers';
PhoneNumber.fields = [
{
name: 'phone_number_sid',
type: 'string',
primaryKey: true
},
{
name: 'number',
type: 'string',
required: true
},
{
name: 'voip_carrier_sid',
type: 'string',
required: true
},
{
name: 'account_sid',
type: 'string',
},
{
name: 'application_sid',
type: 'string',
}
];
module.exports = PhoneNumber;

View File

@@ -0,0 +1,27 @@
const Model = require('./model');
class VoipCarrier extends Model {
constructor() {
super();
}
}
VoipCarrier.table = 'voip_carriers';
VoipCarrier.fields = [
{
name: 'voip_carrier_sid',
type: 'string',
primaryKey: true
},
{
name: 'name',
type: 'string',
required: true
},
{
name: 'description',
type: 'string'
}
];
module.exports = VoipCarrier;

View File

@@ -0,0 +1,29 @@
const router = require('express').Router();
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const Account = require('../../models/account');
const ServiceProvider = require('../../models/service-provider');
const decorate = require('./decorate');
const preconditions = {
'add': validateAdd,
'update': validateUpdate,
'delete': validateDelete
};
async function validateAdd(req) {
/* check that service provider exists */
const result = await ServiceProvider.retrieve(req.body.service_provider_sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`service_provider not found for sid ${req.body.service_provider_sid}`);
}
}
async function validateUpdate(req, sid) {
if (req.body.service_provider_sid) throw new DbErrorBadRequest('service_provider_sid may not be modified')
}
async function validateDelete(req, sid) {
const assignedPhoneNumbers = await Account.getForeignKeyReferences('phone_numbers.account_sid', sid);
if (assignedPhoneNumbers > 0) throw new DbErrorUnprocessableRequest('cannot delete account with phone numbers');
}
decorate(router, Account, ['*'], preconditions);
module.exports = router;

View File

@@ -1,7 +1,6 @@
const router = require('express').Router();
const {DbErrorBadRequest} = require('../../utils/errors');
const ApiKey = require('../../models/api-key');
const {isAdmin, isServiceProvider, isUser} = require('../../utils/scopes');
const decorate = require('./decorate');
const uuidv4 = require('uuid/v4');
const assert = require('assert');
@@ -11,38 +10,28 @@ const preconditions = {
'delete': validateDeleteToken
};
/**
* if user scope, add to the associated account
* if admin scope, only admin-level tokens may be created
*/
function validateAddToken(req) {
if (isAdmin(req) && ('account_sid' in req.body)) {
throw new DbErrorBadRequest('admin users may not create account-level tokens');
if (req.user.hasScope('admin') && ('account_sid' in req.body)) {
// ok
}
else if (isServiceProvider(req) && (!('account_sid' in req.body) && !('service_provider_sid' in req.body))) {
req.body['service_provider_sid'] = req.user.service_provider_sid
else if (req.user.hasScope('service_provider') &&
(!('account_sid' in req.body) && !('service_provider_sid' in req.body))) {
req.body['service_provider_sid'] = req.user.service_provider_sid;
}
else if (isUser(req)) {
else if (req.user.hasScope('account') && !req.user.hasScope('service_provider')) {
delete req.body['service_provider_sid'];
req.body['account_sid'] = req.user.account_sid;
}
req.body.token = uuidv4();
}
/**
* admin users can only delete admin tokens or service provider tokens
* service_provider users can delete service provider or user tokens
* user-scope may only delete their own tokens
*/
async function validateDeleteToken(req, sid) {
const results = await ApiKey.retrieve(sid);
if (0 == results.length) return;
if (isAdmin(req)) {
if (results[0].account_sid) {
throw new DbErrorBadRequest('an admin user may not delete account level api keys');
}
if (req.user.hasScope('admin')) {
// can do anything
}
else if (isServiceProvider(req)) {
else if (req.user.hasScope('service_provider')) {
if (results[0].service_provider_sid === null && results[0].account_sid === null) {
throw new DbErrorBadRequest('a service provider user may not delete an admin token');
}
@@ -50,7 +39,7 @@ async function validateDeleteToken(req, sid) {
throw new DbErrorBadRequest('a service provider user may not delete api key from another service provider');
}
}
else if (isUser(req)) {
else {
if (results[0].account_sid !== req.user.account_sid) {
throw new DbErrorBadRequest('a user may not delete a token associated with a different account');
}

View File

@@ -0,0 +1,54 @@
const router = require('express').Router();
const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors');
const Application = require('../../models/application');
const decorate = require('./decorate');
const sysError = require('./error');
const preconditions = {
'add': validateAdd,
'update': validateUpdate,
'delete': validateDelete
};
/* only user-level tokens can add applications */
async function validateAdd(req) {
if (req.user.account_sid) {
req.body.account_sid = req.user.account_sid;
}
}
async function validateUpdate(req, sid) {
if (req.user.account_sid && sid !== req.user.account_sid) {
throw new DbErrorBadRequest('you may not update or delete an application associated with a different account');
}
}
async function validateDelete(req, sid) {
if (req.user.account_sid && sid !== req.user.account_sid) {
throw new DbErrorBadRequest('you may not update or delete an application associated with a different account');
}
const assignedPhoneNumbers = await Application.getForeignKeyReferences('phone_numbers.application_sid', sid);
if (assignedPhoneNumbers > 0) throw new DbErrorUnprocessableRequest('cannot delete application with phone numbers');
}
decorate(router, Application, ['*'], preconditions);
/**
* if account-level privileges, retrieve only applications for that account
* ditto if service provider
*/
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
try {
let results;
if (req.user.account_sid) results = Application.retrieveAllForAccount(req.user.account_sid);
else if (req.user.service_provider_sid) {
results = Application.retrieveAllForServiceProvider(req.user.service_provider_sid);
}
else results = Application.Application.retrieveAll();
res.status(200).json(results);
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router;

View File

@@ -19,12 +19,16 @@ function decorate(router, klass, methods, preconditions) {
});
}
function list(router, klass) {
function list(router, klass, preconditions) {
router.get('/', async(req, res) => {
const logger = req.app.locals.logger;
//logger.info(`user: ${JSON.stringify(req.user)}`);
//logger.info(`scope: ${JSON.stringify(req.authInfo.scope)}`);
try {
if ('list' in preconditions) {
assert(typeof preconditions.list === 'function');
await preconditions.list(req);
}
const results = await klass.retrieveAll();
res.status(200).json(results);
} catch (err) {
@@ -63,11 +67,15 @@ function retrieve(router, klass) {
});
}
function update(router, klass) {
function update(router, klass, preconditions) {
router.put('/:sid', async(req, res) => {
const sid = req.params.sid;
const logger = req.app.locals.logger;
try {
if ('update' in preconditions) {
assert(typeof preconditions.update === 'function');
await preconditions.update(req, sid);
}
const rowsAffected = await klass.update(sid, req.body);
if (rowsAffected === 0) {
return res.status(404).end();

View File

@@ -2,15 +2,15 @@ const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/er
function sysError(logger, res, err) {
if (err instanceof DbErrorBadRequest) {
logger.error(err, 'invalid client request');
logger.info(err, 'invalid client request');
return res.status(400).json({msg: err.message});
}
if (err instanceof DbErrorUnprocessableRequest) {
logger.error(err, 'unprocessable request');
logger.info(err, 'unprocessable request');
return res.status(422).json({msg: err.message});
}
if (err.message.includes('ER_DUP_ENTRY')) {
logger.error(err, 'duplicate entry on insert');
logger.info(err, 'duplicate entry on insert');
return res.status(422).json({msg: err.message});
}
logger.error(err, 'Database error');

View File

@@ -1,8 +1,7 @@
const api = require('express').Router();
const {isAdmin} = require('../../utils/scopes');
function isAdminScope(req, res, next) {
if (isAdmin(req)) return next();
if (req.user.hasScope('admin')) return next();
res.status(403).json({
status: 'fail',
message: 'insufficient privileges'
@@ -10,6 +9,10 @@ function isAdminScope(req, res, next) {
}
api.use('/ServiceProviders', isAdminScope, require('./service-providers'));
api.use('/VoipCarriers', isAdminScope, require('./voip-carriers'));
api.use('/PhoneNumbers', isAdminScope, require('./phone-numbers'));
api.use('/ApiKeys', require('./api-keys'));
api.use('/Accounts', require('./accounts'));
api.use('/Applications', require('./applications'));
module.exports = api;

View File

@@ -0,0 +1,53 @@
const router = require('express').Router();
const {DbErrorUnprocessableRequest, DbErrorBadRequest} = require('../../utils/errors');
const PhoneNumber = require('../../models/phone-number');
const VoipCarrier = require('../../models/voip-carrier');
const decorate = require('./decorate');
const validateNumber = require('../../utils/phone-number-syntax');
const preconditions = {
'add': validateAdd,
'delete': checkInUse,
'update': validateUpdate
};
/* check for required fields when adding */
async function validateAdd(req) {
try {
if (!req.body.voip_carrier_sid) throw new DbErrorBadRequest('voip_carrier_sid is required');
if (!req.body.number) throw new DbErrorBadRequest('number is required');
validateNumber(req.body.number);
} catch (err) {
throw new DbErrorBadRequest(err.message);
}
/* check that voip carrier exists */
const result = await VoipCarrier.retrieve(req.body.voip_carrier_sid);
if (!result || result.length === 0) {
throw new DbErrorBadRequest(`voip_carrier not found for sid ${req.body.voip_carrier_sid}`);
}
}
/* can not delete a phone number if it in use */
async function checkInUse(req, sid) {
const phoneNumber = await PhoneNumber.retrieve(sid);
if (phoneNumber.account_sid) {
throw new DbErrorUnprocessableRequest('cannot delete phone number that is assigned to an account');
}
}
/* can not change number or voip carrier */
async function validateUpdate(req, sid) {
const result = await PhoneNumber.retrieve(sid);
if (req.body.voip_carrier_sid) throw new DbErrorBadRequest('voip_carrier_sid may not be modified');
if (req.body.number) throw new DbErrorBadRequest('number may not be modified');
// TODO: if we are assigning to an account, verify it exists
// TODO: if we are assigning to an application, verify it is associated to the same account
// TODO: if we are removing from an account, verify we are also removing from application.
}
decorate(router, PhoneNumber, ['*'], preconditions);
module.exports = router;

View File

@@ -0,0 +1,17 @@
const router = require('express').Router();
const {DbErrorUnprocessableRequest} = require('../../utils/errors');
const VoipCarrier = require('../../models/voip-carrier');
const decorate = require('./decorate');
const preconditions = {
'delete': noActiveAccounts
};
/* can not delete a voip provider if it has any active phone numbers */
async function noActiveAccounts(req, sid) {
const activeAccounts = await VoipCarrier.getForeignKeyReferences('phone_numbers.voip_carrier_sid', sid);
if (activeAccounts > 0) throw new DbErrorUnprocessableRequest('cannot delete voip carrier with active phone numbers');
}
decorate(router, VoipCarrier, ['*'], preconditions);
module.exports = router;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
function validate(number) {
if (typeof number !== 'string') throw new Error('phone number must be a string');
if (!/^\d+$/.test(number)) throw new Error('phone number must only include digits');
if (number.length < 8) throw new Error('invalid phone number: insufficient digits');
if (number[0] === '1' && number.length !== 11) throw new Error('invalid US phone number');
}
module.exports = validate;

View File

@@ -1,17 +0,0 @@
function isAdmin(req) {
return req.authInfo.scope.includes('admin');
}
function isServiceProvider(req) {
return req.authInfo.scope.includes('service_provider');
}
function isUser(req) {
return req.authInfo.scope.includes('user');
}
module.exports = {
isAdmin,
isServiceProvider,
isUser
};

View File

@@ -4,7 +4,9 @@
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "NODE_ENV=test node test/ | ./node_modules/.bin/tap-spec",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib"
},
"author": "",
"license": "ISC",
@@ -19,7 +21,17 @@
"request": "^2.88.0",
"request-debug": "^0.2.0",
"swagger-ui-express": "^4.1.2",
"tape": "^4.11.0",
"uuid": "^3.3.3",
"yamljs": "^0.3.0"
},
"devDependencies": {
"eslint": "^6.7.2",
"eslint-plugin-promise": "^4.2.1",
"nyc": "^14.1.1",
"request-promise-native": "^1.0.8",
"tap": "^14.10.2",
"tap-dot": "^2.0.0",
"tap-spec": "^5.0.0"
}
}

99
test/accounts.js Normal file
View File

@@ -0,0 +1,99 @@
const test = require('tape').test ;
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const {createVoipCarrier, createServiceProvider, createPhoneNumber, deleteObjectBySid} = require('./utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('account tests', async(t) => {
const app = require('../app');
let sid;
try {
let result;
/* add service provider, phone number, and voip carrier */
const voip_carrier_sid = await createVoipCarrier(request);
const service_provider_sid = await createServiceProvider(request);
const phone_number_sid = await createPhoneNumber(request, voip_carrier_sid);
/* add an account */
result = await request.post('/Accounts', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: 'daveh',
service_provider_sid
}
});
t.ok(result.statusCode === 201, 'successfully created account');
const sid = result.body.sid;
/* query all accounts */
result = await request.get('/Accounts', {
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 , 'successfully queried all accounts');
/* query one accounts */
result = await request.get(`/Accounts/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.name === 'daveh' , 'successfully retrieved account by sid');
/* update accounts */
result = await request.put(`/Accounts/${sid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
name: 'robb'
}
});
t.ok(result.statusCode === 204, 'successfully updated account');
/* assign phone number to account */
result = await request.put(`/PhoneNumbers/${phone_number_sid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
account_sid: sid
}
});
t.ok(result.statusCode === 204, 'successfully assigned phone number to account');
/* cannot delete account that has phone numbers assigned */
result = await request.delete(`/Accounts/${sid}`, {
auth: authAdmin,
resolveWithFullResponse: true,
simple: false,
json: true
});
t.ok(result.statusCode === 422 && result.body.msg === 'cannot delete account with phone numbers', 'cannot delete account with phone numbers');
/* delete account */
await request.delete(`/PhoneNumbers/${phone_number_sid}`, {auth: authAdmin});
result = await request.delete(`/Accounts/${sid}`, {
auth: authAdmin,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted account after removing phone number');
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
t.end();
}
catch (err) {
//console.error(err);
t.end(err);
}
});

109
test/applications.js Normal file
View File

@@ -0,0 +1,109 @@
const test = require('tape').test ;
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const {createVoipCarrier, createServiceProvider,
createPhoneNumber, createAccount, deleteObjectBySid
} = require('./utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('application tests', async(t) => {
const app = require('../app');
let sid;
try {
let result;
/* add service provider, phone number, and voip carrier */
const voip_carrier_sid = await createVoipCarrier(request);
const service_provider_sid = await createServiceProvider(request);
const phone_number_sid = await createPhoneNumber(request, voip_carrier_sid);
const account_sid = await createAccount(request, service_provider_sid);
/* add an application */
result = await request.post('/Applications', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: 'daveh',
account_sid,
call_hook: 'http://example.com',
call_status_hook: 'http://example.com'
}
});
t.ok(result.statusCode === 201, 'successfully created application');
const sid = result.body.sid;
/* query all applications */
result = await request.get('/Applications', {
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 , 'successfully queried all applications');
/* query one applications */
result = await request.get(`/Applications/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.name === 'daveh' , 'successfully retrieved application by sid');
/* update applications */
result = await request.put(`/Applications/${sid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
call_hook: 'http://example2.com'
}
});
t.ok(result.statusCode === 204, 'successfully updated application');
/* assign phone number to application */
result = await request.put(`/PhoneNumbers/${phone_number_sid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
application_sid: sid,
account_sid
}
});
t.ok(result.statusCode === 204, 'successfully assigned phone number to application');
/* delete application */
result = await request.delete(`/Applications/${sid}`, {
auth: authAdmin,
resolveWithFullResponse: true,
simple: false,
json: true
});
//console.log(results);
t.ok(result.statusCode === 422, 'cannot delete application with phone numbers');
/* delete application */
await request.delete(`/PhoneNumbers/${phone_number_sid}`, {auth: authAdmin});
result = await request.delete(`/Applications/${sid}`, {
auth: authAdmin,
resolveWithFullResponse: true,
});
//console.log(result);
t.ok(result.statusCode === 204, 'successfully deleted application after removing phone number');
await deleteObjectBySid(request, '/Accounts', account_sid);
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid);
t.end();
}
catch (err) {
//console.error(err);
t.end(err);
}
});

View File

@@ -0,0 +1,25 @@
version: '3'
networks:
jambones-api-server:
driver: bridge
ipam:
config:
- subnet: 172.22.0.0/16
services:
mysql:
image: mysql:8
environment:
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
MYSQL_DATABASE: jambones
MYSQL_USER: jambones
MYSQL_PASSWORD: jambones
command: --default-authentication-plugin=mysql_native_password
ports:
- "3406:3306/tcp"
volumes:
- ../db:/tmp
networks:
jambones-api-server:
ipv4_address: 172.22.0.10

62
test/docker-start.js Normal file
View File

@@ -0,0 +1,62 @@
const test = require('tape').test ;
const exec = require('child_process').exec ;
test('starting docker network..', (t) => {
exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => {
if (err) t.end(err);
console.log('docker network started, giving extra time to create test mysql database...');
testMysql(60000, (err) => {
if (err) {
exec(`docker logs test_mysql_1`, (err, stdout, stderr) => {
console.log(stdout);
console.log(stderr);
});
}
else t.pass('successfully connected to mysql');
setTimeout(() => t.end(), 2000);
});
});
});
test('creating schema', (t) => {
exec('docker exec test_mysql_1 mysql -h localhost -u jambones -D jambones -pjambones -e "source /tmp/jambones-sql.sql"', (err, stdout, stderr) => {
if (!err) t.pass('successfully created schema');
else {
console.log(stderr);
console.log(stdout);
}
t.end(err);
});
});
test('creating initial auth token', (t) => {
exec('docker exec test_mysql_1 mysql -h localhost -u jambones -D jambones -pjambones -e "source /tmp/create-admin-token.sql"', (err, stdout, stderr) => {
if (!err) t.pass('successfully created auth token');
else {
console.log(stderr);
console.log(stdout);
}
t.end(err);
});
});
function testMysql(timeout, callback) {
const retryTimer = setInterval(() => {
exec('docker exec test_mysql_1 mysql -h localhost -u jambones -D jambones -pjambones -e "SELECT 1"', (err, stdout, stderr) => {
if (!err) {
clearTimeout(timeoutTimer);
clearInterval(retryTimer);
return callback(null);
}
//console.log(`failed connecting (err): ${stderr}`);
//console.log(`failed connecting (out): ${stdout}`);
});
}, 4000);
const timeoutTimer = setTimeout(() => {
clearInterval(retryTimer);
callback('timeout connecting to mysql');
}, timeout);
}

11
test/docker-stop.js Normal file
View File

@@ -0,0 +1,11 @@
const test = require('tape').test ;
const exec = require('child_process').exec ;
test('stopping docker network..', (t) => {
t.timeoutAfter(10000);
exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml down`, (err, stdout, stderr) => {
//console.log(`stderr: ${stderr}`);
process.exit(0);
});
t.end() ;
});

7
test/index.js Normal file
View File

@@ -0,0 +1,7 @@
require('./docker-start');
require('./service-providers');
require('./voip-carriers');
require('./accounts');
require('./phone-numbers');
require('./applications');
require('./docker-stop');

121
test/phone-numbers.js Normal file
View File

@@ -0,0 +1,121 @@
const test = require('tape').test ;
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
const {createVoipCarrier, deleteObjectBySid} = require('./utils');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('phone number tests', async(t) => {
const app = require('../app');
let sid;
try {
let result;
/* add service provider, phone number, and voip carrier */
const voip_carrier_sid = await createVoipCarrier(request);
/* provision phone number - failure case: voip_carrier_sid is required */
result = await request.post('/PhoneNumbers', {
resolveWithFullResponse: true,
simple: false,
auth: authAdmin,
json: true,
body: {
number: '15083084809'
}
});
t.ok(result.statusCode === 400 && result.body.msg === 'voip_carrier_sid is required',
'voip_carrier_sid is required when provisioning a phone number');
/* provision phone number - failure case: digits only */
result = await request.post('/PhoneNumbers', {
resolveWithFullResponse: true,
simple: false,
auth: authAdmin,
json: true,
body: {
number: '+15083084809',
voip_carrier_sid
}
});
t.ok(result.statusCode === 400 && result.body.msg === 'phone number must only include digits',
'service_provider_sid is required when provisioning a phone number');
/* provision phone number - failure case: insufficient digits */
result = await request.post('/PhoneNumbers', {
resolveWithFullResponse: true,
simple: false,
auth: authAdmin,
json: true,
body: {
number: '1508308',
voip_carrier_sid
}
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 400 && result.body.msg === 'invalid phone number: insufficient digits',
'invalid phone number: insufficient digits');
/* provision phone number - failure case: invalid US number */
result = await request.post('/PhoneNumbers', {
resolveWithFullResponse: true,
simple: false,
auth: authAdmin,
json: true,
body: {
number: '150830848091',
voip_carrier_sid
}
});
t.ok(result.statusCode === 400 && result.body.msg === 'invalid US phone number',
'invalid US phone number');
/* add a phone number */
result = await request.post('/PhoneNumbers', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
number: '16173333456',
voip_carrier_sid
}
});
t.ok(result.statusCode === 201, 'successfully created phone number');
const sid = result.body.sid;
/* query all phone numbers */
result = await request.get('/PhoneNumbers', {
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 , 'successfully queried all phone numbers');
/* query one phone numbers */
result = await request.get(`/PhoneNumbers/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.number === '16173333456' , 'successfully retrieved phone number by sid');
/* delete phone number */
result = await request.delete(`/PhoneNumbers/${sid}`, {
auth: authAdmin,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted phone number');
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
t.end();
}
catch (err) {
console.error(err);
t.end(err);
}
});

79
test/service-providers.js Normal file
View File

@@ -0,0 +1,79 @@
const test = require('tape').test ;
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('service provider tests', async(t) => {
const app = require('../app');
let sid;
try {
let result;
result = await request.post('/ServiceProviders', {
simple: false,
resolveWithFullResponse: true,
//auth: authAdmin,
json: true,
body: {
name: 'daveh'
}
});
t.ok(result.statusCode === 401, 'fails with 401 if Bearer token not supplied');
/* add a service provider */
result = await request.post('/ServiceProviders', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: 'daveh'
}
});
t.ok(result.statusCode === 201, 'successfully created service provider');
const sid = result.body.sid;
/* query all service providers */
result = await request.get('/ServiceProviders', {
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 , 'successfully queried all service providers');
/* query one service providers */
result = await request.get(`/ServiceProviders/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.name === 'daveh' , 'successfully retrieved service provider by sid');
/* update service providers */
result = await request.put(`/ServiceProviders/${sid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
name: 'robb'
}
});
t.ok(result.statusCode === 204, 'successfully updated service provider');
/* delete service providers */
result = await request.delete(`/ServiceProviders/${sid}`, {
auth: authAdmin,
resolveWithFullResponse: true,
});
t.ok(result.statusCode === 204, 'successfully deleted service provider');
t.end();
}
catch (err) {
console.error(err);
t.end(err);
}
});

64
test/utils.js Normal file
View File

@@ -0,0 +1,64 @@
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
async function createServiceProvider(request) {
const result = await request.post('/ServiceProviders', {
auth: authAdmin,
json: true,
body: {
name: 'daveh'
}
});
return result.sid;
}
async function createVoipCarrier(request) {
const result = await request.post('/VoipCarriers', {
auth: authAdmin,
json: true,
body: {
name: 'daveh'
}
});
return result.sid;
}
async function createPhoneNumber(request, voip_carrier_sid) {
const result = await request.post('/PhoneNumbers', {
auth: authAdmin,
json: true,
body: {
number: '15083084809',
voip_carrier_sid
}
});
return result.sid;
}
async function createAccount(request, service_provider_sid) {
const result = await request.post('/Accounts', {
auth: authAdmin,
json: true,
body: {
name: 'daveh',
service_provider_sid
}
});
return result.sid;
}
async function deleteObjectBySid(request, path, sid) {
const result = await request.delete(`${path}/${sid}`, {
auth: authAdmin,
});
return result;
}
module.exports = {
createServiceProvider,
createVoipCarrier,
createPhoneNumber,
createAccount,
deleteObjectBySid
};

109
test/voip-carriers.js Normal file
View File

@@ -0,0 +1,109 @@
const test = require('tape').test ;
const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de';
const authAdmin = {bearer: ADMIN_TOKEN};
const request = require('request-promise-native').defaults({
baseUrl: 'http://127.0.0.1:3000/v1'
});
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
test('voip carrier tests', async(t) => {
const app = require('../app');
let sid;
try {
let result;
/* add a voip carrier */
result = await request.post('/VoipCarriers', {
resolveWithFullResponse: true,
auth: authAdmin,
json: true,
body: {
name: 'daveh'
}
});
t.ok(result.statusCode === 201, 'successfully created voip carrier');
const sid = result.body.sid;
/* query all voip carriers */
result = await request.get('/VoipCarriers', {
auth: authAdmin,
json: true,
});
t.ok(result.length === 1 , 'successfully queried all voip carriers');
/* query one voip carriers */
result = await request.get(`/VoipCarriers/${sid}`, {
auth: authAdmin,
json: true,
});
t.ok(result.name === 'daveh' , 'successfully retrieved voip carrier by sid');
/* update voip carriers */
result = await request.put(`/VoipCarriers/${sid}`, {
auth: authAdmin,
json: true,
resolveWithFullResponse: true,
body: {
name: 'robb'
}
});
t.ok(result.statusCode === 204, 'successfully updated voip carrier');
/* provision a phone number for the voip carrier */
result = await request.post('/PhoneNumbers', {
resolveWithFullResponse: true,
simple: false,
auth: authAdmin,
json: true,
body: {
number: '15083084809',
voip_carrier_sid: sid
}
});
t.ok(result.statusCode === 201, 'successfully provisioned a phone number from voip carrier');
const phone_number_sid = result.body.sid;
/* can't delete a voip carrier that has phone numbers assigned */
result = await request.delete(`/VoipCarriers/${sid}`, {
resolveWithFullResponse: true,
simple: false,
json: true,
auth: authAdmin
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 422 && result.body.msg === 'cannot delete voip carrier with active phone numbers',
'cannot delete voip carrier with active phone numbers');
/* delete phone number */
result = await request.delete(`/PhoneNumbers/${phone_number_sid}`, {
resolveWithFullResponse: true,
simple: false,
json: true,
auth: authAdmin
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 204, 'successfully deleted phone number');
/* delete voip carrier */
result = await request.delete(`/VoipCarriers/${sid}`, {
resolveWithFullResponse: true,
simple: false,
json: true,
auth: authAdmin
});
//console.log(`result: ${JSON.stringify(result)}`);
t.ok(result.statusCode === 204, 'successfully deleted voip carrier');
t.end();
}
catch (err) {
console.error(err);
t.end(err);
}
});