diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..956789d
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,8 @@
+sudo: required
+language: node_js
+node_js:
+ - "lts/*"
+services:
+ - docker
+script:
+ - npm test
diff --git a/app.js b/app.js
index f002fb5..d1a76a9 100644
--- a/app.js
+++ b/app.js
@@ -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;
diff --git a/config/test.json b/config/test.json
new file mode 100644
index 0000000..8ff1812
--- /dev/null
+++ b/config/test.json
@@ -0,0 +1,12 @@
+{
+ "logging": {
+ "level": "error"
+ },
+ "mysql": {
+ "host": "localhost",
+ "user": "jambones",
+ "password": "jambones",
+ "database": "jambones",
+ "port": 3406
+ }
+}
\ No newline at end of file
diff --git a/db/create-admin-token.sql b/db/create-admin-token.sql
new file mode 100644
index 0000000..32c80d4
--- /dev/null
+++ b/db/create-admin-token.sql
@@ -0,0 +1,2 @@
+insert into api_keys (api_key_sid, token)
+values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
\ No newline at end of file
diff --git a/db/jambones-sql.sql b/db/jambones-sql.sql
index 971e019..318935b 100644
--- a/db/jambones-sql.sql
+++ b/db/jambones-sql.sql
@@ -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`);
diff --git a/db/jambones.sqs b/db/jambones.sqs
index 3b00699..ac5aa84 100644
--- a/db/jambones.sqs
+++ b/db/jambones.sqs
@@ -111,7 +111,7 @@
266.00
80.00
- 14
+ 13
@@ -204,58 +204,6 @@
-
-
-
-
-
-
- 78.00
- 178.00
-
-
- 344.00
- 80.00
-
- 13
-
-
-
- 1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- voip_carrier_sid
- voip_carriers
-
-
- 4
- 1
-
-
-
-
-
-
-
-
-
-
-
@@ -746,7 +694,7 @@
83.00
- 322.00
+ 191.00
331.00
@@ -771,6 +719,20 @@
+
+
+
+ voip_carrier_sid
+ voip_carriers
+
+
+ 4
+ 1
+
+
+
+
+
@@ -779,10 +741,10 @@
4
- 1
+ 2
-
+
@@ -801,13 +763,6 @@
-
-
-
-
-
-
-
@@ -1036,17 +991,17 @@
-
+
-
-
-
+
+
+
-
+
diff --git a/lib/auth/index.js b/lib/auth/index.js
index fcdae12..15139d3 100644
--- a/lib/auth/index.js
+++ b/lib/auth/index.js
@@ -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});
});
diff --git a/lib/db/mysql.js b/lib/db/mysql.js
index 88c1ef7..9768060 100644
--- a/lib/db/mysql.js
+++ b/lib/db/mysql.js
@@ -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');
});
});
diff --git a/lib/models/account.js b/lib/models/account.js
new file mode 100644
index 0000000..6ed0105
--- /dev/null
+++ b/lib/models/account.js
@@ -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;
diff --git a/lib/models/application.js b/lib/models/application.js
new file mode 100644
index 0000000..f46fd2a
--- /dev/null
+++ b/lib/models/application.js
@@ -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;
diff --git a/lib/models/model.js b/lib/models/model.js
index bc72ee2..782b3ef 100644
--- a/lib/models/model.js
+++ b/lib/models/model.js
@@ -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) => {
diff --git a/lib/models/phone-number.js b/lib/models/phone-number.js
new file mode 100644
index 0000000..4e6295a
--- /dev/null
+++ b/lib/models/phone-number.js
@@ -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;
diff --git a/lib/models/voip-carrier.js b/lib/models/voip-carrier.js
new file mode 100644
index 0000000..bc505ba
--- /dev/null
+++ b/lib/models/voip-carrier.js
@@ -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;
diff --git a/lib/routes/api/accounts.js b/lib/routes/api/accounts.js
new file mode 100644
index 0000000..ae6a972
--- /dev/null
+++ b/lib/routes/api/accounts.js
@@ -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;
diff --git a/lib/routes/api/api-keys.js b/lib/routes/api/api-keys.js
index a2113a5..b9b8aed 100644
--- a/lib/routes/api/api-keys.js
+++ b/lib/routes/api/api-keys.js
@@ -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');
}
diff --git a/lib/routes/api/applications.js b/lib/routes/api/applications.js
new file mode 100644
index 0000000..973a1d3
--- /dev/null
+++ b/lib/routes/api/applications.js
@@ -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;
diff --git a/lib/routes/api/decorate.js b/lib/routes/api/decorate.js
index d3fd114..0181901 100644
--- a/lib/routes/api/decorate.js
+++ b/lib/routes/api/decorate.js
@@ -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();
diff --git a/lib/routes/api/error.js b/lib/routes/api/error.js
index 0e83292..57e5cf6 100644
--- a/lib/routes/api/error.js
+++ b/lib/routes/api/error.js
@@ -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');
diff --git a/lib/routes/api/index.js b/lib/routes/api/index.js
index 21c7b70..83d2e7e 100644
--- a/lib/routes/api/index.js
+++ b/lib/routes/api/index.js
@@ -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;
diff --git a/lib/routes/api/phone-numbers.js b/lib/routes/api/phone-numbers.js
new file mode 100644
index 0000000..7d7a6ba
--- /dev/null
+++ b/lib/routes/api/phone-numbers.js
@@ -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;
diff --git a/lib/routes/api/voip-carriers.js b/lib/routes/api/voip-carriers.js
new file mode 100644
index 0000000..e1e978c
--- /dev/null
+++ b/lib/routes/api/voip-carriers.js
@@ -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;
diff --git a/lib/swagger/swagger.yaml b/lib/swagger/swagger.yaml
index 0535c10..f9a6f67 100644
--- a/lib/swagger/swagger.yaml
+++ b/lib/swagger/swagger.yaml
@@ -64,6 +64,300 @@ paths:
404:
description: api key or account not found
+ /VoipCarriers:
+ post:
+ summary: create voip carrier
+ operationId: createVoipCarrier
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ description: voip carrier name
+ description:
+ type: string
+ required:
+ - name
+ responses:
+ 201:
+ description: voip carrier successfully created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SuccessfulAdd'
+ 400:
+ description: bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ 422:
+ description: unprocessable entity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ get:
+ summary: list voip carriers
+ operationId: listVoipCarriers
+ responses:
+ 200:
+ description: list of voip carriers
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/VoipCarrier'
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ /VoipCarriers/{VoipCarrierSid}:
+ parameters:
+ - name: VoipCarrierSid
+ in: path
+ required: true
+ style: simple
+ explode: false
+ schema:
+ type: string
+ delete:
+ summary: delete a voip carrier
+ operationId: deleteVoipCarrier
+ responses:
+ 204:
+ description: voip carrier successfully deleted
+ 404:
+ description: voip carrier not found
+ 422:
+ description: unprocessable entity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ example:
+ msg: a service provider with active accounts can not be deleted
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ get:
+ summary: retrieve voip carrier
+ operationId: getVoipCarrier
+ responses:
+ 200:
+ description: voip carrier found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VoipCarrier'
+ 404:
+ description: voip carrier not found
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ put:
+ summary: update voip carrier
+ operationId: updateVoipCarrier
+ parameters:
+ - name: VoipCarrierSid
+ in: path
+ required: true
+ style: simple
+ explode: false
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VoipCarrier'
+ responses:
+ 204:
+ description: voip carrier updated
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VoipCarrier'
+ 400:
+ description: bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ 404:
+ description: voip carrier not found
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+
+ /PhoneNumbers:
+ post:
+ summary: provision a phone number into inventory from a Voip Carrier
+ operationId: provisionPhoneNumber
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ number:
+ type: string
+ description: telephone number
+ voip_carrier_sid:
+ type: string
+ format: uuid
+ required:
+ - number
+ - voip_carrier_sid
+ responses:
+ 201:
+ description: phone number successfully provisioned
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/SuccessfulAdd'
+ 400:
+ description: bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ example:
+ msg: invalid telephone number format
+ 404:
+ description: voip carrier not found
+ 422:
+ description: unprocessable entity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ example:
+ msg: the phone number provided already exists in inventory
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ get:
+ summary: list phone numbers
+ operationId: listProvisionedPhoneNumbers
+ responses:
+ 200:
+ description: list of phone numbers
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/PhoneNumber'
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ /PhoneNumbers/{PhoneNumberSid}:
+ parameters:
+ - name: PhoneNumberSid
+ in: path
+ required: true
+ style: simple
+ explode: false
+ schema:
+ type: string
+ delete:
+ summary: delete a phone number
+ operationId: deletePhoneNumber
+ responses:
+ 204:
+ description: phone number successfully deleted
+ 404:
+ description: phone number not found
+ 422:
+ description: unprocessable entity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ example:
+ msg: phone number that is assigned to an account may not be deleted
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ get:
+ summary: retrieve phone number
+ operationId: getPhoneNumber
+ responses:
+ 200:
+ description: phone number found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PhoneNumber'
+ 404:
+ description: phone number not found
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ put:
+ summary: update phone number
+ operationId: updatePhoneNumber
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PhoneNumber'
+ responses:
+ 204:
+ description: phone number updated
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VoipCarrier'
+ 400:
+ description: bad request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ 404:
+ description: phone number not found
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+
/ServiceProviders:
post:
summary: create service provider
@@ -107,6 +401,13 @@ paths:
type: array
items:
$ref: '#/components/schemas/ServiceProvider'
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+
/ServiceProviders/{ServiceProviderSid}:
parameters:
- name: ServiceProviderSid
@@ -130,6 +431,13 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+
get:
summary: retrieve service provider
operationId: getServiceProvider
@@ -142,6 +450,13 @@ paths:
$ref: '#/components/schemas/ServiceProvider'
404:
description: service provider not found
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+
put:
summary: update service provider
operationId: updateServiceProvider
@@ -161,14 +476,14 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GeneralError'
- /ServiceProviders/{ServiceProviderSid}/Accounts:
- parameters:
- - name: ServiceProviderSid
- in: path
- required: true
- schema:
- type: string
- format: uuid
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+
+ /Accounts:
post:
summary: create an account
operationId: createAccount
@@ -182,34 +497,48 @@ paths:
type: string
description: account name
example: foobar
- sipRealm:
+ sip_realm:
type: string
description: sip realm for registration
example: sip.mycompany.com
- registrationUrl:
+ registration_hook:
type: string
format: url
description: authentication webhook for registration
example: https://mycompany.com
+ service_provider_sid:
+ type: string
+ format: uuid
+ example: 85f9c036-ba61-4f28-b2f5-617c23fa68ff
required:
- name
+ - service_provider_sid
responses:
201:
description: account successfully created
content:
application/json:
schema:
- required:
- - accountSid
- properties:
- accountSid:
- type: string
- format: uuid
- example: 2531329f-fb09-4ef7-887e-84e648214436
+ $ref: '#/components/schemas/SuccessfulAdd'
400:
description: bad request
- 409:
- description: account with this name already exists
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ 422:
+ description: unprocessable entity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+
callbacks:
onRegistrationAttempt:
'{$request.body#/registrationUrl}/auth':
@@ -314,7 +643,7 @@ paths:
address will be blacklisted by the platform.
get:
- summary: list accounts for a service provider
+ summary: list accounts
operationId: listAccounts
responses:
200:
@@ -325,14 +654,15 @@ paths:
type: array
items:
$ref: '#/components/schemas/Account'
- /ServiceProviders/{ServiceProviderSid}/Accounts/{AccountSid}:
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+
+ /Accounts/{AccountSid}:
parameters:
- - name: ServiceProviderSid
- in: path
- required: true
- schema:
- type: string
- format: uuid
- name: AccountSid
in: path
required: true
@@ -343,181 +673,71 @@ paths:
summary: delete an account
operationId: deleteAccount
responses:
- 200:
+ 204:
description: account successfully deleted
404:
description: account not found
- 409:
- description: account with applications or phone numbers can not be deleted
+ 422:
+ description: unprocessable entity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ get:
+ summary: retrieve account
+ operationId: getAccount
+ responses:
+ 200:
+ description: account found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Account'
+ 404:
+ description: account not found
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
- /VoipCarriers:
- post:
- summary: create voip carrier
- operationId: createVoipCarrier
- requestBody:
- content:
- application/json:
- schema:
- type: object
- properties:
- name:
- type: string
- description: voip carrier name
- description:
- type: string
- required:
- - name
- responses:
- 201:
- description: voip carrier successfully created
- content:
- application/json:
- schema:
- required:
- - voipCarrierSid
- properties:
- accountSid:
- type: string
- format: uuid
- example: 2531329f-fb09-4ef7-887e-84e648214436
- 400:
- description: bad request
- 409:
- description: an existing voip carrier already exists with this name
- get:
- summary: list voip carriers
- operationId: listVoipCarriers
- responses:
- 200:
- description: list of voip carriers
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/VoipCarrier'
- /VoipCarriers/{VoipCarrierSid}:
- parameters:
- - name: VoipCarrierSid
- in: path
- required: true
- style: simple
- explode: false
- schema:
- type: string
- delete:
- summary: delete a voip carrier
- operationId: deleteVoipCarrier
- responses:
- 200:
- description: voip carrier successfully deleted
- 404:
- description: voip carrier not found
- 409:
- description: voip carrier with active phone numbers can not be deleted
- get:
- summary: retrieve voip carrier
- operationId: getVoipCarrier
- responses:
- 200:
- description: voip carrier found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VoipCarrier'
- 404:
- description: voip carrier not found
put:
- summary: update voip carrier
- operationId: updateVoipCarrier
- parameters:
- - name: VoipCarrierSid
- in: path
- required: true
- style: simple
- explode: false
- schema:
- type: string
+ summary: update account
+ operationId: updateAccount
requestBody:
content:
application/json:
schema:
- $ref: '#/components/schemas/VoipCarrier'
+ $ref: '#/components/schemas/Account'
responses:
- 200:
- description: voip carrier updated
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VoipCarrier'
+ 204:
+ description: account updated
404:
- description: voip carrier not found
- /VoipCarriers/{VoipCarrierSid}/PhoneNumbers:
- parameters:
- - name: VoipCarrierSid
- in: path
- required: true
- schema:
- type: string
- format: uuid
- post:
- summary: provision a phone number into inventory from a Voip Carrier
- operationId: provisionPhoneNumber
- requestBody:
- content:
- application/json:
- schema:
- type: object
- properties:
- number:
- type: string
- description: telephone number
- description:
- type: string
- required:
- - number
- responses:
- 201:
- description: phone number successfully provisioned into inventory
+ description: account not found
+ 422:
+ description: unprocessable entity
content:
application/json:
schema:
- required:
- - phoneNumberSid
- properties:
- phoneNumberSid:
- type: string
- format: uuid
- example: 2531329f-fb09-4ef7-887e-84e648214436
- 400:
- description: bad request
- 409:
- description: the specified phone number already exists in inventory
- get:
- summary: list phone numbers for a carrier
- operationId: listProvisionedPhoneNumbers
- responses:
- 200:
- description: list of phone numbers
+ $ref: '#/components/schemas/GeneralError'
+ 500:
+ description: system error
content:
application/json:
schema:
- type: array
- items:
- $ref: '#/components/schemas/PhoneNumber'
+ $ref: '#/components/schemas/GeneralError'
- /Accounts/{AccountSid}/Applications:
+ /Applications:
post:
summary: create application
operationId: createApplication
- parameters:
- - name: AccountSid
- in: path
- required: true
- style: simple
- explode: false
- schema:
- type: string
requestBody:
content:
application/json:
@@ -527,70 +747,68 @@ paths:
name:
type: string
description: application name
- description:
+ account_sid:
type: string
- statusUrl:
+ format: uuid
+ call_hook:
+ type: string
+ format: url
+ description: webhook to invoke when call is received
+ call_status_hook:
type: string
format: url
description: webhook to pass call status updates to
- voiceUrl:
- type: string
- format: url
- description: webhook to call when call is received
- voiceFallbackUrl:
- type: string
- format: url
- description: fallback webook url
required:
- name
- - statusUrl
- - voiceUrl
+ - account_sid
+ - call_hook
+ - call_status_hook
responses:
201:
description: application successfully created
content:
application/json:
schema:
- required:
- - ApplicationSid
- properties:
- accountSid:
- type: string
- format: uuid
- example: 2531329f-fb09-4ef7-887e-84e648214436
+ $ref: '#/components/schemas/SuccessfulAdd'
400:
description: bad request
- 409:
- description: an existing application already exists with this name
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ 422:
+ description: unprocessable entity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
get:
- summary: list applications for an account
+ summary: list applications
operationId: listApplications
- parameters:
- - name: AccountSid
- in: path
- required: true
- style: simple
- explode: false
- schema:
- type: string
responses:
200:
- description: retrieve applications for the account
+ description: list of applications
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Application'
- /Accounts/{AccountSid}/Applications/{ApplicationSid}:
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+
+ /Applications/{ApplicationSid}:
parameters:
- - name: AccountSid
- in: path
- required: true
- style: simple
- explode: false
- schema:
- type: string
- name: ApplicationSid
in: path
required: true
@@ -602,13 +820,24 @@ paths:
summary: delete an application
operationId: deleteApplication
responses:
- 200:
+ 204:
description: application successfully deleted
404:
description: application not found
+ 422:
+ description: unprocessable entity
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
get:
- summary: retrieve application
- operationId: getApplication
+ summary: retrieve an application
responses:
200:
description: application found
@@ -618,137 +847,38 @@ paths:
$ref: '#/components/schemas/Application'
404:
description: application not found
+ 500:
+ description: system error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GeneralError'
put:
summary: update application
operationId: updateApplication
- parameters:
- - name: AccountSid
- in: path
- required: true
- style: simple
- explode: false
- schema:
- type: string
- - name: ApplicationSid
- in: path
- required: true
- style: simple
- explode: false
- schema:
- type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Application'
responses:
- 200:
+ 204:
description: application updated
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/Application'
404:
description: application not found
- /Accounts/{AccountSid}/Applications/{ApplicationSid}/PhoneNumbers/{PhoneNumberSid}:
- parameters:
- - name: AccountSid
- in: path
- required: true
- schema:
- type: string
- format: uuid
- - name: ApplicationSid
- in: path
- required: true
- schema:
- type: string
- format: uuid
- - name: PhoneNumberSid
- in: path
- required: true
- schema:
- type: string
- format: uuid
- delete:
- summary: remove a phone number from an application
- operationId: removePhoneNumberFromApplication
- responses:
- 200:
- description: phone number removed from this application
- 404:
- description: phone number not found or was not assigned to this application
- put:
- summary: provision a phone number for an Application
- operationId: assignPhoneNumberToApplication
- responses:
- 200:
- description: phone number successfully assigned to application
+ 422:
+ description: unprocessable entity
content:
application/json:
schema:
- $ref: '#/components/schemas/PhoneNumber'
- 400:
- description: bad request
- 409:
- description: the specified phone number is already assigned to another application or account
-
- /Accounts/{AccountSid}/Apikeys:
- post:
- summary: create an account level api key
- operationId: createAccountApikey
- parameters:
- - name: AccountSid
- in: path
- required: true
- style: simple
- explode: false
- schema:
- type: string
- responses:
- 201:
- description: api key successfully created
+ $ref: '#/components/schemas/GeneralError'
+ 500:
+ description: system error
content:
application/json:
schema:
- required:
- - apiKey
- - apiKeySid
- properties:
- api_key_sid:
- type: string
- description: system identifier for api key that was created
- format: uuid
- example: 7531328e-eb08-4eff-887e-84e648214872
- token:
- type: string
- description: api key authorization token
- format: uuid
- example: 2531329f-fb09-4ef7-887e-84e648214436
- 404:
- description: Account not found
- /Accounts/{AccountSid}/Apikeys/{ApiKeySid}:
- delete:
- summary: delete account-level api key
- operationId: deleteAccountApiKey
- parameters:
- - name: AccountSid
- in: path
- required: true
- style: simple
- explode: false
- schema:
- type: string
- - name: ApiKeySid
- in: path
- required: true
- schema:
- type: string
- responses:
- 200:
- description: api key deleted
- 404:
- description: api key or account not found
+ $ref: '#/components/schemas/GeneralError'
+
/Accounts/{AccountSid}/Applications/{ApplicationSid}/Calls:
post:
@@ -900,8 +1030,9 @@ components:
registration_hook:
type: string
format: url
- service_provider:
- $ref: '#/components/schemas/ServiceProvider'
+ service_provider_sid:
+ type: string
+ format: uuid
required:
- account_sid
- name
@@ -914,8 +1045,9 @@ components:
format: uuid
name:
type: string
- account:
- $ref: '#/components/schemas/Account'
+ account_sid:
+ type: string
+ format: uuid
call_hook:
type: string
format: url
@@ -925,7 +1057,7 @@ components:
required:
- application_sid
- name
- - account
+ - account_sid
- call_hook
- call_status_hook
PhoneNumber:
@@ -936,17 +1068,19 @@ components:
format: uuid
number:
type: string
- voip_carrier:
- $ref: '#/components/schemas/VoipCarrier'
- account:
- $ref: '#/components/schemas/Account'
- application:
- $ref: '#/components/schemas/Application'
+ voip_carrier_sid:
+ type: string
+ format: uuid
+ account_sid:
+ type: string
+ format: uuid
+ application_sid:
+ type: string
+ format: uuid
required:
- phone_number_sid
- number
- - voip_carrier
- - account
+ - voip_carrier_sid
Registration:
type: object
properties:
diff --git a/lib/utils/phone-number-syntax.js b/lib/utils/phone-number-syntax.js
new file mode 100644
index 0000000..9749632
--- /dev/null
+++ b/lib/utils/phone-number-syntax.js
@@ -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;
diff --git a/lib/utils/scopes.js b/lib/utils/scopes.js
deleted file mode 100644
index e73ec28..0000000
--- a/lib/utils/scopes.js
+++ /dev/null
@@ -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
-};
diff --git a/package.json b/package.json
index 028f2d3..e0b74ad 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/test/accounts.js b/test/accounts.js
new file mode 100644
index 0000000..100f27c
--- /dev/null
+++ b/test/accounts.js
@@ -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);
+ }
+});
+
diff --git a/test/applications.js b/test/applications.js
new file mode 100644
index 0000000..6943e07
--- /dev/null
+++ b/test/applications.js
@@ -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);
+ }
+});
+
diff --git a/test/docker-compose-testbed.yaml b/test/docker-compose-testbed.yaml
new file mode 100644
index 0000000..8b25144
--- /dev/null
+++ b/test/docker-compose-testbed.yaml
@@ -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
diff --git a/test/docker-start.js b/test/docker-start.js
new file mode 100644
index 0000000..eea941d
--- /dev/null
+++ b/test/docker-start.js
@@ -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);
+}
+
diff --git a/test/docker-stop.js b/test/docker-stop.js
new file mode 100644
index 0000000..c985de4
--- /dev/null
+++ b/test/docker-stop.js
@@ -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() ;
+});
diff --git a/test/index.js b/test/index.js
new file mode 100644
index 0000000..e2107c3
--- /dev/null
+++ b/test/index.js
@@ -0,0 +1,7 @@
+require('./docker-start');
+require('./service-providers');
+require('./voip-carriers');
+require('./accounts');
+require('./phone-numbers');
+require('./applications');
+require('./docker-stop');
diff --git a/test/phone-numbers.js b/test/phone-numbers.js
new file mode 100644
index 0000000..3b8dfc7
--- /dev/null
+++ b/test/phone-numbers.js
@@ -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);
+ }
+});
+
diff --git a/test/service-providers.js b/test/service-providers.js
new file mode 100644
index 0000000..2c6f0f6
--- /dev/null
+++ b/test/service-providers.js
@@ -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);
+ }
+});
+
diff --git a/test/utils.js b/test/utils.js
new file mode 100644
index 0000000..6bcb621
--- /dev/null
+++ b/test/utils.js
@@ -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
+};
diff --git a/test/voip-carriers.js b/test/voip-carriers.js
new file mode 100644
index 0000000..25c75a0
--- /dev/null
+++ b/test/voip-carriers.js
@@ -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);
+ }
+});
+