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();
+ });
+});