packages/core: added StaticAuthSessionManager, for sessions without refresh

This commit is contained in:
Patrik Oldsberg
2020-05-25 15:55:01 +02:00
parent 5ca73f6d02
commit 98ebcf9c54
2 changed files with 182 additions and 0 deletions
@@ -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();
});
});
@@ -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<T> = {
/** The connector used for acting on the auth session */
connector: AuthConnector<T>;
/** Used to get the scope of the session */
sessionScopes: (session: T) => Set<string>;
/** The default scopes that should always be present in a session, defaults to none. */
defaultScopes?: Set<string>;
};
/**
* StaticAuthSessionManager manages an underlying session that does not expire.
*/
export class StaticAuthSessionManager<T> implements SessionManager<T> {
private readonly connector: AuthConnector<T>;
private readonly helper: SessionScopeHelper<T>;
private currentSession: T | undefined;
constructor(options: Options<T>) {
const { connector, defaultScopes = new Set(), sessionScopes } = options;
this.connector = connector;
this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes });
}
async getSession(options: {
optional: false;
scopes?: Set<string>;
}): Promise<T>;
async getSession(options: {
optional?: boolean;
scopes?: Set<string>;
}): Promise<T | undefined>;
async getSession(options: {
optional?: boolean;
scopes?: Set<string>;
}): Promise<T | undefined> {
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?
}
}