Repository: parse-community/Parse-SDK-JS
Pull request #779 (2019-04-02 19:37:36)

Add test cases for FacebookUtils
Improve documentation for `link` and `logIn`. Minor clean up

Improves coverage (no tests exists)

Flow types will be in a separate PR.

codecov[bot]: # [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/779?src=pr&el=h1) Report
> Merging [#779](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/779?src=pr&el=desc) into [master](https://codecov.io/gh/parse-community/Parse-SDK-JS/commit/02366c9d5a556c82367a001006375142d7baf91a?src=pr&el=desc) will **increase** coverage by `1.41%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/779/graphs/tree.svg?width=650&token=8nCENM0QHj&height=150&src=pr)](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/779?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #779      +/-   ##
==========================================
+ Coverage   90.57%   91.99%   +1.41%     
==========================================
  Files          54       54              
  Lines        4903     4897       -6     
  Branches     1088     1086       -2     
==========================================
+ Hits         4441     4505      +64     
+ Misses        462      392      -70
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/779?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/FacebookUtils.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/779/diff?src=pr&el=tree#diff-c3JjL0ZhY2Vib29rVXRpbHMuanM=) | `100% <100%> (+95.89%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/779?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/779?src=pr&el=footer). Last update [02366c9...6fa80b5](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/779?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/integration/server.js b/integration/server.js
index d7a5c4a..967028b 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/src/FacebookUtils.js b/src/FacebookUtils.js
index 297de79..85729b9 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:
+   *
+   * <code>logIn(permission: string, authData: Object);</code>
+   *
+   * Advanced API: Used for handling your own oAuth tokens
+   * {@link https://docs.parseplatform.org/rest/guide/#linking-users}
+   *
+   * <code>logIn(authData: Object, options?: Object);</code>
+   *
    * @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:
+   *
+   * <code>link(user: Parse.User, permission: string, authData?: Object);</code>
+   *
+   * Advanced API: Used for handling your own oAuth tokens
+   * {@link https://docs.parseplatform.org/rest/guide/#linking-users}
+   *
+   * <code>link(user: Parse.User, authData: Object, options?: FullOptions);</code>
+   *
    * @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;
   }
 };
 


Test patch:
diff --git a/integration/test/ParseUserTest.js b/integration/test/ParseUserTest.js
index 15a0d82..78a8809 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/__tests__/FacebookUtils-test.js b/src/__tests__/FacebookUtils-test.js
new file mode 100644
index 0000000..5dd4a53
--- /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();
+  });
+});