Repository: auth0/auth0-spa-js
Pull request #561 (2020-08-26 14:55:47)

[SDK-1924] client methods should handle partially filled arguments
### Description

`getUser`, `getIdTokenClaims` and `getTokenWithPopup` relies on a default object argument for it's defaults, this can be an issue when passing a partially filled or empty object argument eg `getUser({})`. This will not use the default object so not apply the default arguments.

### References

See #554 

### Testing

Login to the SPA JS playground
call `await auth0.getUser({})`

Expected: an object containing your user data
Actual: undefined

- [ ] This change adds test coverage for new/changed/fixed functionality

### Checklist

- [ ] I have added documentation for new/changed functionality in this PR or in auth0.com/docs
- [ ] All active GitHub checks for tests, formatting, and security are passing
- [ ] The correct base branch is being used, if not `master`



Patch:
diff --git a/src/Auth0Client.ts b/src/Auth0Client.ts
index e1fb764..a87b774 100644
--- a/src/Auth0Client.ts
+++ b/src/Auth0Client.ts
@@ -303,6 +303,7 @@ export default class Auth0Client {
    * otherwise the popup will be blocked in most browsers.
    *
    * @param options
+   * @param config
    */
   public async loginWithPopup(
     options: PopupLoginOptions = {},
@@ -379,17 +380,14 @@ export default class Auth0Client {
    *
    * @param options
    */
-  public async getUser(
-    options: GetUserOptions = {
-      audience: this.options.audience || 'default',
-      scope: this.scope || this.defaultScope
-    }
-  ) {
-    options.scope = getUniqueScopes(this.defaultScope, options.scope);
+  public async getUser(options: GetUserOptions = {}) {
+    const audience = options.audience || this.options.audience || 'default';
+    const scope = getUniqueScopes(this.defaultScope, this.scope, options.scope);
 
     const cache = this.cache.get({
       client_id: this.options.client_id,
-      ...options
+      audience,
+      scope
     });
 
     return cache && cache.decodedToken && cache.decodedToken.user;
@@ -404,21 +402,14 @@ export default class Auth0Client {
    *
    * @param options
    */
-  public async getIdTokenClaims(
-    options: GetIdTokenClaimsOptions = {
-      audience: this.options.audience || 'default',
-      scope: this.scope || this.defaultScope
-    }
-  ) {
-    options.scope = getUniqueScopes(
-      this.defaultScope,
-      this.scope,
-      options.scope
-    );
+  public async getIdTokenClaims(options: GetIdTokenClaimsOptions = {}) {
+    const audience = options.audience || this.options.audience || 'default';
+    const scope = getUniqueScopes(this.defaultScope, this.scope, options.scope);
 
     const cache = this.cache.get({
       client_id: this.options.client_id,
-      ...options
+      audience,
+      scope
     });
 
     return cache && cache.decodedToken && cache.decodedToken.claims;
@@ -643,19 +634,22 @@ export default class Auth0Client {
    * results will be valid according to their expiration times.
    *
    * @param options
+   * @param config
    */
   public async getTokenWithPopup(
-    options: GetTokenWithPopupOptions = {
-      audience: this.options.audience,
-      scope: this.scope || this.defaultScope
-    },
-    config: PopupConfigOptions = DEFAULT_POPUP_CONFIG_OPTIONS
+    options: GetTokenWithPopupOptions = {},
+    config: PopupConfigOptions = {}
   ) {
+    options.audience = options.audience || this.options.audience;
     options.scope = getUniqueScopes(
       this.defaultScope,
       this.scope,
       options.scope
     );
+    config = {
+      ...DEFAULT_POPUP_CONFIG_OPTIONS,
+      ...config
+    };
 
     await this.loginWithPopup(options, config);
 
diff --git a/src/global.ts b/src/global.ts
index f5ed311..49ee571 100644
--- a/src/global.ts
+++ b/src/global.ts
@@ -201,22 +201,22 @@ export interface GetUserOptions {
   /**
    * The scope that was used in the authentication request
    */
-  scope: string;
+  scope?: string;
   /**
    * The audience that was used in the authentication request
    */
-  audience: string;
+  audience?: string;
 }
 
 export interface GetIdTokenClaimsOptions {
   /**
    * The scope that was used in the authentication request
    */
-  scope: string;
+  scope?: string;
   /**
    * The audience that was used in the authentication request
    */
-  audience: string;
+  audience?: string;
 }
 
 /*


Test patch:
diff --git a/__tests__/Auth0Client.test.ts b/__tests__/Auth0Client.test.ts
index 4bc0830..527ddd8 100644
--- a/__tests__/Auth0Client.test.ts
+++ b/__tests__/Auth0Client.test.ts
@@ -62,7 +62,10 @@ const setup = (
         exp: Date.now() / 1000 + 86400
       },
       claims
-    )
+    ),
+    user: {
+      sub: 'me'
+    }
   });
 
   return auth0;
@@ -192,6 +195,23 @@ describe('Auth0Client', () => {
     });
   });
 
+  it('should log the user in and get the user', async () => {
+    const auth0 = setup({ scope: 'foo' });
+    await login(auth0);
+    expect(await auth0.getUser()).toBeTruthy();
+    expect(await auth0.getUser({})).toBeTruthy();
+    expect(await auth0.getUser({ audience: 'default' })).toBeTruthy();
+    expect(await auth0.getUser({ scope: 'foo' })).toBeTruthy();
+    expect(await auth0.getUser({ audience: 'invalid' })).toBeUndefined();
+    expect(await auth0.getIdTokenClaims()).toBeTruthy();
+    expect(await auth0.getIdTokenClaims({})).toBeTruthy();
+    expect(await auth0.getIdTokenClaims({ audience: 'default' })).toBeTruthy();
+    expect(await auth0.getIdTokenClaims({ scope: 'foo' })).toBeTruthy();
+    expect(
+      await auth0.getIdTokenClaims({ audience: 'invalid' })
+    ).toBeUndefined();
+  });
+
   it('should log the user in with custom auth0Client', async () => {
     const auth0Client = { name: '__test_client__', version: '0.0.0' };
     const auth0 = setup({ auth0Client });
diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts
index dcf8db8..7fb3d67 100644
--- a/__tests__/index.test.ts
+++ b/__tests__/index.test.ts
@@ -2064,6 +2064,32 @@ describe('Auth0', () => {
       const token = await auth0.getTokenWithPopup();
       expect(token).toBe(TEST_ACCESS_TOKEN);
     });
+
+    it('accepts empty options and config', async () => {
+      const { auth0 } = await localSetup({ audience: 'foo' });
+
+      await auth0.getTokenWithPopup();
+      expect(auth0.loginWithPopup).toHaveBeenCalledWith(
+        {
+          audience: 'foo',
+          scope: 'openid profile email'
+        },
+        { timeoutInSeconds: 60 }
+      );
+    });
+
+    it('accepts partial options and config', async () => {
+      const { auth0 } = await localSetup({ audience: 'foo' });
+
+      await auth0.getTokenWithPopup({ scope: 'bar' }, { popup: 'baz' });
+      expect(auth0.loginWithPopup).toHaveBeenCalledWith(
+        {
+          audience: 'foo',
+          scope: 'openid profile email bar'
+        },
+        { timeoutInSeconds: 60, popup: 'baz' }
+      );
+    });
   });
 
   describe('logout()', () => {