Skip to content

Commit

Permalink
feat: Add support for login with OAuth 2
Browse files Browse the repository at this point in the history
  • Loading branch information
Philip Mander committed Nov 5, 2018
1 parent 4968916 commit 2baf1eb
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 29 deletions.
58 changes: 55 additions & 3 deletions src/steps.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const chai = require('chai');
const request = require('superagent');
const Ajv = require('ajv');
const cookie = require('cookie');
const JSONPath = require('jsonpath-plus');
Expand Down Expand Up @@ -26,7 +27,54 @@ function registerSteps({ Given, When, Then }) {
* @function anonymous
*/
Given('I am anonymous', function () {
// nothing to do here
// remove any active oauth2 token
this.oauth2.token = null;
});

/**
* ### Given I obtain an access token from {string} using the credentials:
* Supports logging into using OAuth2 credentials, typically with the passwrod scheme
* Sessions (access tokens) will be stored and supported for subsequent requests
*
* @example
* Given I obtain an access token from {string} using the credentials:
* | client_id | harver |
* | client_secret | harver123 |
* | username | gerald |
* | password | foobar |
* | grant_type | password |
*
*/
Given('I obtain an access token from {string} using the credentials:', async function (url, credentialsTable) {
const credentials = credentialsTable.rowsHash();
const session = this.oauth2.sessions.find(session =>
credentials.client_id === session.credentials.client_id &&
credentials.username === session.credentials.username
);

// TODO: check if token has expired
let token;
if (!session) {
const res = await request
.post(this.baseUrl + this.replaceVars(url))
.type('form')
.send(credentials);

if (res.body.accessToken) {
token = res.body.accessToken;
this.oauth2.sessions.push({
credentials,
token,
});
} else {
throw new Error(`Could not authenticate with OAuth2:\n\t${res.body}`);
}
} else {
token = session.token;
}

// set a current token
this.oauth2.token = token || null;
});

/**
Expand Down Expand Up @@ -58,8 +106,12 @@ function registerSteps({ Given, When, Then }) {
*
* @function makeRequest
*/
When('I send a {string} request to {string}', function (method, path) {
this.req = this.currentAgent[method.toLowerCase()](this.baseUrl + path);
When('I send a {string} request to {string}', function (method, url) {
this.req = this.currentAgent[method.toLowerCase()](this.baseUrl + url);

if (this.oauth2.token) {
this.req.set('Authorization', `Bearer ${this.oauth2.token}`);
}

if (methodsWithBodies.includes(method)) {
this.req.set('Content-Type', 'application/json');
Expand Down
54 changes: 28 additions & 26 deletions src/world.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ class World {
const envFile = process.env.ENV_FILE || null;
this.envVars = envFile ? JSON.parse(readFileSync(resolve(process.cwd(), envFile))).values : [];
this.responseVars = [];

// keep a set of sessions
this.oauth2 = {
token: null, // currently active access token
sessions: [], // a store of used credentials and their access tokens
}
}

/**
Expand Down Expand Up @@ -98,35 +104,34 @@ class World {
return {};
}
}

replaceVars(val) {
const vars = [].concat(this.responseVars).concat(this.envVars);

if (!val || !vars.length) {
return val;
}

// cheeky way to easily replace on whole objects:
const placeHolders = vars.map(pair => pair.key).join('|');
const regex = new RegExp(`\{(${placeHolders})\}`, 'g');
return JSON.parse(JSON.stringify(val).replace(regex, (match, p1) => {
const matchPair = vars.find(pair => pair.key === p1);
return matchPair ? matchPair.value : match;
}));
}

/**
* Returns Super Agent middleware that replaces placeholders with
* variables
*/
replaceVariablesInitiator() {
function simpleReplace(val, regex, vars) {
if (!val) {
return val;
}

// cheeky way to easily replace on whole objects:
return JSON.parse(JSON.stringify(val).replace(regex, (match, p1) => {
const matchPair = vars.find(pair => pair.key === p1);
return matchPair ? matchPair.value : match;
}));
}

return req => {
const vars = [].concat(this.responseVars).concat(this.envVars);
if (!vars.length) {
return req;
}
const placeHolders = vars.map(pair => pair.key).join('|');
const placeHolderRegex = new RegExp(`\{(${placeHolders})\}`, 'g');
req.url = simpleReplace(req.url, placeHolderRegex, vars);
req.qs = simpleReplace(req.qs, placeHolderRegex, vars);
req.headers = simpleReplace(req.headers, placeHolderRegex, vars);
req.cookies = simpleReplace(req.cookies, placeHolderRegex, vars);
req._data = simpleReplace(req._data, placeHolderRegex, vars);
req.url = this.replaceVars(req.url);
req.qs = this.replaceVars(req.qs);
req.headers = this.replaceVars(req.headers);
req.cookies = this.replaceVars(req.cookies);
req._data = this.replaceVars(req._data);
return req;
};
}
Expand Down Expand Up @@ -155,9 +160,6 @@ class World {
* @param {} res A Superagent response object
*/
getResponseBody(res) {
if (res.header['content-type'] && res.header['content-type'].startsWith('text/html')) {
return JSON.parse(res.text);
}
return res.body;
}

Expand Down
13 changes: 13 additions & 0 deletions test/features/test-steps.feature
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,16 @@ Feature: API Testing Steps
Then I should receive a response with the status 200
And the response body json path at "$.name" should equal "Felix"

@oauth
Scenario: Testing OAuth support
Given I obtain an access token from '{base}/auth/token' using the credentials:
| client_id | 123 |
| username | jayani |
When I send a 'GET' request to '{base}/secret/jayani'
Then I should receive a response with the status 201

Given I obtain an access token from '{base}/auth/token' using the credentials:
| client_id | 123 |
| username | gerald |
When I send a 'GET' request to '{base}/secret/gerald'
Then I should receive a response with the status 201
29 changes: 29 additions & 0 deletions test/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const cookieParser = require('cookie-parser');
const { equal, deepEqual, AssertionError } = require('assert');

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
Expand Down Expand Up @@ -80,6 +81,34 @@ app.put('/pets/:id', (req, res, next) => {
}
});

const tokens = {
'jayani': 't1',
'hasini': 't2',
'gerald': 't3',
}

app.post('/auth/token', function (req, res) {
if (tokens[req.body.username]) {
return res.json({
accessToken: tokens[req.body.username],
})
} else {
res.status(401);
res.json({
msg: 'Access denied',
})
}
});

app.get('/secret/:username', function (req, res, next) {
try {
equal(`Bearer ${tokens[req.params.username]}`, req.get('Authorization'));
res.status(201);
res.send('OK');
} catch (err) {
next(err)
}
});
app.use((err, req, res, next) => {
console.warn(err.message);
res.status(err instanceof AssertionError ? 418 : (err.status || 500));
Expand Down
8 changes: 8 additions & 0 deletions world-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* [World](#module_World)
* [~World](#module_World..World)
* [.baseUrl](#module_World..World+baseUrl)
* [.req](#module_World..World+req)
* [.req](#module_World..World+req)
* [.currentAgent](#module_World..World+currentAgent)
Expand All @@ -24,6 +25,7 @@ State and stateful utilities can be shared between steps using an instance of "W
**Kind**: inner class of [<code>World</code>](#module_World)

* [~World](#module_World..World)
* [.baseUrl](#module_World..World+baseUrl)
* [.req](#module_World..World+req)
* [.req](#module_World..World+req)
* [.currentAgent](#module_World..World+currentAgent)
Expand All @@ -36,6 +38,12 @@ State and stateful utilities can be shared between steps using an instance of "W
* [.saveCurrentResponse()](#module_World..World+saveCurrentResponse)
* [.retrieveResponse(resource, method, status)](#module_World..World+retrieveResponse)

<a name="module_World..World+baseUrl"></a>

#### world.baseUrl
Getter for the `baseUrl` used for all requests

**Kind**: instance property of [<code>World</code>](#module_World..World)
<a name="module_World..World+req"></a>

#### world.req
Expand Down

0 comments on commit 2baf1eb

Please sign in to comment.