mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2025-12-19 05:47:46 +00:00
major updates and test suite
This commit is contained in:
8
.travis.yml
Normal file
8
.travis.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
sudo: required
|
||||
language: node_js
|
||||
node_js:
|
||||
- "lts/*"
|
||||
services:
|
||||
- docker
|
||||
script:
|
||||
- npm test
|
||||
5
app.js
5
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;
|
||||
|
||||
12
config/test.json
Normal file
12
config/test.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"logging": {
|
||||
"level": "error"
|
||||
},
|
||||
"mysql": {
|
||||
"host": "localhost",
|
||||
"user": "jambones",
|
||||
"password": "jambones",
|
||||
"database": "jambones",
|
||||
"port": 3406
|
||||
}
|
||||
}
|
||||
2
db/create-admin-token.sql
Normal file
2
db/create-admin-token.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
insert into api_keys (api_key_sid, token)
|
||||
values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de');
|
||||
@@ -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`);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
|
||||
@@ -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
36
lib/models/account.js
Normal 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
77
lib/models/application.js
Normal 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;
|
||||
@@ -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) => {
|
||||
|
||||
36
lib/models/phone-number.js
Normal file
36
lib/models/phone-number.js
Normal 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;
|
||||
27
lib/models/voip-carrier.js
Normal file
27
lib/models/voip-carrier.js
Normal 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;
|
||||
29
lib/routes/api/accounts.js
Normal file
29
lib/routes/api/accounts.js
Normal 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;
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
54
lib/routes/api/applications.js
Normal file
54
lib/routes/api/applications.js
Normal 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;
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
53
lib/routes/api/phone-numbers.js
Normal file
53
lib/routes/api/phone-numbers.js
Normal 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;
|
||||
17
lib/routes/api/voip-carriers.js
Normal file
17
lib/routes/api/voip-carriers.js
Normal 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
8
lib/utils/phone-number-syntax.js
Normal file
8
lib/utils/phone-number-syntax.js
Normal 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;
|
||||
@@ -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
|
||||
};
|
||||
14
package.json
14
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"
|
||||
}
|
||||
}
|
||||
|
||||
99
test/accounts.js
Normal file
99
test/accounts.js
Normal 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
109
test/applications.js
Normal 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);
|
||||
}
|
||||
});
|
||||
|
||||
25
test/docker-compose-testbed.yaml
Normal file
25
test/docker-compose-testbed.yaml
Normal 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
62
test/docker-start.js
Normal 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
11
test/docker-stop.js
Normal 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
7
test/index.js
Normal 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
121
test/phone-numbers.js
Normal 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
79
test/service-providers.js
Normal 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
64
test/utils.js
Normal 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
109
test/voip-carriers.js
Normal 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);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user