Skip to content

Commit

Permalink
feat(twitter): EN-6897: Add Twitter features. (#88)
Browse files Browse the repository at this point in the history
* Add functionality to save Twitter OAuth tokens.
* Add functionality to retrieve connected Twitter usernames.

Signed-off-by: Jeff Cuevas-Koch <[email protected]>

Co-authored-by: Jeff Cuevas-Koch <[email protected]>
  • Loading branch information
cuevaskoch and Jeff Cuevas-Koch authored Dec 15, 2020
1 parent 9f19d27 commit 79b7dfe
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 0 deletions.
52 changes: 52 additions & 0 deletions src/examples/manage_twitter_authentication.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import fetchMock from 'fetch-mock';
import Track from '../index';
import {
charlie,
twitter as mockTwitter,
} from '../mocks';

chai.should();
chai.use(chaiAsPromised);

describe('When saving a twitter oauth token', () => {
const api = new Track({ autoRenew: false });

beforeEach(() => charlie.setUpSuccessfulMock(api.client));
beforeEach(() => mockTwitter.setUpSuccessfulMock(api.client));
beforeEach(() => fetchMock.catch(503));
afterEach(fetchMock.restore);

it('should save a token', () => {
api.logIn({ username: '[email protected]', password: 'securepassword' });

const tokenPromise = api.customer('SYNC').twitterOAuth({
username: 'GMVSyncromatics',
token: 'example_oauth_token',
secret: 'example_oauth_secret',
profile_image_url: 'https://example.com/gmvsyncromatics.png',
}).update().then(success => success); // check success

return tokenPromise;
});
});

describe('When retrieving the connected twitter username', () => {
const api = new Track({ autoRenew: false });

beforeEach(() => charlie.setUpSuccessfulMock(api.client));
beforeEach(() => mockTwitter.setUpSuccessfulMock(api.client));
beforeEach(() => fetchMock.catch(503));
afterEach(fetchMock.restore);

it('should save a token', () => {
api.logIn({ username: '[email protected]', password: 'securepassword' });

const usernamePromise = api.customer('SYNC').twitterUsername()
.fetch()
.then(username => username); // Do things with username

return usernamePromise;
});
});
1 change: 1 addition & 0 deletions src/mocks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export { default as signs } from './signs';
export { default as stops } from './stops';
export { default as tags } from './tags';
export { default as trips } from './trips';
export { default as twitter } from './twitter';
export { default as users } from './users';
export { default as vehicles } from './vehicles';
export { default as voipTickets } from './voipTicket';
Expand Down
25 changes: 25 additions & 0 deletions src/mocks/twitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import fetchMock from 'fetch-mock';
import Client from '../Client';

const twitter = {
setUpSuccessfulMock: (client) => {
const oauthUrl = '/1/SYNC/twitter/oauth';
const oauthMutationResponse = () => new Response(undefined);

const usernameUrl = '/1/SYNC/twitter/username';
const usernameResponse = () => new Response(Client.toBlob({
href: '/1/SYNC/twitter/username',
username: 'GMVSYNC',
is_valid: true,
profile_image_url: 'https://example.com/gmvsync.png',
}));

fetchMock
.get(client.resolve(usernameUrl), usernameResponse)
.put(client.resolve(oauthUrl), oauthMutationResponse)
.delete(client.resolve(oauthUrl), oauthMutationResponse);
},
};

export default twitter;
18 changes: 18 additions & 0 deletions src/resources/Customer.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import StopsContext from './StopsContext';
import Tag from './Tag';
import TagsContext from './TagsContext';
import Trip from './Trip';
import TwitterOAuth from './TwitterOAuth';
import TwitterUsername from './TwitterUsername';
import Vehicle from './Vehicle';
import VehiclesContext from './VehiclesContext';
import VoipTicket from './VoipTicket';
Expand Down Expand Up @@ -419,6 +421,22 @@ class Customer extends Resource {
return this.resource(Trip, Trip.makeHref(this.code, id));
}

/**
* Gets a TwitterOAuth resource
* @returns {TwitterOAuth} TwitterOAuth resource
*/
twitterOAuth() {
return this.resource(TwitterOAuth, TwitterOAuth.makeHref(this.code));
}

/**
* Gets a TwitterUsername resource
* @returns {TwitterUsername} TwitterUsername resource
*/
twitterUsername() {
return this.resource(TwitterUsername, TwitterUsername.makeHref(this.code));
}

/**
* Gets a context for querying users that have access to this customer.
* Note that returned users might also have access to other customers,
Expand Down
4 changes: 4 additions & 0 deletions src/resources/Customer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import StopsContext from './StopsContext';
import Tag from './Tag';
import TagsContext from './TagsContext';
import Trip from './Trip';
import TwitterOAuth from './TwitterOAuth';
import TwitterUsername from './TwitterUsername';
import Vehicle from './Vehicle';
import VehiclesContext from './VehiclesContext';
import VoipTicket from './VoipTicket';
Expand Down Expand Up @@ -82,6 +84,8 @@ describe('When getting resources related to a customer', () => {
it('should allow tags to be searched', () => customer.tags().should.be.instanceof(TagsContext));
it('should allow a tag to be retrieved', () => customer.tag().should.be.instanceof(Tag));
it('should allow a trip to be retrieved', () => customer.trip().should.be.instanceof(Trip));
it('should allow a twitter oauth token to be created', () => customer.twitterOAuth().should.be.instanceof(TwitterOAuth));
it('should allow a twitter username to be retrieved', () => customer.twitterUsername().should.be.instanceof(TwitterUsername));
it('should allow users to be searched', () => customer.users().should.be.instanceOf(CustomerUsersContext));
it('should allow vehicles to be searched', () => customer.vehicles().should.be.instanceof(VehiclesContext));
it('should allow a vehicle to be retrieved', () => customer.vehicle().should.be.instanceof(Vehicle));
Expand Down
59 changes: 59 additions & 0 deletions src/resources/TwitterOAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Resource from './Resource';

/**
* Write-only Twitter OAuth resource
*/
class TwitterOAuth extends Resource {
/**
* Creates a new TwitterOAuth
*
* Will populate itself with the values given after the client parameter
* @param {Client} client Instance of pre-configured client
* @param {Object} rest The object to use in assigning values to this instance
*/
constructor(client, rest) {
super(client);
const { code, ...newProperties } = rest;
this.customerCode = code;
Object.assign(this, newProperties, {
hydrated: false,
});
}

/**
* Makes a href for a given customer code
* @param {string} customerCode Customer code
* @returns {string} URI to instance of TwitterOAuth
*/
static makeHref(customerCode) {
return {
href: `/1/${customerCode}/twitter/oauth`,
code: customerCode,
};
}

/**
* Saves data for this Twitter OAuth to the server
*
* Does not return the created object since TwitterOAuth
* is meant to be write-only.
* @returns {Promise} If successful, returns a completed promise.
*/
update() {
const { client, customerCode, hydrated, ...body } = this;
const { href } = TwitterOAuth.makeHref(customerCode);
return client.put(href, { body }).then(() => ({ success: true }));
}

/**
* Removes stored Twitter OAuth information via the client
* @returns {Promise} If successful, returns a resolved promise
*/
delete() {
const { client, customerCode } = this;
const { href } = TwitterOAuth.makeHref(customerCode);
return client.delete(href).then(() => {});
}
}

export default TwitterOAuth;
41 changes: 41 additions & 0 deletions src/resources/TwitterOAuth.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import fetchMock from 'fetch-mock';

import Client from '../Client';
import TwitterOAuth from './TwitterOAuth';
import { twitter as mockTwitter } from '../mocks';

chai.should();
chai.use(chaiAsPromised);

describe('When instantiating a TwitterOAuth based on customer', () => {
const client = new Client();
const oauth = new TwitterOAuth(client, TwitterOAuth.makeHref('SYNC'));

it('should set the href', () => oauth.href.should.equal('/1/SYNC/twitter/oauth'));
it('should not be hydrated', () => oauth.hydrated.should.equal(false));
});

describe('When updating an TwitterOAuth information for a customer', () => {
const client = new Client();

beforeEach(() => mockTwitter.setUpSuccessfulMock(client));
beforeEach(() => fetchMock.catch(503));
afterEach(fetchMock.restore);

let promise;
beforeEach(() => {
const oauth = new TwitterOAuth(client, {
code: 'SYNC',
username: 'GMVSyncromatics',
token: 'example_oauth_token',
secret: 'example_oauth_secret',
profile_image_url: 'https://example.com/gmvsyncromatics.png',
});
promise = oauth.update().then(updated => updated);
});

it('should resolve the promise', () => promise.should.be.fulfilled);
it('should return success', () => promise.then(v => v.success).should.eventually.equal(true));
});
48 changes: 48 additions & 0 deletions src/resources/TwitterUsername.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Resource from './Resource';

/**
* Read-only Twitter Username resource.
*/
class TwitterUsername extends Resource {
/**
* Creates a new TwitterUsername
*
* @param {Client} client Instance of pre-configured client
* @param {Object} rest The object to use in assigning values to this instance
*/
constructor(client, rest) {
super(client);
const { code, ...newProperties } = rest;
this.customerCode = code;
const hydrated = !Object.keys(newProperties).every(k => k === 'href' || k === 'customerCode');
Object.assign(this, newProperties, {
hydrated,
});
}

/**
* Makes a href for a given customer code
* @param {string} customerCode Customer code
* @returns {string} URI to instance of call
*/
static makeHref(customerCode) {
return {
code: customerCode,
href: `/1/${customerCode}/twitter/username`,
};
}

/**
* Fetches the data for this TwitterUsername via the client
* @returns {Promise} If successful, a hydrated instance of this TwitterUsername
*/
fetch() {
const { customerCode } = this;
const { href } = TwitterUsername.makeHref(customerCode);
return this.client.get(href)
.then(response => response.json())
.then(username => new TwitterUsername(this.client, { ...this, ...username }));
}
}

export default TwitterUsername;
40 changes: 40 additions & 0 deletions src/resources/TwitterUsername.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import fetchMock from 'fetch-mock';

import Client from '../Client';
import TwitterUsername from './TwitterUsername';
import { twitter as mockTwitter } from '../mocks';

chai.should();
chai.use(chaiAsPromised);

describe('When instantiating a TwitterUsername based on customer', () => {
const client = new Client();
const oauth = new TwitterUsername(client, TwitterUsername.makeHref('SYNC'));

it('should set the href', () => oauth.href.should.equal('/1/SYNC/twitter/username'));
it('should not be hydrated', () => oauth.hydrated.should.equal(false));
});

describe('When fetching a TwitterUsername for a customer', () => {
const client = new Client();

beforeEach(() => mockTwitter.setUpSuccessfulMock(client));
beforeEach(() => fetchMock.catch(503));
afterEach(fetchMock.restore);

let promise;
beforeEach(() => {
promise = new TwitterUsername(client, TwitterUsername.makeHref('SYNC'))
.fetch()
.then(username => username);
});

it('should resolve the promise', () => promise.should.be.fulfilled);
it('should set the href', () => promise.then(v => v.href).should.eventually.equal('/1/SYNC/twitter/username'));
it('should set the username', () => promise.then(v => v.username).should.eventually.equal('GMVSYNC'));
it('should set is_valid', () => promise.then(v => v.is_valid).should.eventually.equal(true));
it('should set the profile image url', () => promise.then(v => v.profile_image_url).should.eventually.equal('https://example.com/gmvsync.png'));
it('should be hydrated', () => promise.then(v => v.hydrated).should.eventually.equal(true));
});

0 comments on commit 79b7dfe

Please sign in to comment.