Skip to content

Commit

Permalink
feat: add regional and edge support (twilio#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
eshanholtz committed May 6, 2020
1 parent 09d20da commit 42d7d6b
Show file tree
Hide file tree
Showing 7 changed files with 360 additions and 19 deletions.
1 change: 1 addition & 0 deletions src/base-commands/twilio-client-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class TwilioClientCommand extends BaseCommand {
buildClient(ClientClass) {
return new ClientClass(this.currentProfile.apiKey, this.currentProfile.apiSecret, {
accountSid: this.flags[CliFlags.ACCOUNT_SID] || this.currentProfile.accountSid,
edge: process.env.TWILIO_EDGE || this.userConfig.edge,
region: this.currentProfile.region,
httpClient: this.httpClient
});
Expand Down
21 changes: 15 additions & 6 deletions src/services/config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const fs = require('fs-extra');
const path = require('path');
const shell = require('shelljs');
const MessageTemplates = require('./messaging/templates');

const CLI_NAME = 'twilio-cli';
Expand All @@ -15,14 +14,21 @@ class ConfigDataProfile {

class ConfigData {
constructor() {
this.edge = undefined;
this.email = {};
this.prompts = {};
this.profiles = [];
this.activeProfile = null;
}

getProfileFromEnvironment() {
const { TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_API_KEY, TWILIO_API_SECRET } = process.env;
const {
TWILIO_ACCOUNT_SID,
TWILIO_AUTH_TOKEN,
TWILIO_API_KEY,
TWILIO_API_SECRET,
TWILIO_REGION
} = process.env;
if (!TWILIO_ACCOUNT_SID) return;

if (TWILIO_API_KEY && TWILIO_API_SECRET)
Expand All @@ -31,7 +37,8 @@ class ConfigData {
id: '${TWILIO_API_KEY}/${TWILIO_API_SECRET}',
accountSid: TWILIO_ACCOUNT_SID,
apiKey: TWILIO_API_KEY,
apiSecret: TWILIO_API_SECRET
apiSecret: TWILIO_API_SECRET,
region: TWILIO_REGION
};

if (TWILIO_AUTH_TOKEN)
Expand All @@ -40,7 +47,8 @@ class ConfigData {
id: '${TWILIO_ACCOUNT_SID}/${TWILIO_AUTH_TOKEN}',
accountSid: TWILIO_ACCOUNT_SID,
apiKey: TWILIO_ACCOUNT_SID,
apiSecret: TWILIO_AUTH_TOKEN
apiSecret: TWILIO_AUTH_TOKEN,
region: TWILIO_REGION
};
}

Expand Down Expand Up @@ -130,6 +138,7 @@ class ConfigData {
}

loadFromObject(configObj) {
this.edge = configObj.edge;
this.email = configObj.email || {};
this.prompts = configObj.prompts || {};
// Note the historical 'projects' naming.
Expand Down Expand Up @@ -163,15 +172,15 @@ class Config {

async save(configData) {
configData = {
edge: configData.edge,
email: configData.email,
prompts: configData.prompts,
// Note the historical 'projects' naming.
projects: configData.profiles,
activeProject: configData.activeProfile
};

// Migrate to 'fs.mkdirSync' with 'recursive: true' when no longer supporting Node8.
shell.mkdir('-p', this.configDir);
fs.mkdirSync(this.configDir, { recursive: true });
await fs.writeJSON(this.filePath, configData, { flag: 'w' });

return MessageTemplates.configSaved({ path: this.filePath });
Expand Down
32 changes: 21 additions & 11 deletions src/services/open-api-client.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const url = require('url');
const { logger } = require('./messaging/logging');
const { doesObjectHaveProperty } = require('./javascript-utilities');
const JsonSchemaConverter = require('./api-schema/json-converter');
Expand Down Expand Up @@ -42,20 +43,13 @@ class OpenApiClient {
if (!opts.host) {
opts.host = path.server;
}

if (opts.region) {
const parts = opts.host.split('.');

// From 'https://api.twilio.com/' to 'https://api.{region}.twilio.com/'
if (parts.length > 1 && parts[1] !== opts.region) {
parts.splice(1, 0, opts.region);
opts.host = parts.join('.');
}
}

opts.uri = opts.host + opts.uri;
}

const uri = new url.URL(opts.uri);
uri.hostname = this.getHost(uri.hostname, opts);
opts.uri = uri.href;

opts.params = (isPost ? null : params);
opts.data = (isPost ? params : null);

Expand Down Expand Up @@ -97,6 +91,22 @@ class OpenApiClient {
});
}

getHost(host, opts) {
if (opts.region || opts.edge) {
const domain = host.split('.').slice(-2).join('.');
const prefix = host.split('.' + domain)[0];
let [product, edge, region] = prefix.split('.');
if (edge && !region) {
region = edge;
edge = undefined;
}
edge = opts.edge || edge;
region = opts.region || region || (opts.edge && 'us1');
return [product, edge, region, domain].filter(part => part).join('.');
}
return host;
}

parseResponse(domain, operation, response, requestOpts) {
if (response.body) {
const responseSchema = this.getResponseSchema(domain, operation, response.statusCode, requestOpts.headers.Accept);
Expand Down
7 changes: 5 additions & 2 deletions src/services/twilio-api/twilio-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class TwilioApiClient {
this.username = username;
this.password = password;
this.accountSid = opts.accountSid || this.username;
this.edge = opts.edge;
this.region = opts.region;

this.apiClient = new OpenApiClient({
Expand Down Expand Up @@ -149,7 +150,8 @@ class TwilioApiClient {
* @param {string} opts.method - The http method
* @param {string} opts.path - The request path
* @param {string} [opts.host] - The request host
* @param {string} [opts.region] - The request region
* @param {string} [opts.edge] - The request edge. Defaults to none.
* @param {string} [opts.region] - The request region. Default to us1 if edge defined
* @param {string} [opts.uri] - The request uri
* @param {string} [opts.username] - The username used for auth
* @param {string} [opts.password] - The password used for auth
Expand All @@ -164,7 +166,6 @@ class TwilioApiClient {

opts.username = opts.username || this.username;
opts.password = opts.password || this.password;
opts.region = opts.region || this.region;
opts.headers = opts.headers || {};
opts.data = opts.data || {};
opts.pathParams = opts.pathParams || {};
Expand All @@ -186,6 +187,8 @@ class TwilioApiClient {
}
}

opts.edge = opts.edge || this.edge;
opts.region = opts.region || this.region;
return this.apiClient.request(opts);
}
}
Expand Down
70 changes: 70 additions & 0 deletions test/base-commands/twilio-client-command.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const { expect, test, constants } = require('@twilio/cli-test');
const TwilioClientCommand = require('../../src/base-commands/twilio-client-command');
const { Config, ConfigData } = require('../../src/services/config');

const ORIGINAL_ENV = process.env;

describe('base-commands', () => {
describe('twilio-client-command', () => {
class TestClientCommand extends TwilioClientCommand {
Expand Down Expand Up @@ -65,6 +67,7 @@ describe('base-commands', () => {
expect(ctx.testCmd.twilioClient.username).to.equal(constants.FAKE_API_KEY);
expect(ctx.testCmd.twilioClient.password).to.equal(constants.FAKE_API_SECRET + 'MyFirstProfile');
expect(ctx.testCmd.twilioClient.region).to.equal(undefined);
expect(ctx.testCmd.twilioClient.edge).to.equal(undefined);
});

setUpTest(['-l', 'debug', '--account-sid', 'ACbaccbaccbaccbaccbaccbaccbaccbacc'], { commandClass: AccountSidClientCommand }).it(
Expand All @@ -75,6 +78,7 @@ describe('base-commands', () => {
expect(ctx.testCmd.twilioClient.username).to.equal(constants.FAKE_API_KEY);
expect(ctx.testCmd.twilioClient.password).to.equal(constants.FAKE_API_SECRET + 'MyFirstProfile');
expect(ctx.testCmd.twilioClient.region).to.equal(undefined);
expect(ctx.testCmd.twilioClient.edge).to.equal(undefined);
}
);

Expand Down Expand Up @@ -230,5 +234,71 @@ describe('base-commands', () => {
expect(ctx.stderr).to.contain('A fake API error');
});
});

describe('regional and edge support', () => {
const envTest = (
args = [],
{ envRegion, envEdge, configRegion = 'configRegion', configEdge } = {}
) => {
return test
.do(ctx => {
ctx.userConfig = new ConfigData();
ctx.userConfig.edge = configEdge;

if (envRegion) {
process.env.TWILIO_REGION = envRegion;
process.env.TWILIO_ACCOUNT_SID = constants.FAKE_ACCOUNT_SID;
process.env.TWILIO_AUTH_TOKEN = constants.FAKE_API_SECRET;
}
if (envEdge) {
process.env.TWILIO_EDGE = envEdge;
}

ctx.userConfig.addProfile('default-profile', constants.FAKE_ACCOUNT_SID);
ctx.userConfig.addProfile('region-edge-testing', constants.FAKE_ACCOUNT_SID, configRegion);
})
.twilioCliEnv(Config)
.do(async ctx => {
ctx.testCmd = new TwilioClientCommand(args, ctx.fakeConfig);
ctx.testCmd.secureStorage =
{
async getCredentials(profileId) {
return {
apiKey: constants.FAKE_API_KEY,
apiSecret: constants.FAKE_API_SECRET + profileId
};
}
};

// This is essentially what oclif does behind the scenes.
try {
await ctx.testCmd.run();
} catch (error) {
await ctx.testCmd.catch(error);
}
process.env = ORIGINAL_ENV;
});
};

envTest([], { configEdge: 'edge' }).it('should use the config edge when defined', ctx => {
expect(ctx.testCmd.twilioApiClient.edge).to.equal('edge');
expect(ctx.testCmd.twilioApiClient.region).to.be.undefined;
});

envTest(['-p', 'region-edge-testing']).it('should use the config region when defined', ctx => {
expect(ctx.testCmd.twilioApiClient.region).to.equal('configRegion');
expect(ctx.testCmd.twilioApiClient.edge).to.be.undefined;
});

envTest([], { envRegion: 'region' }).it('should use the env region over a config region', ctx => {
expect(ctx.testCmd.twilioApiClient.region).to.equal('region');
expect(ctx.testCmd.twilioApiClient.edge).to.be.undefined;
});

envTest([], { configEdge: 'configEdge', envEdge: 'edge', envRegion: 'region' }).it('should use the env edge over a config edge', ctx => {
expect(ctx.testCmd.twilioApiClient.edge).to.equal('edge');
expect(ctx.testCmd.twilioApiClient.region).to.equal('region');
});
});
});
});
24 changes: 24 additions & 0 deletions test/services/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('services', () => {
const profile = configData.getProfileById('DOES_NOT_EXIST');
expect(profile).to.be.undefined;
});

test.it('should return undefined if no profiles, even with env vars', () => {
const configData = new ConfigData();
process.env.TWILIO_ACCOUNT_SID = constants.FAKE_ACCOUNT_SID;
Expand All @@ -43,6 +44,7 @@ describe('services', () => {
const profile = configData.getProfileById('DOES_NOT_EXIST');
expect(profile).to.be.undefined;
});

test.it('should return first profile if it exists, and no env vars', () => {
const configData = new ConfigData();
configData.addProfile('firstProfile', constants.FAKE_ACCOUNT_SID);
Expand All @@ -52,6 +54,7 @@ describe('services', () => {
expect(profile.apiKey).to.be.undefined;
expect(profile.apiSecret).to.be.undefined;
});

test.it('return the active profile if there are multiple profiles', () => {
const configData = new ConfigData();
configData.addProfile('firstProfile', constants.FAKE_ACCOUNT_SID);
Expand All @@ -64,6 +67,7 @@ describe('services', () => {
expect(profile.apiKey).to.be.undefined;
expect(profile.apiSecret).to.be.undefined;
});

test.it('should return profile populated from AccountSid/AuthToken env vars', () => {
const configData = new ConfigData();
configData.addProfile('envProfile', constants.FAKE_ACCOUNT_SID);
Expand Down Expand Up @@ -91,6 +95,21 @@ describe('services', () => {
expect(profile.apiKey).to.equal(constants.FAKE_API_KEY);
expect(profile.apiSecret).to.equal(constants.FAKE_API_SECRET);
});

test.it('should return profile populated with region env var', () => {
const configData = new ConfigData();
configData.addProfile('envProfile', constants.FAKE_ACCOUNT_SID);

process.env.TWILIO_ACCOUNT_SID = constants.FAKE_ACCOUNT_SID;
process.env.TWILIO_AUTH_TOKEN = FAKE_AUTH_TOKEN;
process.env.TWILIO_REGION = 'region';

const profile = configData.getProfileById();
expect(profile.accountSid).to.equal(constants.FAKE_ACCOUNT_SID);
expect(profile.apiKey).to.equal(constants.FAKE_ACCOUNT_SID);
expect(profile.apiSecret).to.equal(FAKE_AUTH_TOKEN);
expect(profile.region).to.equal('region');
});
});

describe('ConfigData.activeProfile', () => {
Expand All @@ -104,6 +123,7 @@ describe('services', () => {
expect(active.id).to.equal('firstProfile');
expect(active.accountSid).to.equal(constants.FAKE_ACCOUNT_SID);
});

test.it('should return active profile when active profile has been set', () => {
const configData = new ConfigData();
configData.addProfile('firstProfile', constants.FAKE_ACCOUNT_SID);
Expand All @@ -115,12 +135,14 @@ describe('services', () => {
expect(active.id).to.equal('secondProfile');
expect(active.accountSid).to.equal('new_account_SID');
});

test.it('should not allow the active profile to not exist', () => {
const configData = new ConfigData();
configData.addProfile('firstProfile', constants.FAKE_ACCOUNT_SID);
expect(configData.setActiveProfile('secondProfile')).to.be.undefined;
expect(configData.getActiveProfile().id).to.equal('firstProfile');
});

test.it('should return undefined if profile does not exist and there are no profiles configured', () => {
const configData = new ConfigData();
const active = configData.getActiveProfile();
Expand All @@ -143,6 +165,7 @@ describe('services', () => {

expect(configData.profiles.length).to.equal(originalLength);
});

test.it('removes profile', () => {
const configData = new ConfigData();
configData.addProfile('firstProfile', constants.FAKE_ACCOUNT_SID);
Expand All @@ -154,6 +177,7 @@ describe('services', () => {
expect(configData.profiles[1].id).to.equal('thirdProfile');
expect(configData.profiles[1].accountSid).to.equal('newest_account_SID');
});

test.it('removes active profile', () => {
const configData = new ConfigData();
configData.addProfile('firstProfile', constants.FAKE_ACCOUNT_SID);
Expand Down

0 comments on commit 42d7d6b

Please sign in to comment.