Skip to content

Commit 0825a6d

Browse files
authored
fix: upgrade dependencies, make fb tests more reliable (#424)
* upgrades dependencies * handles facebook test user registrations in an updated manner, enclosed RFC with details
1 parent 93b7371 commit 0825a6d

58 files changed

Lines changed: 1784 additions & 1210 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/cleanup-test-users.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
require('dotenv').config();
33

44
const rp = require('request-promise').defaults({
5-
url: `https://graph.facebook.com/v3.3/${process.env.FACEBOOK_CLIENT_ID}/accounts/test-users`,
5+
url: `https://graph.facebook.com/v4.0/${process.env.FACEBOOK_CLIENT_ID}/accounts/test-users`,
66
json: true,
77
qs: {
88
access_token: process.env.FACEBOOK_APP_TOKEN,
@@ -12,7 +12,7 @@ const rp = require('request-promise').defaults({
1212
async function fetchUsers(url = '') {
1313
const { data, paging } = await rp.get(url);
1414
for (const user of data) {
15-
await rp.delete(`https://graph.facebook.com/v3.3/${user.id}`);
15+
await rp.delete(`https://graph.facebook.com/v4.0/${user.id}`);
1616
process.stdout.write('.');
1717
}
1818

package.json

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,29 @@
2828
},
2929
"homepage": "https://github.com/makeomatic/ms-users#readme",
3030
"dependencies": {
31-
"@hapi/bell": "^10.0.0",
32-
"@hapi/hapi": "^18.0.0",
33-
"@hapi/vision": "^5.4.4",
34-
"@microfleet/core": "^14.0.1",
35-
"@microfleet/transport-amqp": "^14.0.2",
31+
"@hapi/bell": "^10.1.1",
32+
"@hapi/hapi": "^18.3.2",
33+
"@hapi/vision": "^5.5.3",
34+
"@microfleet/core": "^14.0.3",
35+
"@microfleet/transport-amqp": "^15.0.0",
3636
"@microfleet/validation": "^8.1.2",
3737
"bluebird": "^3.5.5",
3838
"bytes": "^3.0.0",
3939
"common-errors": "^1.0.5",
4040
"csv-write-stream": "^2.0.0",
41-
"disposable-email-domains": "^1.0.47",
41+
"disposable-email-domains": "^1.0.48",
4242
"dlock": "^9.0.1",
4343
"flake-idgen": "^1.1.0",
4444
"get-stdin": "^7.0.0",
4545
"get-value": "^3.0.1",
46-
"handlebars": "^4.1.2",
46+
"handlebars": "^4.2.0",
4747
"ioredis": "^4.14.0",
4848
"is": "^3.3.0",
4949
"jsonwebtoken": "^8.5.1",
5050
"jwa": "^1.4.1",
5151
"lodash": "^4.17.15",
5252
"moment": "^2.23.0",
53-
"ms-conf": "^5.0.0",
53+
"ms-conf": "^5.0.2",
5454
"ms-flakeless": "^4.2.0",
5555
"ms-mailer-client": "^8.0.1",
5656
"ms-mailer-templates": "^1.11.1",
@@ -63,35 +63,35 @@
6363
"request-promise": "^4.2.4",
6464
"scrypt": "^6.0.1",
6565
"serialize-error": "^4.1.0",
66-
"serialize-javascript": "^1.7.0",
66+
"serialize-javascript": "^2.1.0",
6767
"stdout-stream": "^1.4.1",
6868
"tough-cookie": "^3.0.0",
69-
"uuid": "^3.3.2",
70-
"yargs": "^13.3.0"
69+
"uuid": "^3.3.3",
70+
"yargs": "^14.0.0"
7171
},
7272
"devDependencies": {
73-
"@babel/cli": "^7.5.5",
74-
"@babel/core": "^7.5.5",
73+
"@babel/cli": "^7.6.0",
74+
"@babel/core": "^7.6.0",
7575
"@babel/plugin-proposal-class-properties": "^7.5.5",
7676
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
7777
"@babel/plugin-transform-strict-mode": "^7.2.0",
78-
"@babel/register": "^7.5.5",
79-
"@makeomatic/deploy": "^8.4.5",
78+
"@babel/register": "^7.6.0",
79+
"@makeomatic/deploy": "^8.4.7",
8080
"@semantic-release/changelog": "^3.0.4",
81-
"@semantic-release/exec": "^3.3.5",
81+
"@semantic-release/exec": "^3.3.6",
8282
"@semantic-release/git": "^7.0.16",
8383
"apidoc": "^0.17.7",
8484
"apidoc-plugin-schema": "^0.1.8",
85-
"babel-eslint": "^10.0.2",
85+
"babel-eslint": "^10.0.3",
8686
"babel-plugin-istanbul": "^5.2.0",
8787
"chai": "^4.2.0",
8888
"cheerio": "^1.0.0-rc.3",
8989
"codecov": "^3.5.0",
90-
"cross-env": "^5.2.0",
91-
"eslint": "^6.1.0",
92-
"eslint-config-makeomatic": "^3.0.1",
90+
"cross-env": "^5.2.1",
91+
"eslint": "^6.3.0",
92+
"eslint-config-makeomatic": "^3.1.0",
9393
"eslint-plugin-import": "^2.18.2",
94-
"eslint-plugin-mocha": "^6.0.0",
94+
"eslint-plugin-mocha": "^6.1.0",
9595
"eslint-plugin-promise": "^4.2.1",
9696
"faker": "^4.1.0",
9797
"glob": "^7.1.4",
@@ -100,8 +100,8 @@
100100
"mocha": "^6.2.0",
101101
"nyc": "^14.1.1",
102102
"puppeteer": "1.19.0",
103-
"rimraf": "^2.6.3",
104-
"sinon": "^7.4.1"
103+
"rimraf": "^3.0.0",
104+
"sinon": "^7.4.2"
105105
},
106106
"engines": {
107107
"node": ">= 10.10.0",
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Facebook OAuth tests change
2+
3+
## Overview and Motivation
4+
After some updates Facebook Graph API throttling mechanisms, OAuth tests started failing randomly with 403 code or
5+
`invalid credentials` ms-users error.
6+
Graph API limits requests by a sliding window algorithm, which allows some(not all) requests even after block.
7+
The changes listed here will provide better stability in OAuth error handling and overall test suites stability.
8+
9+
## Facebook Throttling
10+
After some changes in Facebook GraphAPI `throttling` behavior became much stronger.
11+
On Every GraphAPI request, facebook response contains special header `X-App-Usage`:
12+
```javascript
13+
const XAppUsage = {
14+
'call_count': 28, //Percentage of calls made
15+
'total_time': 15, //Percentage of total time
16+
'total_cputime' : 24 //Percentage of total CPU time
17+
}
18+
```
19+
If the value of any field becomes greater than 100 causes `throttling`.
20+
This header helps to determine how soon `throttling` will happen.
21+
22+
### OAuth API v3.3 v4.0 Update
23+
Updated OAuth API. There is no braking changes and there's no additional work to upgrade version.
24+
API 3.3 is not available for new Applications and all requests made to this version are redirected to v4.0.
25+
26+
### Tests `Missing Credentials` Error
27+
Mishandling `@hapi/boom` error causes the OAuth strategy to continue its execution but without mandatory `credentials` and this caused the random tests to fail.
28+
29+
##### Solution
30+
Implement additional error conversion from `@boom/error` to general `HTTPError`.
31+
This return OAuth's error processing to normal behavior.
32+
33+
### Tests `403 - Application throttled` Error
34+
Before every test, some direct GraphApi calls made. In some `throttling` conditions requests rejected with `403` Error,
35+
this happens because of Facebook call Throttling.
36+
37+
### @hapi/bell error handling
38+
When the user declines an application to access the 500 Error returned from `@hapi/bell`.
39+
When application throttled or request `200 < statusCode > 299` the same 500 Error returned with Response as `data` field.
40+
41+
On catch block after `http.auth.test` call added additional error processing on @hapi/boom error:
42+
* Cleans references to `http.IncomingMessage` from error, otherwise further error serialization shows all contents of `http.IncommingMessage` class.
43+
IMHO: That's weird to show Buffers and other internal stuff.
44+
* If error message contains information that user Rejected App access, returns Error.AuthenticationRequiredError` with `OAuth App rejected: Permissions error` . to save current behavior and return 401 StatusCode.
45+
* Other errors are thrown as @hapi/Bomm error.
46+
47+
Removed `isError` check and function from `auth/oauth/index.js` because it's impossible to receive a response with statusCode other than 200 from `@hapi/bell`.
48+
According to current source code, only `redirects` and `h.unauthenticated(internal_errors)`(used to report OAuth request errors) thrown from `hapi.auth.test`.
49+
50+
#### Overall Graph API over-usage
51+
Over-usage of Graph API request time produced by `createTestUser` API calls, this API call uses a lot of API's execution time and increments Facebook's counters fast.
52+
Before every test, we execute `createTestUser` 3*testCount times, so each suite execution performs about 39 long-running calls.
53+
Facebook Graph API starts throttling requests after 4-6 suite runs.
54+
55+
##### Solution
56+
Exclude long-running `createTestUser` operation from tests and reuse test users.
57+
Attempt to reduce API call count.
58+
59+
## Test logic changes
60+
61+
### Test Users Create
62+
Test users will be created once before all tests and reused.
63+
After each test run, users Application permissions revoked depending on user context:
64+
* For `testUserInstalledPartial` test revokes only `email` permission.
65+
* For `testUser` test revokes all permissions.
66+
67+
When the test suite finishes users deleted, this saves us from hitting the Facebook TestUser count limit.
68+
69+
### Test Users
70+
There are 3 types of Facebook users used In the current test suites:
71+
```javascript
72+
const createTestUser = (localCache = cache) => Promise.props({
73+
testUser: createTestUserAPI(),
74+
testUserInstalled: createTestUserAPI({ installed: true }),
75+
testUserInstalledPartial: createTestUserAPI({ permissions: 'public_profile' }),
76+
})
77+
```
78+
79+
#### testUserInstalled
80+
**`testUserInstalled`** not used in tests. After review of all commit history for `suites/oauth/facebook.js`, found that
81+
this user was defined in [this commit](https://github.com/makeomatic/ms-users/blob/733aba371b62d90935c42087ca6d3912152cb63b/test/suites/oauth/facebook.js)
82+
and never used.
83+
84+
According to the users' props, it defined for `sign-in` tests without creating the new user,
85+
but these tests use `testUser` in all suite and behave like other sign-in/sign-up tests.
86+
87+
Startup logic and checks for these tests is almost the same and works like:
88+
1. Call `https://mservice/users/oauth/facebook`
89+
2. The Service redirects to the Facebook Login form.
90+
3. Puppeter fills in Login and password.
91+
4. Puppeter Confirms/Rejects Application request.
92+
93+
`testUser` totally responds to our needs and `testUserInstalled` looks unused.
94+
95+
**!!!** _Assuming that we can remove the `testUserInstalled` user._
96+
97+
#### testUserInstalledPartial
98+
**`testUserPartial`** used in tests that checking partial permissions in registration/login and used as the previous scope,
99+
but users prop `installed` false.
100+
101+
In current partial permission tests, Facebook asks 2 permissions(public_profile, email), this indicates that
102+
partial `permissions` ignored on the user creation process. So we can safely deauthorize application using 'DELETE /uid/permissions' request.
103+
104+
#### Changes:
105+
To make tests more readable and a bit easier to read some methods moved to separate files:
106+
* WebExecuter class - contains all actions performed with the Facebook page using `Puppeter`.
107+
* GraphApi Class - contains all calls to the Facebook Graph API.
108+
109+
Repeating `asserts` moved outside of tests into functions.
110+
Tests regrouped by User type. Some duplicate actions moved into `hooks`.
111+
112+
### Additional tests/functionality?
113+
Current tests cover all possible situations, even for the user with Application `installed === true` property.
114+
All test cases for this type of user look the same as previous tests and code duplicates because the same API used.
115+
One difference in them, when the user already has some permissions provided to the application,
116+
'Facebook Permission Access' screen shows a smaller permission list
117+
and only `signInAndNavigate` method changed in 1 row(index of the checkbox to click).
118+
119+
120+
### GDPR NOTE
121+
In our case, GDPR not applied inside the scope of the 'Facebook Login' feature.
122+
Facebook is a Data Provider for us and using own privacy policy that allows us to use
123+
data provided from Facebook.
124+
From our side, we must add `Cookie and Data collection notification` - already exists.
125+
126+
Some changes should be made if We use Android or IOS Facebook SDK in event tracking functions.
127+
For Detailed description visit [this page](https://www.facebook.com/business/m/one-sheeters/gdpr-developer-faqs)
128+
129+
Additional info can be found [here](https://developers.facebook.com/docs/app-events/best-practices/gdpr-compliance)

src/accounts/init-admin.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ module.exports = function initAccounts() {
4242
.dispatch('referral', { params: { id: userData.referral, referral: account.referral } })
4343
.return({ params: userData });
4444
})
45-
.map(userData => Promise.bind(this, ['register', userData]).spread(this.dispatch).reflect())
45+
.map((userData) => Promise.bind(this, ['register', userData]).spread(this.dispatch).reflect())
4646
.then((users) => {
4747
const totalAccounts = users.length;
4848
const errors = [];

src/actions/activate.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function throwBasedOnStatus(status) {
4040
function isAccountActive(username) {
4141
return getInternalData
4242
.call(this, username)
43-
.then(userData => userData[USERS_ACTIVE_FLAG])
43+
.then((userData) => userData[USERS_ACTIVE_FLAG])
4444
.then(throwBasedOnStatus);
4545
}
4646

@@ -87,7 +87,7 @@ function verifyRequest() {
8787
if (username && token) {
8888
return getInternalData
8989
.call(this.service, username)
90-
.then(userData => [
90+
.then((userData) => [
9191
{ action, token, id: userData[USERS_USERNAME_FIELD] },
9292
{ erase },
9393
])
@@ -195,16 +195,16 @@ function activateAction({ params }) {
195195
.bind(context)
196196
.then(verifyRequest)
197197
.bind(this)
198-
.then(resolvedUsername => getInternalData.call(this, resolvedUsername))
199-
.then(internalData => Promise.join(
198+
.then((resolvedUsername) => getInternalData.call(this, resolvedUsername))
199+
.then((internalData) => Promise.join(
200200
internalData,
201201
getMetadata.call(this, internalData[USERS_ID_FIELD], audience).get(audience)
202202
))
203203
.spread(activateAccount)
204204
.bind(context)
205205
.tap(hook)
206206
.bind(this)
207-
.then(userId => [userId, audience])
207+
.then((userId) => [userId, audience])
208208
.spread(jwt.login);
209209
}
210210

src/actions/ban.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const {
1010
} = require('../constants.js');
1111

1212
// helper
13-
const stringify = data => JSON.stringify(data);
13+
const stringify = (data) => JSON.stringify(data);
1414

1515
function lockUser({
1616
id, reason, whom, remoteip,

src/actions/disposable-password.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ module.exports = function disposablePassword(request) {
2727
.tap(isActive)
2828
.tap(isBanned)
2929
.tap(hasNotPassword)
30-
.then(data => ([challengeType, {
30+
.then((data) => ([challengeType, {
3131
id: data[USERS_USERNAME_FIELD],
3232
action: USERS_ACTION_DISPOSABLE_PASSWORD,
3333
...tokenOptions,

src/actions/getInternalData.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ module.exports = function internalData({ params }) {
2323
return Promise
2424
.bind(this, params.username)
2525
.then(getInternalData)
26-
.then(data => (
26+
.then((data) => (
2727
fields ? pick(data, fields) : data
2828
));
2929
};

src/actions/getMetadata.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ async function retrieveMetadata(username) {
5050
* @param {Array} responses
5151
* @return {Array|Object}
5252
*/
53-
const extractResponse = responses => responses[0];
53+
const extractResponse = (responses) => responses[0];
5454

5555
/**
5656
* @api {amqp} <prefix>.getMetadata Retrieve Public Data

src/actions/invite-list.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ module.exports = function iterateOverInvites(request) {
4242
}
4343

4444
return Promise.join(
45-
Promise.all(ids.map(id => tokenManager.info({ id, action: USERS_ACTION_INVITE }))),
45+
Promise.all(ids.map((id) => tokenManager.info({ id, action: USERS_ACTION_INVITE }))),
4646
length
4747
);
4848
})

0 commit comments

Comments
 (0)