diff --git a/integration/server.js b/integration/server.js index d7a5c4abe..967028b57 100644 --- a/integration/server.js +++ b/integration/server.js @@ -18,6 +18,9 @@ const api = new ParseServer({ module: CustomAuth, option1: 'hello', option2: 'world', + }, + facebook: { + appIds: "test" } } }); diff --git a/integration/test/ParseUserTest.js b/integration/test/ParseUserTest.js index 15a0d82a8..78a8809e9 100644 --- a/integration/test/ParseUserTest.js +++ b/integration/test/ParseUserTest.js @@ -31,6 +31,19 @@ const provider = { }; Parse.User._registerAuthenticationProvider(provider); +const authResponse = { + userID: 'test', + accessToken: 'test', + expiresIn: 'test', // Should be unix timestamp +}; +global.FB = { + init: () => {}, + login: (cb) => { + cb({ authResponse }); + }, + getAuthResponse: () => authResponse, +}; + describe('Parse User', () => { beforeAll(() => { Parse.initialize('integration', null, 'notsosecret'); @@ -673,4 +686,38 @@ describe('Parse User', () => { await user._unlinkFrom(provider); expect(user._isLinked(provider)).toBe(false); }); + + it('can login with facebook', async () => { + Parse.User.enableUnsafeCurrentUser(); + Parse.FacebookUtils.init(); + const user = await Parse.FacebookUtils.logIn(); + expect(Parse.FacebookUtils.isLinked(user)).toBe(true); + }); + + it('can link user with facebook', async () => { + Parse.User.enableUnsafeCurrentUser(); + Parse.FacebookUtils.init(); + const user = new Parse.User(); + user.setUsername('Alice'); + user.setPassword('sekrit'); + await user.signUp(); + await Parse.FacebookUtils.link(user); + expect(Parse.FacebookUtils.isLinked(user)).toBe(true); + await Parse.FacebookUtils.unlink(user); + expect(Parse.FacebookUtils.isLinked(user)).toBe(false); + }); + + it('can link anonymous user with facebook', async () => { + Parse.User.enableUnsafeCurrentUser(); + Parse.FacebookUtils.init(); + const user = await Parse.AnonymousUtils.logIn(); + await Parse.FacebookUtils.link(user); + + expect(Parse.FacebookUtils.isLinked(user)).toBe(true); + expect(Parse.AnonymousUtils.isLinked(user)).toBe(true); + await Parse.FacebookUtils.unlink(user); + + expect(Parse.FacebookUtils.isLinked(user)).toBe(false); + expect(Parse.AnonymousUtils.isLinked(user)).toBe(true); + }); }); diff --git a/src/FacebookUtils.js b/src/FacebookUtils.js index 297de79b5..85729b91a 100644 --- a/src/FacebookUtils.js +++ b/src/FacebookUtils.js @@ -143,6 +143,15 @@ const FacebookUtils = { * SDK to authenticate the user, and then automatically logs in (or * creates, in the case where it is a new user) a Parse.User. * + * Standard API: + * + * logIn(permission: string, authData: Object); + * + * Advanced API: Used for handling your own oAuth tokens + * {@link https://docs.parseplatform.org/rest/guide/#linking-users} + * + * logIn(authData: Object, options?: Object); + * * @method logIn * @name Parse.FacebookUtils.logIn * @param {(String|Object)} permissions The permissions required for Facebook @@ -150,8 +159,7 @@ const FacebookUtils = { * Alternatively, supply a Facebook authData object as described in our * REST API docs if you want to handle getting facebook auth tokens * yourself. - * @param {Object} options Standard options object with success and error - * callbacks. + * @param {Object} options MasterKey / SessionToken. Alternatively can be used for authData if permissions is a string * @returns {Promise} */ logIn(permissions, options) { @@ -163,16 +171,9 @@ const FacebookUtils = { } requestedPermissions = permissions; return ParseUser._logInWith('facebook', options); - } else { - const newOptions = {}; - if (options) { - for (const key in options) { - newOptions[key] = options[key]; - } - } - newOptions.authData = permissions; - return ParseUser._logInWith('facebook', newOptions); } + const authData = { authData: permissions }; + return ParseUser._logInWith('facebook', authData, options); }, /** @@ -180,6 +181,15 @@ const FacebookUtils = { * Facebook SDK to authenticate the user, and then automatically links * the account to the Parse.User. * + * Standard API: + * + * link(user: Parse.User, permission: string, authData?: Object); + * + * Advanced API: Used for handling your own oAuth tokens + * {@link https://docs.parseplatform.org/rest/guide/#linking-users} + * + * link(user: Parse.User, authData: Object, options?: FullOptions); + * * @method link * @name Parse.FacebookUtils.link * @param {Parse.User} user User to link to Facebook. This must be the @@ -189,8 +199,7 @@ const FacebookUtils = { * Alternatively, supply a Facebook authData object as described in our * REST API docs if you want to handle getting facebook auth tokens * yourself. - * @param {Object} options Standard options object with success and error - * callbacks. + * @param {Object} options MasterKey / SessionToken. Alternatively can be used for authData if permissions is a string * @returns {Promise} */ link(user, permissions, options) { @@ -202,16 +211,9 @@ const FacebookUtils = { } requestedPermissions = permissions; return user._linkWith('facebook', options); - } else { - const newOptions = {}; - if (options) { - for (const key in options) { - newOptions[key] = options[key]; - } - } - newOptions.authData = permissions; - return user._linkWith('facebook', newOptions); } + const authData = { authData: permissions }; + return user._linkWith('facebook', authData, options); }, /** @@ -232,6 +234,11 @@ const FacebookUtils = { ); } return user._unlinkFrom('facebook', options); + }, + + // Used for testing purposes + _getAuthProvider() { + return provider; } }; diff --git a/src/__tests__/FacebookUtils-test.js b/src/__tests__/FacebookUtils-test.js new file mode 100644 index 000000000..5dd4a5366 --- /dev/null +++ b/src/__tests__/FacebookUtils-test.js @@ -0,0 +1,297 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +jest.dontMock('../FacebookUtils'); + +class MockUser { + constructor () { + this.className = '_User'; + this.attributes = {}; + } + _isLinked() {} + _linkWith() {} + _unlinkFrom() {} + static _registerAuthenticationProvider() {} + static _logInWith() {} +} + +jest.setMock('../ParseUser', MockUser); + +const FacebookUtils = require('../FacebookUtils').default; + +describe('FacebookUtils', () => { + beforeEach(() => { + jest.clearAllMocks(); + const authResponse = { + userID: 'test', + accessToken: 'test', + expiresIn: 'test', // Should be unix timestamp + }; + global.FB = { + init: () => {}, + login: (cb) => { + cb({ authResponse }); + }, + getAuthResponse: () => authResponse, + }; + }); + + it('can not init without FB SDK', () => { + global.FB = undefined; + try { + FacebookUtils.init(); + } catch (e) { + expect(e.message).toBe('The Facebook JavaScript SDK must be loaded before calling init.'); + } + }); + + it('can not login without init', async () => { + try { + await FacebookUtils.logIn(); + } catch (e) { + expect(e.message).toBe('You must initialize FacebookUtils before calling logIn.'); + } + }); + + it('can not link without init', async () => { + try { + const user = new MockUser(); + await FacebookUtils.link(user); + } catch (e) { + expect(e.message).toBe('You must initialize FacebookUtils before calling link.'); + } + }); + + it('can not unlink without init', () => { + try { + const user = new MockUser(); + FacebookUtils.unlink(user); + } catch (e) { + expect(e.message).toBe('You must initialize FacebookUtils before calling unlink.'); + } + }); + + it('can init', () => { + FacebookUtils.init(); + }); + + it('can init with options', () => { + jest.spyOn(console, 'warn') + .mockImplementationOnce(() => { + return { + call: () => {} + } + }) + FacebookUtils.init({ status: true }); + expect(console.warn).toHaveBeenCalled(); + }); + + it('can link', async () => { + FacebookUtils.init(); + const user = new MockUser(); + await FacebookUtils.link(user); + }); + + it('can link with permission string', async () => { + FacebookUtils.init(); + const user = new MockUser(); + await FacebookUtils.link(user, 'public_profile'); + }); + + it('can link with authData object', async () => { + FacebookUtils.init(); + const user = new MockUser(); + const authData = { + id: '1234' + }; + jest.spyOn(user, '_linkWith'); + await FacebookUtils.link(user, authData); + expect(user._linkWith).toHaveBeenCalledWith('facebook', { authData: { id: '1234' } }, undefined); + }); + + it('can link with options', async () => { + FacebookUtils.init(); + const user = new MockUser(); + jest.spyOn(user, '_linkWith'); + await FacebookUtils.link(user, {}, { useMasterKey: true }); + expect(user._linkWith).toHaveBeenCalledWith('facebook', { authData: {} }, { useMasterKey: true }); + }); + + it('can check isLinked', async () => { + FacebookUtils.init(); + const user = new MockUser(); + jest.spyOn(user, '_isLinked'); + await FacebookUtils.isLinked(user); + expect(user._isLinked).toHaveBeenCalledWith('facebook'); + }); + + it('can unlink', async () => { + FacebookUtils.init(); + const user = new MockUser(); + const spy = jest.spyOn(user, '_unlinkFrom'); + await FacebookUtils.unlink(user); + expect(user._unlinkFrom).toHaveBeenCalledTimes(1); + spy.mockRestore(); + }); + + it('can login', async () => { + FacebookUtils.init(); + await FacebookUtils.logIn(); + }); + + it('can login with permission string', async () => { + FacebookUtils.init(); + jest.spyOn(MockUser, '_logInWith'); + await FacebookUtils.logIn('public_profile'); + expect(MockUser._logInWith).toHaveBeenCalledTimes(1); + }); + + it('can login with authData', async () => { + FacebookUtils.init(); + jest.spyOn(MockUser, '_logInWith'); + await FacebookUtils.logIn({ id: '1234' }); + expect(MockUser._logInWith).toHaveBeenCalledTimes(1); + }); + + it('can login with options', async () => { + FacebookUtils.init(); + jest.spyOn(MockUser, '_logInWith'); + await FacebookUtils.logIn({}, { useMasterKey: true }); + expect(MockUser._logInWith).toHaveBeenCalledWith('facebook', { authData: {} }, {useMasterKey: true }); + }); + + it('provider getAuthType', async () => { + const provider = FacebookUtils._getAuthProvider(); + expect(provider.getAuthType()).toBe('facebook'); + }); + + it('provider deauthenticate', async () => { + const provider = FacebookUtils._getAuthProvider(); + jest.spyOn(provider, 'restoreAuthentication'); + provider.deauthenticate(); + expect(provider.restoreAuthentication).toHaveBeenCalled(); + }); +}); + +describe('FacebookUtils provider', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('restoreAuthentication', async () => { + const provider = FacebookUtils._getAuthProvider(); + const didRestore = provider.restoreAuthentication(); + expect(didRestore).toBe(true); + }); + + it('restoreAuthentication with invalid authData', async () => { + global.FB = { + init: () => {}, + logout: () => {}, + getAuthResponse: () => { + return { userID: '5678' }; + }, + }; + jest.spyOn(global.FB, 'logout'); + const provider = FacebookUtils._getAuthProvider(); + provider.restoreAuthentication({ id: '1234'}); + expect(global.FB.logout).toHaveBeenCalled(); + }); + + it('restoreAuthentication with valid authData', async () => { + global.FB = { + init: () => {}, + getAuthResponse: () => { + return { userID: '1234' }; + }, + }; + FacebookUtils.init({ status: false }); + jest.spyOn(global.FB, 'init'); + const provider = FacebookUtils._getAuthProvider(); + provider.restoreAuthentication({ id: '1234'}); + expect(global.FB.init).toHaveBeenCalled(); + }); + + it('restoreAuthentication with valid authData', async () => { + global.FB = { + init: () => {}, + getAuthResponse: () => { + return { userID: '1234' }; + }, + }; + FacebookUtils.init({ status: false }); + jest.spyOn(global.FB, 'init'); + const provider = FacebookUtils._getAuthProvider(); + provider.restoreAuthentication({ id: '1234'}); + expect(global.FB.init).toHaveBeenCalled(); + }); + + it('authenticate without FB error', async () => { + global.FB = undefined; + const options = { + error: () => {} + }; + jest.spyOn(options, 'error'); + const provider = FacebookUtils._getAuthProvider(); + try { + provider.authenticate(options); + } catch (e) { + expect(options.error).toHaveBeenCalledWith(provider, 'Facebook SDK not found.'); + } + }); + + it('authenticate with FB response', async () => { + const authResponse = { + userID: '1234', + accessToken: 'access_token', + expiresIn: '2000-01-01', + }; + global.FB = { + init: () => {}, + login: (cb) => { + cb({ authResponse }); + }, + }; + const options = { + success: () => {} + }; + jest.spyOn(options, 'success'); + const provider = FacebookUtils._getAuthProvider(); + provider.authenticate(options); + expect(options.success).toHaveBeenCalledWith(provider, { access_token: 'access_token', expiration_date: null, id: '1234' }); + }); + + it('authenticate with no FB response', async () => { + global.FB = { + init: () => {}, + login: (cb) => { + cb({}); + }, + }; + const options = { + error: () => {} + }; + jest.spyOn(options, 'error'); + const provider = FacebookUtils._getAuthProvider(); + provider.authenticate(options); + expect(options.error).toHaveBeenCalledWith(provider, {}); + }); + + it('getAuthType', async () => { + const provider = FacebookUtils._getAuthProvider(); + expect(provider.getAuthType()).toBe('facebook'); + }); + + it('deauthenticate', async () => { + const provider = FacebookUtils._getAuthProvider(); + jest.spyOn(provider, 'restoreAuthentication'); + provider.deauthenticate(); + expect(provider.restoreAuthentication).toHaveBeenCalled(); + }); +});