Skip to content

Commit a82eb9b

Browse files
pajgoAVVS
authored andcommitted
chore(tests): more tests and WebExecuter changes (#425)
1 parent 7ab3be2 commit a82eb9b

7 files changed

Lines changed: 476 additions & 68 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Facebook OAuth Tests and Service Logic
2+
First version of this file was targeted only on Tests update but after some research,
3+
decided to change some logic in `OAuth` handler too.
4+
5+
General changes were intended to add additional tests for Facebook OAuth
6+
and provide a better WebExecuter error handling when Facebook throttling occurs.
7+
8+
After some checks on OAuth endpoint logic, I found that error handling must be updated.
9+
Current error handling was passing a lot of extra data in response, that's shouldn't be shown to the client.
10+
11+
## Tests changes
12+
### OAuth Error tests
13+
Tests use stub as `http.auth.test` method with custom error and check that `service`
14+
produces the correct response.
15+
16+
These tests aim that OAuth Errors coming from `@hapi/bell` exist in service response:
17+
1. The Test checking that service responds with correct error in case of OAuth error.
18+
2. The Test checking that additional data do not exist in response.
19+
20+
Also added one integrity test checking that `WebExecuter` will throw a different error when
21+
`waitFor...` timeout error is thrown. This error appears in the situation when `Page` wait's for some content, but this content or response not received because of some unpredicted errors.
22+
23+
### WebExecuter Timeout Error
24+
Added Additional error processing logic to the `helpers/oauth/facebook/web-executer.js`, this will
25+
provide more informative output from tests. Now if `page.waitFor...` Timeout happens, the error message will
26+
contain last Service response or page contents.
27+
28+
## Service logic changes
29+
### @hapi/bell Boom error handling
30+
In `production` ENV the Service still renders full error data coming from `@hapi/bell`. Stubbed OAuth call with simulated errors
31+
returns such response:
32+
33+
```javascript
34+
const $ms_users_inj_post_messsge = { payload:
35+
{ data:
36+
{ data: { i_am_very_long_body: true, },
37+
isBoom: true,
38+
isServer: false,
39+
output:
40+
{ statusCode: 403,
41+
payload:
42+
{ statusCode: 403, error: 'Forbidden', message: 'X-Throttled' },
43+
headers: {} },
44+
name: 'Error',
45+
message: 'X-Throttled',
46+
stack:
47+
'Error: X-Throttled\n at Context.forbidden (/src/test/suites/oauth/facebook.js:97:34)\n at process._tickCallback (internal/process/next_tick.js:68:7)' },
48+
isBoom: true,
49+
isServer: true,
50+
output:
51+
{ statusCode: 500,
52+
payload:
53+
{ statusCode: 500,
54+
error: 'Internal Server Error',
55+
message: 'An internal server error occurred' },
56+
headers: {} },
57+
name: 'Error',
58+
message: 'BadError' },
59+
error: true,
60+
type: 'ms-users:attached',
61+
title: 'Failed to attach account',
62+
meta: {} }
63+
```
64+
65+
Even if `HTTP.IncomingMessage` deleted from error, too much data passed in response.
66+
Rendering of the Error is performed by `serialize-error` in Hook instead `Hapi` server and this shows all error contents.
67+
68+
This great for debug purposes but not for passing to the client.
69+
I decided to add a new error `OAuthError` with custom `toJSON` serializer, which removes all unnecessary data
70+
and returns Simplified Object. This error is only for the `auth/OAuth` scope.
71+
72+
After this change Errors render in this way:
73+
74+
```javascript
75+
const p = {
76+
payload: {
77+
message: 'BadError',
78+
name: 'OAuthError',
79+
inner_error: {
80+
message: 'BadError',
81+
name: 'Error',
82+
stack:
83+
'Error: BadError\n at Object.internal (/src/test/suites/oauth/facebook.js:106:28)\n at Object.invoke (/src/node_modules/sinon/lib/sinon/behavior.js:151:35)\n at module.exports.internals.Auth.functionStub (/src/node_modules/sinon/lib/sinon/stub.js:130:47)\n at Function.invoke (/src/node_modules/sinon/lib/sinon/spy.js:297:51)\n at module.exports.internals.Auth.functionStub (/src/node_modules/sinon/lib/sinon/spy.js:90:30)\n at Users.test (/src/src/auth/oauth/index.js:149:45)\n at Users.tryCatcher (/src/node_modules/bluebird/js/release/util.js:16:23)\n at Promise._settlePromiseFromHandler (/src/node_modules/bluebird/js/release/promise.js:517:31)\n at Promise._settlePromise (/src/node_modules/bluebird/js/release/promise.js:574:18)\n at Promise._settlePromiseCtx (/src/node_modules/bluebird/js/release/promise.js:611:10)\n at _drainQueueStep (/src/node_modules/bluebird/js/release/async.js:142:12)\n at _drainQueue (/src/node_modules/bluebird/js/release/async.js:131:9)\n at Async._drainQueues (/src/node_modules/bluebird/js/release/async.js:147:5)\n at Immediate.Async.drainQueues [as _onImmediate] (/src/node_modules/bluebird/js/release/async.js:17:14)\n at runCallback (timers.js:705:18)\n at tryOnImmediate (timers.js:676:5)\n at processImmediate (timers.js:658:5)',
84+
data:
85+
{ message: 'X-Throttled',
86+
name: 'Error',
87+
stack:
88+
'Error: X-Throttled\n at Context.forbidden (/src/test/suites/oauth/facebook.js:97:34)\n at process._tickCallback (internal/process/next_tick.js:68:7)',
89+
data: { i_am_very_long_body: true } } } },
90+
error: true,
91+
type: 'ms-users:attached',
92+
title: 'Failed to attach account',
93+
meta: {}
94+
}
95+
96+
```
97+
98+
## TODO
99+
- [x] WebExecuter Timeout handling
100+
- [x] Test checking that service return error
101+
- [x] `@hapi/boom` error serialization on `OAuth preResponse` hook
102+
- [x] Clean up code
103+
- [x] Cleanup docs
104+

src/auth/oauth/index.js

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const extractJWT = require('./utils/extractJWT');
99
const { getInternalData } = require('../../utils/userData');
1010

1111
const { verifyToken, loginAttempt } = require('../../utils/amqp');
12-
const { Redirect } = require('./utils/errors');
12+
const { Redirect, OAuthError } = require('./utils/errors');
1313
const { USERS_ID_FIELD, ErrorTotpRequired } = require('../../constants');
1414

1515
// helpers
@@ -18,28 +18,6 @@ const is404 = ({ statusCode }) => statusCode === 404;
1818
const isError = ({ statusCode }) => statusCode >= 400;
1919
const isHTMLRedirect = ({ statusCode, source }) => statusCode === 200 && source;
2020

21-
/**
22-
* Cleanup boom error and return matching error;
23-
* @param {Boom} error
24-
* @returns {error}
25-
*/
26-
function checkBoomError(error) {
27-
const { data: errData } = error;
28-
const { message } = error;
29-
30-
// can contain another Boom error
31-
if (errData !== null && typeof errData === 'object') {
32-
// delete reference to http.IncommingMessage
33-
delete errData.data.res;
34-
}
35-
36-
if (message.startsWith('App rejected')) {
37-
return Errors.AuthenticationRequiredError(`OAuth ${error.message}`, error);
38-
}
39-
40-
return error;
41-
}
42-
4321
/**
4422
* Authentication handler
4523
*
@@ -171,9 +149,15 @@ module.exports = async function authHandler({ action, transportRequest }) {
171149
const { credentials } = await http.auth.test(strategy, transportRequest);
172150
response = [null, credentials];
173151
} catch (err) {
174-
// No need to go further if Oauth Error happened
175152
if (Boom.isBoom(err)) {
176-
throw checkBoomError(err);
153+
const { message } = err;
154+
155+
// Error thrown when user declines OAuth
156+
if (message.startsWith('App rejected')) {
157+
throw Errors.AuthenticationRequiredError(`OAuth ${err.message}`, err);
158+
}
159+
160+
throw OAuthError(err.message, err);
177161
}
178162
// continue if redirect
179163
response = [err];

src/auth/oauth/utils/errors.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,69 @@
11
const Errors = require('common-errors');
2+
const Boom = require('@hapi/boom');
23

34
exports.Redirect = Errors.helpers.generateClass('Redirect', {
45
args: ['redirectUri'],
56
});
7+
8+
const OAuthError = exports.OAuthError = Errors.helpers.generateClass('OAuthError', {
9+
args: ['message', 'inner_error'],
10+
});
11+
12+
/**
13+
* Removes `@hapi/boom` attributes
14+
* @param error
15+
* @returns {*|{stack: *, data: *, name: *, message: *}}
16+
*/
17+
function stripBoomAttrs(error) {
18+
const { message, name, stack, data: errorData } = error;
19+
let data;
20+
21+
/* assuming that 'data' not passed or it's not and object when Boom.error created. */
22+
if (errorData !== null && typeof errorData === 'object') {
23+
/* Remove http.IncomingMessage - @hapi/wreck adds it when error is coming from response. */
24+
if (errorData.isResponseError) {
25+
const { res, ...otherData } = errorData;
26+
data = otherData;
27+
} else {
28+
({ data } = error);
29+
}
30+
} else {
31+
data = errorData;
32+
}
33+
34+
/* Recursive check */
35+
if (Boom.isBoom(error.data)) {
36+
data = stripBoomAttrs(error.data);
37+
}
38+
39+
return {
40+
message,
41+
name,
42+
stack,
43+
data,
44+
};
45+
}
46+
47+
/**
48+
* OAuthError.toJSON custom serialization
49+
* Returns simplified object
50+
* @returns {*}
51+
*/
52+
OAuthError.prototype.toJSON = function toJSON() {
53+
const { inner_error: innerError, message, name, stack } = this;
54+
let innerErrorJsonObj;
55+
56+
/* Boom error contains additional data */
57+
if (Boom.isBoom(innerError)) {
58+
innerErrorJsonObj = stripBoomAttrs(innerError);
59+
} else {
60+
innerErrorJsonObj = innerError;
61+
}
62+
63+
return {
64+
message,
65+
name,
66+
stack,
67+
inner_error: innerErrorJsonObj,
68+
};
69+
};

test/helpers/oauth/facebook/graph-api.js

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const request = require('request-promise');
2-
2+
const assert = require('assert');
33
/**
44
* Class wraps Facebook Graph API requests
55
*/
@@ -12,6 +12,15 @@ class GraphAPI {
1212
json: true,
1313
});
1414

15+
/**
16+
* Just to be sure that correct user passed
17+
* @param user
18+
*/
19+
static checkUser(user) {
20+
assert(user, 'No user provided');
21+
assert(user.id, 'User must have `id`');
22+
}
23+
1524
/**
1625
* Creates test user with passed `props`
1726
* @param props
@@ -25,43 +34,50 @@ class GraphAPI {
2534
installed: false,
2635
...props,
2736
},
28-
}).promise();
37+
});
2938
}
3039

3140
/**
32-
* Deletes test user by id
33-
* @param userId
34-
* @returns {*}
41+
* Deletes test user
42+
* @param facebook user
43+
* @returns {Promise<*>}
3544
*/
36-
static deleteTestUser(userId) {
45+
static deleteTestUser(user) {
46+
this.checkUser(user);
47+
3748
return this.graphApi({
38-
uri: `${userId}`,
49+
uri: `${user.id}`,
3950
method: 'DELETE',
40-
}).promise();
51+
});
4152
}
4253

4354
/**
4455
* Removes all Application permissions.
4556
* This only the way to De Authorize Application from user.
46-
* @param userId
47-
* @returns {Promise<void>}
57+
* @param facebook user
58+
* @returns {Promise<*>}
4859
*/
49-
static async deAuthApplication(userId) {
60+
static deAuthApplication(user) {
61+
this.checkUser(user);
62+
5063
return this.graphApi({
51-
uri: `/${userId}/permissions`,
64+
uri: `/${user.id}/permissions`,
5265
method: 'DELETE',
5366
});
5467
}
5568

5669
/**
5770
* Delete any Application permission from user.
58-
* @param userId
71+
* @param facebook user
5972
* @param permission
60-
* @returns {Promise<void>}
73+
* @returns {Promise<*>}
6174
*/
62-
static async deletePermission(userId, permission) {
75+
static deletePermission(user, permission) {
76+
this.checkUser(user);
77+
assert(permission, 'No `permission` provided');
78+
6379
return this.graphApi({
64-
uri: `/${userId}/permissions/${permission}`,
80+
uri: `/${user.id}/permissions/${permission}`,
6581
method: 'DELETE',
6682
});
6783
}

test/helpers/oauth/facebook/index.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)