diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.test.ts b/packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.test.ts new file mode 100644 index 0000000000..dc13aec0c2 --- /dev/null +++ b/packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { StaticAuthSessionManager } from './StaticAuthSessionManager'; + +const defaultOptions = { + sessionScopes: (session: string) => new Set(session.split(' ')), +}; + +describe('StaticAuthSessionManager', () => { + const baseConnector = { + refreshSession() { + throw new Error('refreshSession should not be called'); + }, + removeSession() { + throw new Error('removeSession should not be called'); + }, + }; + + it('should get session by creating session once', async () => { + const createSession = jest.fn().mockResolvedValue('my-session'); + const manager = new StaticAuthSessionManager({ + connector: { createSession, ...baseConnector }, + ...defaultOptions, + }); + + expect(createSession).toHaveBeenCalledTimes(0); + await expect(manager.getSession({})).resolves.toBe('my-session'); + expect(createSession).toHaveBeenCalledTimes(1); + await expect(manager.getSession({})).resolves.toBe('my-session'); + expect(createSession).toHaveBeenCalledTimes(1); + }); + + it('should fail to get session if user rejects the request', async () => { + const createSession = jest.fn().mockRejectedValue(new Error('NOPE')); + const manager = new StaticAuthSessionManager({ + connector: { createSession, ...baseConnector }, + ...defaultOptions, + }); + + expect(createSession).toHaveBeenCalledTimes(0); + await expect(manager.getSession({})).rejects.toThrow('NOPE'); + expect(createSession).toHaveBeenCalledTimes(1); + await expect(manager.getSession({ optional: true })).resolves.toBe( + undefined, + ); + }); + + it('should only request auth once for same scopes', async () => { + const createSession = jest + .fn() + .mockImplementation(scopes => [...scopes].join(' ')); + const manager = new StaticAuthSessionManager({ + connector: { createSession, ...baseConnector }, + ...defaultOptions, + }); + + expect(createSession).toHaveBeenCalledTimes(0); + await expect(manager.getSession({ scopes: new Set(['a']) })).resolves.toBe( + 'a', + ); + expect(createSession).toHaveBeenCalledTimes(1); + await expect(manager.getSession({ scopes: new Set(['a']) })).resolves.toBe( + 'a', + ); + expect(createSession).toHaveBeenCalledTimes(1); + await expect(manager.getSession({ scopes: new Set(['b']) })).resolves.toBe( + 'a b', + ); + expect(createSession).toHaveBeenCalledTimes(2); + }); + + it('should remove session and reload', async () => { + const location = { ...window.location }; + delete window.location; + window.location = location; + jest.spyOn(window.location, 'reload').mockImplementation(); + + const removeSession = jest.fn(); + const manager = new StaticAuthSessionManager({ + connector: { removeSession }, + ...defaultOptions, + } as any); + + await manager.removeSession(); + expect(window.location.reload).toHaveBeenCalled(); + expect(removeSession).toHaveBeenCalled(); + }); +}); diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.ts b/packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.ts new file mode 100644 index 0000000000..8d057ecc58 --- /dev/null +++ b/packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SessionManager } from './types'; +import { AuthConnector } from '../AuthConnector'; +import { SessionScopeHelper } from './common'; + +type Options = { + /** The connector used for acting on the auth session */ + connector: AuthConnector; + /** Used to get the scope of the session */ + sessionScopes: (session: T) => Set; + /** The default scopes that should always be present in a session, defaults to none. */ + defaultScopes?: Set; +}; + +/** + * StaticAuthSessionManager manages an underlying session that does not expire. + */ +export class StaticAuthSessionManager implements SessionManager { + private readonly connector: AuthConnector; + private readonly helper: SessionScopeHelper; + + private currentSession: T | undefined; + + constructor(options: Options) { + const { connector, defaultScopes = new Set(), sessionScopes } = options; + + this.connector = connector; + this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes }); + } + + async getSession(options: { + optional: false; + scopes?: Set; + }): Promise; + async getSession(options: { + optional?: boolean; + scopes?: Set; + }): Promise; + async getSession(options: { + optional?: boolean; + scopes?: Set; + }): Promise { + if ( + this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes) + ) { + return this.currentSession; + } + + // If we continue here we will show a popup, so exit if this is an optional session request. + if (options.optional) { + return undefined; + } + + // We can call authRequester multiple times, the returned session will contain all requested scopes. + this.currentSession = await this.connector.createSession( + this.helper.getExtendedScope(this.currentSession, options.scopes), + ); + return this.currentSession; + } + + async removeSession() { + await this.connector.removeSession(); + window.location.reload(); // TODO(Rugvip): make this work without reload? + } +}