Skip to content

Commit 2baf1eb

Browse files
author
Philip Mander
committed
feat: Add support for login with OAuth 2
1 parent 4968916 commit 2baf1eb

File tree

5 files changed

+133
-29
lines changed

5 files changed

+133
-29
lines changed

src/steps.js

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const chai = require('chai');
2+
const request = require('superagent');
23
const Ajv = require('ajv');
34
const cookie = require('cookie');
45
const JSONPath = require('jsonpath-plus');
@@ -26,7 +27,54 @@ function registerSteps({ Given, When, Then }) {
2627
* @function anonymous
2728
*/
2829
Given('I am anonymous', function () {
29-
// nothing to do here
30+
// remove any active oauth2 token
31+
this.oauth2.token = null;
32+
});
33+
34+
/**
35+
* ### Given I obtain an access token from {string} using the credentials:
36+
* Supports logging into using OAuth2 credentials, typically with the passwrod scheme
37+
* Sessions (access tokens) will be stored and supported for subsequent requests
38+
*
39+
* @example
40+
* Given I obtain an access token from {string} using the credentials:
41+
* | client_id | harver |
42+
* | client_secret | harver123 |
43+
* | username | gerald |
44+
* | password | foobar |
45+
* | grant_type | password |
46+
*
47+
*/
48+
Given('I obtain an access token from {string} using the credentials:', async function (url, credentialsTable) {
49+
const credentials = credentialsTable.rowsHash();
50+
const session = this.oauth2.sessions.find(session =>
51+
credentials.client_id === session.credentials.client_id &&
52+
credentials.username === session.credentials.username
53+
);
54+
55+
// TODO: check if token has expired
56+
let token;
57+
if (!session) {
58+
const res = await request
59+
.post(this.baseUrl + this.replaceVars(url))
60+
.type('form')
61+
.send(credentials);
62+
63+
if (res.body.accessToken) {
64+
token = res.body.accessToken;
65+
this.oauth2.sessions.push({
66+
credentials,
67+
token,
68+
});
69+
} else {
70+
throw new Error(`Could not authenticate with OAuth2:\n\t${res.body}`);
71+
}
72+
} else {
73+
token = session.token;
74+
}
75+
76+
// set a current token
77+
this.oauth2.token = token || null;
3078
});
3179

3280
/**
@@ -58,8 +106,12 @@ function registerSteps({ Given, When, Then }) {
58106
*
59107
* @function makeRequest
60108
*/
61-
When('I send a {string} request to {string}', function (method, path) {
62-
this.req = this.currentAgent[method.toLowerCase()](this.baseUrl + path);
109+
When('I send a {string} request to {string}', function (method, url) {
110+
this.req = this.currentAgent[method.toLowerCase()](this.baseUrl + url);
111+
112+
if (this.oauth2.token) {
113+
this.req.set('Authorization', `Bearer ${this.oauth2.token}`);
114+
}
63115

64116
if (methodsWithBodies.includes(method)) {
65117
this.req.set('Content-Type', 'application/json');

src/world.js

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ class World {
2929
const envFile = process.env.ENV_FILE || null;
3030
this.envVars = envFile ? JSON.parse(readFileSync(resolve(process.cwd(), envFile))).values : [];
3131
this.responseVars = [];
32+
33+
// keep a set of sessions
34+
this.oauth2 = {
35+
token: null, // currently active access token
36+
sessions: [], // a store of used credentials and their access tokens
37+
}
3238
}
3339

3440
/**
@@ -98,35 +104,34 @@ class World {
98104
return {};
99105
}
100106
}
107+
108+
replaceVars(val) {
109+
const vars = [].concat(this.responseVars).concat(this.envVars);
110+
111+
if (!val || !vars.length) {
112+
return val;
113+
}
114+
115+
// cheeky way to easily replace on whole objects:
116+
const placeHolders = vars.map(pair => pair.key).join('|');
117+
const regex = new RegExp(`\{(${placeHolders})\}`, 'g');
118+
return JSON.parse(JSON.stringify(val).replace(regex, (match, p1) => {
119+
const matchPair = vars.find(pair => pair.key === p1);
120+
return matchPair ? matchPair.value : match;
121+
}));
122+
}
123+
101124
/**
102125
* Returns Super Agent middleware that replaces placeholders with
103126
* variables
104127
*/
105128
replaceVariablesInitiator() {
106-
function simpleReplace(val, regex, vars) {
107-
if (!val) {
108-
return val;
109-
}
110-
111-
// cheeky way to easily replace on whole objects:
112-
return JSON.parse(JSON.stringify(val).replace(regex, (match, p1) => {
113-
const matchPair = vars.find(pair => pair.key === p1);
114-
return matchPair ? matchPair.value : match;
115-
}));
116-
}
117-
118129
return req => {
119-
const vars = [].concat(this.responseVars).concat(this.envVars);
120-
if (!vars.length) {
121-
return req;
122-
}
123-
const placeHolders = vars.map(pair => pair.key).join('|');
124-
const placeHolderRegex = new RegExp(`\{(${placeHolders})\}`, 'g');
125-
req.url = simpleReplace(req.url, placeHolderRegex, vars);
126-
req.qs = simpleReplace(req.qs, placeHolderRegex, vars);
127-
req.headers = simpleReplace(req.headers, placeHolderRegex, vars);
128-
req.cookies = simpleReplace(req.cookies, placeHolderRegex, vars);
129-
req._data = simpleReplace(req._data, placeHolderRegex, vars);
130+
req.url = this.replaceVars(req.url);
131+
req.qs = this.replaceVars(req.qs);
132+
req.headers = this.replaceVars(req.headers);
133+
req.cookies = this.replaceVars(req.cookies);
134+
req._data = this.replaceVars(req._data);
130135
return req;
131136
};
132137
}
@@ -155,9 +160,6 @@ class World {
155160
* @param {} res A Superagent response object
156161
*/
157162
getResponseBody(res) {
158-
if (res.header['content-type'] && res.header['content-type'].startsWith('text/html')) {
159-
return JSON.parse(res.text);
160-
}
161163
return res.body;
162164
}
163165

test/features/test-steps.feature

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,16 @@ Feature: API Testing Steps
113113
Then I should receive a response with the status 200
114114
And the response body json path at "$.name" should equal "Felix"
115115

116+
@oauth
117+
Scenario: Testing OAuth support
118+
Given I obtain an access token from '{base}/auth/token' using the credentials:
119+
| client_id | 123 |
120+
| username | jayani |
121+
When I send a 'GET' request to '{base}/secret/jayani'
122+
Then I should receive a response with the status 201
123+
124+
Given I obtain an access token from '{base}/auth/token' using the credentials:
125+
| client_id | 123 |
126+
| username | gerald |
127+
When I send a 'GET' request to '{base}/secret/gerald'
128+
Then I should receive a response with the status 201

test/server.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const cookieParser = require('cookie-parser');
44
const { equal, deepEqual, AssertionError } = require('assert');
55

66
const app = express();
7+
78
app.use(bodyParser.json());
89
app.use(bodyParser.urlencoded({ extended: false }));
910
app.use(cookieParser());
@@ -80,6 +81,34 @@ app.put('/pets/:id', (req, res, next) => {
8081
}
8182
});
8283

84+
const tokens = {
85+
'jayani': 't1',
86+
'hasini': 't2',
87+
'gerald': 't3',
88+
}
89+
90+
app.post('/auth/token', function (req, res) {
91+
if (tokens[req.body.username]) {
92+
return res.json({
93+
accessToken: tokens[req.body.username],
94+
})
95+
} else {
96+
res.status(401);
97+
res.json({
98+
msg: 'Access denied',
99+
})
100+
}
101+
});
102+
103+
app.get('/secret/:username', function (req, res, next) {
104+
try {
105+
equal(`Bearer ${tokens[req.params.username]}`, req.get('Authorization'));
106+
res.status(201);
107+
res.send('OK');
108+
} catch (err) {
109+
next(err)
110+
}
111+
});
83112
app.use((err, req, res, next) => {
84113
console.warn(err.message);
85114
res.status(err instanceof AssertionError ? 418 : (err.status || 500));

world-api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

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

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

41+
<a name="module_World..World+baseUrl"></a>
42+
43+
#### world.baseUrl
44+
Getter for the `baseUrl` used for all requests
45+
46+
**Kind**: instance property of [<code>World</code>](#module_World..World)
3947
<a name="module_World..World+req"></a>
4048

4149
#### world.req

0 commit comments

Comments
 (0)