core-app-api: added OptionalRefreshSessionManagerMux + tests

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-08-20 16:09:54 +02:00
parent eae4656cb8
commit 2fbcaab096
2 changed files with 243 additions and 0 deletions
@@ -0,0 +1,138 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { Observable, SessionState } from '@backstage/core-plugin-api';
import { OptionalRefreshSessionManagerMux } from './OptionalRefreshSessionManagerMux';
import { MutableSessionManager, SessionManager } from './types';
class MockManager implements MutableSessionManager<string> {
constructor(public session?: string) {}
setSession(session: string | undefined): void {
this.session = session;
}
async getSession(): Promise<string | undefined> {
return this.session;
}
async removeSession(): Promise<void> {
delete this.session;
}
sessionState$(): Observable<SessionState> {
throw new Error('Method not implemented.');
}
}
function trackState(manager: SessionManager<any>) {
const states = new Array<boolean>();
manager
.sessionState$()
.subscribe(state => states.push(state === SessionState.SignedIn));
return states;
}
describe('OptionalRefreshSessionManagerMux', () => {
it('finds no session', async () => {
const mux = new OptionalRefreshSessionManagerMux({
staticSessionManager: new MockManager(),
refreshingSessionManager: new MockManager(),
sessionCanRefresh: () => false,
});
const states = trackState(mux);
await expect(mux.getSession({})).resolves.toBe(undefined);
expect(states).toEqual([false]);
});
it('prioritizes a static session', async () => {
const mux = new OptionalRefreshSessionManagerMux({
staticSessionManager: new MockManager('static'),
refreshingSessionManager: new MockManager('refreshing'),
sessionCanRefresh: () => false,
});
const states = trackState(mux);
await expect(mux.getSession({})).resolves.toBe('static');
expect(states).toEqual([false, true]);
});
it('transfers a refreshing session to the static manager', async () => {
const staticSessionManager = new MockManager();
const refreshingSessionManager = new MockManager('refreshing');
const mux = new OptionalRefreshSessionManagerMux({
staticSessionManager,
refreshingSessionManager,
sessionCanRefresh: () => false,
});
const states = trackState(mux);
expect(staticSessionManager.session).toBeUndefined();
await expect(mux.getSession({})).resolves.toBe('refreshing');
expect(staticSessionManager.session).toBe('refreshing');
expect(states).toEqual([false, true]);
});
it('relies on the refreshing manager if refresh is available', async () => {
const staticSessionManager = new MockManager();
const refreshingSessionManager = new MockManager('refreshing');
const mux = new OptionalRefreshSessionManagerMux({
staticSessionManager,
refreshingSessionManager,
sessionCanRefresh: () => true,
});
const states = trackState(mux);
await expect(mux.getSession({})).resolves.toBe('refreshing');
expect(staticSessionManager.session).toBeUndefined();
expect(states).toEqual([false, true]);
});
it('can switch between refreshing and static sessions', async () => {
let canRefresh = true;
const staticSessionManager = new MockManager();
const refreshingSessionManager = new MockManager('refreshing');
const mux = new OptionalRefreshSessionManagerMux({
staticSessionManager,
refreshingSessionManager,
sessionCanRefresh: () => canRefresh,
});
const states = trackState(mux);
await expect(mux.getSession({})).resolves.toBe('refreshing');
expect(staticSessionManager.session).toBeUndefined();
canRefresh = false;
await expect(mux.getSession({})).resolves.toBe('refreshing');
expect(staticSessionManager.session).toBe('refreshing');
expect(states).toEqual([false, true]);
});
it('removes sessions from both managers', async () => {
const staticSessionManager = new MockManager('static');
const refreshingSessionManager = new MockManager('refreshing');
const mux = new OptionalRefreshSessionManagerMux({
staticSessionManager,
refreshingSessionManager,
sessionCanRefresh: () => true,
});
const states = trackState(mux);
await expect(mux.getSession({})).resolves.toBe('static');
await mux.removeSession();
expect(staticSessionManager.session).toBeUndefined();
expect(refreshingSessionManager.session).toBeUndefined();
expect(states).toEqual([false, true, false]);
});
});
@@ -0,0 +1,105 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { Observable, SessionState } from '@backstage/core-plugin-api';
import {
SessionManager,
MutableSessionManager,
GetSessionOptions,
} from './types';
import { SessionStateTracker } from './SessionStateTracker';
type Options<T> = {
/**
* A callback that is called to determine whether a given session supports refresh
*/
sessionCanRefresh: (session: T) => boolean;
/**
* The session manager that is used if the a session does not support refresh.
*/
staticSessionManager: MutableSessionManager<T>;
/**
* The session manager that is used if the a session supports refresh.
*/
refreshingSessionManager: SessionManager<T>;
};
/**
* OptionalRefreshSessionManagerMux wraps two different session managers, one for
* static session storage and another one that supports refresh. For each session
* that is retrieved is checked for whether it supports refresh. If it does, the
* refreshing session manager is used, otherwise the static session manager is used.
*/
export class OptionalRefreshSessionManagerMux<T> implements SessionManager<T> {
private readonly stateTracker = new SessionStateTracker();
private readonly sessionCanRefresh: (session: T) => boolean;
private readonly staticSessionManager: MutableSessionManager<T>;
private readonly refreshingSessionManager: SessionManager<T>;
constructor(options: Options<T>) {
this.sessionCanRefresh = options.sessionCanRefresh;
this.staticSessionManager = options.staticSessionManager;
this.refreshingSessionManager = options.refreshingSessionManager;
}
async getSession(options: GetSessionOptions): Promise<T | undefined> {
// First we check if there is an existing static session, using an optional request
const staticSession = await this.staticSessionManager.getSession({
...options,
optional: true,
});
if (staticSession) {
this.stateTracker.setIsSignedIn(true);
return staticSession;
}
// If there is no static session available, we ask the refresh manager to get a session
const session = await this.refreshingSessionManager.getSession(options);
// Handling the case where the session request is optional
if (!session) {
this.stateTracker.setIsSignedIn(false);
return undefined;
}
// Next we check if the session we received from the refreshing manager can actually
// be refreshed. If it can, we use this session without storing it in the static manager.
if (this.sessionCanRefresh(session)) {
this.stateTracker.setIsSignedIn(true);
return session;
}
// If the session can't be refreshed, we store it in the static manager
this.staticSessionManager.setSession(session);
this.stateTracker.setIsSignedIn(true);
return session;
}
async removeSession(): Promise<void> {
await Promise.all([
this.refreshingSessionManager.removeSession(),
this.staticSessionManager.removeSession(),
]);
this.stateTracker.setIsSignedIn(false);
}
sessionState$(): Observable<SessionState> {
return this.stateTracker.sessionState$();
}
}