Skip to content

Commit 60357cb

Browse files
jwang1919outoftime
authored andcommitted
Completed redux-logic transition for linkGithubIdentity code (#1672)
* CHECKPOINT jest * Fix unnecessary addition of an eslint-disable directive * Initial transition to react-logic. Moved over unlickGithubIdentity logic. Still need to get react-saga and react-logic to play nicely. * Got redux-saga and redux-logic working at the same time. * Attempt to get jest working on IntelliJ * Make jest config more consistent; transform lodash-es * Mocks for bugsnag and firebase libraries These libraries are set up in code that runs at module load time, which is probably not a great practice, but for now we just mock out the libraries themselves so our modules will load without error. * Revert unnecessary change to karma config * Add some jest rules to eslint config * Write test for unlinkGithubIdentityTest.js * Small refactor * Fix yarn.lock with deduplicate * Fix yarn.lock again? * Refactored test, installed jest-extended (but didn't use it) * Fix package.json after rebase * Fix yarn.lock again * Fix incorrect merge with .eslintrc * Add jest override to .eslintrc * Lint fixing * Start redux-logic transition for linkGithubIdentity code * Completed redux-logic transition for linkGithubIdentity code * Fix easy nit-pits. Will work on Rosie and factory approach later. * Fix eslint warnings triggered by Travis * Add support for factories Cherry-picked from CodeMirror branch * Attempt to use Rosie into jest testing * Label all factory objects with a suffix of "Factory". Added mock user object. * Fix eslint failing in Travis * 2nd attempt at fixing eslint failing in Travis * Added more realistic mock data for githubProfileFactory. Revert changes to .eslintrc * Upgrade eslint-plugin-import, fix import order As expected, `eslint-plugin-import` released a fix that corrects the behavior with internal modules that use module lookup aliases. So now the modules aliased under `@factory` are correctly recognized as internal. * Created new client factory github.js and moved relevant code there. Adjusted test description. * outoftime requested changes
1 parent 118039a commit 60357cb

File tree

11 files changed

+278
-107
lines changed

11 files changed

+278
-107
lines changed

.eslintrc

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"jest": true
4242
},
4343
"files": [
44-
"**/__tests__/*.js"
44+
"**/{__factories__,__mocks__,__tests__}/*.js{,x}"
4545
],
4646
"rules": {
4747
"jest/no-alias-methods": "warn",
@@ -64,7 +64,17 @@
6464
"react",
6565
"promise",
6666
"private-props"
67-
]
67+
],
68+
"settings": {
69+
"import/resolver": {
70+
"@popcodeorg/eslint-import-resolver-jest": {
71+
"jestConfigFile": "./jest.config.js"
72+
},
73+
"node": {
74+
"extensions": [".js", ".jsx"]
75+
}
76+
}
77+
}
6878
},
6979
{
7080
"files": "src/**/*",

__factories__/clients/firebase.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {Factory} from 'rosie';
2+
3+
class FirebaseError extends Error {
4+
constructor(args) {
5+
super(args.name);
6+
this.code = args.name;
7+
this.credential = args.credential;
8+
Error.captureStackTrace(this, FirebaseError);
9+
}
10+
}
11+
12+
export const credentialFactory = new Factory().attrs({
13+
providerId: 'github.com',
14+
accessToken: 'abc123',
15+
});
16+
17+
export const firebaseErrorFactory = Factory.define(
18+
'firebaseError',
19+
FirebaseError,
20+
).attrs({
21+
name: 'some other error',
22+
});
23+
24+
export const credentialInUseErrorFactory = new Factory().extend(
25+
firebaseErrorFactory,
26+
).attrs({
27+
name: 'auth/credential-already-in-use',
28+
credential: () => credentialFactory.build(),
29+
});
30+
31+
export const userProviderDataFactory = new Factory().attrs({
32+
displayName: 'popcoder',
33+
email: null,
34+
phoneNumber: null,
35+
photoUrl: null,
36+
providerId: 'github.com',
37+
uid: '1234567',
38+
});
39+
40+
export const userFactory = new Factory().extend(
41+
userProviderDataFactory,
42+
).attrs({
43+
emailVerified: false,
44+
isAnonymous: false,
45+
metadata: {
46+
creationTime: Date.now(),
47+
lastSignInTime: Date.now(),
48+
},
49+
providerData: () => [userProviderDataFactory.build()],
50+
refreshToken: 'token123',
51+
});

__factories__/clients/github.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {Factory} from 'rosie';
2+
3+
export const githubProfileFactory = new Factory().attrs({
4+
login: 'popcoder',
5+
id: 123456,
6+
node_id: 'ABC123',
7+
avatar_url: null,
8+
gravatar_id: null,
9+
url: 'https://api.github.com/users/popcoder',
10+
html_url: 'https://github.com/popcoder',
11+
followers_url: 'https://api.github.com/users/popcoder/followers',
12+
following_url: 'https://api.github.com/users/popcoder/following{/other_user}',
13+
gists_url: 'https://api.github.com/users/popcoder/gists{/gist_id}',
14+
starred_url: 'https://api.github.com/users/popcoder/starred{/owner}{/repo}',
15+
subscriptions_url: 'https://api.github.com/users/popcoder/subscriptions',
16+
organizations_url: 'https://api.github.com/users/popcoder/orgs',
17+
repos_url: 'https://api.github.com/users/popcoder/repos',
18+
events_url: 'https://api.github.com/users/popcoder/events{/privacy}',
19+
received_events_url: 'https://api.github.com/users/popcoder/received_events',
20+
type: 'User',
21+
site_admin: false,
22+
name: 'Popcoder',
23+
company: null,
24+
blog: '',
25+
location: null,
26+
email: null,
27+
hireable: null,
28+
bio: null,
29+
public_repos: 0,
30+
public_gists: 0,
31+
followers: 0,
32+
following: 0,
33+
created_at: '2014-09-12T15:02:43Z',
34+
updated_at: '2019-05-02T13:50:32Z',
35+
private_gists: 0,
36+
total_private_repos: 0,
37+
owned_private_repos: 0,
38+
disk_usage: 0,
39+
collaborators: 0,
40+
two_factor_authentication: false,
41+
plan: {
42+
name: 'free',
43+
space: 1234567,
44+
collaborators: 0,
45+
private_repos: 10000,
46+
},
47+
});

jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
// https://jestjs.io/docs/en/configuration.html
66

77
module.exports = {
8+
clearMocks: true,
9+
moduleNameMapper: {
10+
'@factories/(.*)$': '<rootDir>/__factories__/$1',
11+
},
812
testPathIgnorePatterns: [
913
'/node_modules/',
1014
'/bower_components/',

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@
288288
"@babel/polyfill": "^7.2.5",
289289
"@babel/preset-env": "^7.3.4",
290290
"@babel/preset-react": "^7.0.0",
291+
"@popcodeorg/eslint-import-resolver-jest": "^2.1.1-popcode.1",
291292
"@redux-saga/testing-utils": "^1.0.1",
292293
"almost-equal": "^1.1.0",
293294
"babel-eslint": "^10.0.1",
@@ -306,7 +307,7 @@
306307
"eslint": "^5.6.0",
307308
"eslint-import-resolver-webpack": "^0.10.1",
308309
"eslint-loader": "^2.1.1",
309-
"eslint-plugin-import": "^2.14.0",
310+
"eslint-plugin-import": "^2.17.2",
310311
"eslint-plugin-jest": "^22.3.0",
311312
"eslint-plugin-private-props": "^0.3.0",
312313
"eslint-plugin-promise": "^4.0.1",
@@ -342,6 +343,7 @@
342343
"postcss-preset-env": "^5.3.0",
343344
"raw-loader": "^0.5.1",
344345
"redux-saga-test-plan": "^4.0.0-beta.2",
346+
"rosie": "^2.0.1",
345347
"script-ext-html-webpack-plugin": "^2.0.1",
346348
"sinon": "^5.0.7",
347349
"source-map-explorer": "^1.4.0",
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import linkGithubIdentity from '../linkGithubIdentity';
2+
import {
3+
linkGithub,
4+
saveCredentialForCurrentUser,
5+
} from '../../clients/firebase';
6+
import {getProfileForAuthenticatedUser} from '../../clients/github';
7+
import {bugsnagClient} from '../../util/bugsnag';
8+
9+
import {
10+
credentialFactory,
11+
credentialInUseErrorFactory,
12+
firebaseErrorFactory,
13+
userFactory,
14+
} from '@factories/clients/firebase';
15+
16+
import {githubProfileFactory} from '@factories/clients/github';
17+
18+
jest.mock('../../clients/firebase.js');
19+
jest.mock('../../clients/github.js');
20+
jest.mock('../../util/bugsnag.js');
21+
22+
describe('linkGithubIdentity', () => {
23+
test('success', async() => {
24+
const mockCredential = credentialFactory.build();
25+
const mockUser = userFactory.build();
26+
27+
linkGithub.mockResolvedValue({
28+
user: mockUser,
29+
credential: mockCredential,
30+
});
31+
32+
const {
33+
type,
34+
payload: {
35+
credential: {providerId},
36+
user,
37+
},
38+
} = await linkGithubIdentity.process();
39+
40+
expect(linkGithub).toHaveBeenCalledWith();
41+
expect(saveCredentialForCurrentUser).toHaveBeenCalledWith(mockCredential);
42+
expect(type).toBe('IDENTITY_LINKED');
43+
expect(providerId).toBe(mockCredential.providerId);
44+
expect(user).toEqual(mockUser);
45+
});
46+
47+
test('credential already in use', async() => {
48+
const error = credentialInUseErrorFactory.build();
49+
const githubProfile = githubProfileFactory.build();
50+
51+
linkGithub.mockRejectedValue(error);
52+
getProfileForAuthenticatedUser.mockResolvedValue(githubProfile);
53+
54+
const {
55+
type,
56+
payload: {
57+
credential: {
58+
providerId,
59+
accessToken,
60+
},
61+
},
62+
} = await linkGithubIdentity.process();
63+
expect(linkGithub).toHaveBeenCalledWith();
64+
expect(getProfileForAuthenticatedUser).toHaveBeenCalledWith(
65+
error.credential.accessToken,
66+
);
67+
expect(type).toBe('ACCOUNT_MIGRATION_NEEDED');
68+
expect(providerId).toBe(error.credential.providerId);
69+
expect(accessToken).toBe(error.credential.accessToken);
70+
});
71+
72+
test('other error', async() => {
73+
const otherError = firebaseErrorFactory.build();
74+
75+
linkGithub.mockRejectedValue(otherError);
76+
bugsnagClient.notify.mockResolvedValue();
77+
78+
const {
79+
type,
80+
error,
81+
payload: {message},
82+
} = await linkGithubIdentity.process();
83+
expect(linkGithub).toHaveBeenCalledWith();
84+
expect(bugsnagClient.notify).toHaveBeenCalledWith(otherError);
85+
expect(type).toBe('LINK_IDENTITY_FAILED');
86+
expect(error).toBe(true);
87+
expect(message).toBe(otherError.code);
88+
});
89+
});

src/logic/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import linkGithubIdentity from './linkGithubIdentity';
12
import unlinkGithubIdentity from './unlinkGithubIdentity';
23

3-
export default [unlinkGithubIdentity];
4+
export default [
5+
linkGithubIdentity,
6+
unlinkGithubIdentity,
7+
];

src/logic/linkGithubIdentity.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {createLogic} from 'redux-logic';
2+
3+
import {
4+
linkGithub,
5+
saveCredentialForCurrentUser,
6+
} from '../clients/firebase';
7+
import {
8+
accountMigrationNeeded,
9+
identityLinked,
10+
linkIdentityFailed,
11+
} from '../actions/user';
12+
import {getProfileForAuthenticatedUser} from '../clients/github';
13+
import {bugsnagClient} from '../util/bugsnag';
14+
15+
export default createLogic({
16+
type: 'LINK_GITHUB_IDENTITY',
17+
async process() {
18+
try {
19+
const {user: userData, credential} = await linkGithub();
20+
await saveCredentialForCurrentUser(credential);
21+
return identityLinked(userData, credential);
22+
} catch (e) {
23+
if (e.code === 'auth/credential-already-in-use') {
24+
const {data: githubProfile} = await getProfileForAuthenticatedUser(
25+
e.credential.accessToken,
26+
);
27+
return accountMigrationNeeded(githubProfile, e.credential);
28+
}
29+
await bugsnagClient.notify(e);
30+
return linkIdentityFailed(e);
31+
}
32+
},
33+
});

src/sagas/user.js

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,14 @@ import {
1111
} from 'redux-saga/effects';
1212
import {
1313
accountMigrationComplete,
14-
accountMigrationNeeded,
1514
accountMigrationUndoPeriodExpired,
16-
identityLinked,
17-
linkIdentityFailed,
1815
accountMigrationError,
1916
} from '../actions/user';
2017
import {getCurrentAccountMigration} from '../selectors';
2118
import {
22-
linkGithub,
2319
migrateAccount,
2420
signOut,
25-
saveCredentialForCurrentUser,
2621
} from '../clients/firebase';
27-
import {getProfileForAuthenticatedUser} from '../clients/github';
28-
29-
export function* linkGithubIdentity() {
30-
try {
31-
const {user: userData, credential} = yield call(linkGithub);
32-
yield call(saveCredentialForCurrentUser, credential);
33-
yield put(identityLinked(userData, credential));
34-
} catch (e) {
35-
switch (e.code) {
36-
case 'auth/credential-already-in-use': {
37-
const {data: githubProfile} = yield call(
38-
getProfileForAuthenticatedUser,
39-
e.credential.accessToken,
40-
);
41-
yield put(accountMigrationNeeded(githubProfile, e.credential));
42-
break;
43-
}
44-
45-
default:
46-
yield call([bugsnagClient, 'notify'], e);
47-
yield put(linkIdentityFailed(e));
48-
}
49-
}
50-
}
5122

5223
export function* startAccountMigration() {
5324
const {shouldContinue} = yield race({
@@ -82,7 +53,6 @@ export function* logOut() {
8253

8354
export default function* user() {
8455
yield all([
85-
takeEvery('LINK_GITHUB_IDENTITY', linkGithubIdentity),
8656
takeEvery('LOG_OUT', logOut),
8757
takeEvery('START_ACCOUNT_MIGRATION', startAccountMigration),
8858
]);

0 commit comments

Comments
 (0)