add a unit test

Signed-off-by: Gasan Guseinov <gasan.guseinov@ing.com>
This commit is contained in:
Gasan Guseinov
2025-01-23 18:37:54 +01:00
parent 74a9cd58b8
commit 8a61795308
7 changed files with 310 additions and 87 deletions
+3
View File
@@ -422,5 +422,8 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
};
}
console.log('retry');
console.log(`args: ${JSON.stringify(args)}`);
await runJest(args);
}
+32 -2
View File
@@ -21,6 +21,7 @@ import { AuthRequestOptions } from '@backstage/core-plugin-api';
import { BackstageIdentityApi } from '@backstage/core-plugin-api';
import { BackstageIdentityResponse } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { BackstageUserIdentity } from '@backstage/core-plugin-api';
import { bitbucketAuthApiRef } from '@backstage/core-plugin-api';
import { bitbucketServerAuthApiRef } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
@@ -547,7 +548,9 @@ export class OAuth2
SessionApi
{
// (undocumented)
static create(options: OAuth2CreateOptions): OAuth2;
static create(
options: OAuth2CreateOptions | OAuth2CreateOptionsWithAuthConnector,
): OAuth2;
// (undocumented)
getAccessToken(
scope?: string | string[],
@@ -562,6 +565,11 @@ export class OAuth2
// (undocumented)
getProfile(options?: AuthRequestOptions): Promise<ProfileInfo | undefined>;
// (undocumented)
static normalizeScopes(
scopeTransform: (scopes: string[]) => string[],
scopes?: string | string[],
): Set<string>;
// (undocumented)
sessionState$(): Observable<SessionState>;
// (undocumented)
signIn(): Promise<void>;
@@ -573,7 +581,29 @@ export class OAuth2
export type OAuth2CreateOptions = OAuthApiCreateOptions & {
scopeTransform?: (scopes: string[]) => string[];
popupOptions?: PopupOptions;
authConnector?: AuthConnector<OAuth2Session>;
};
// @public
export type OAuth2CreateOptionsWithAuthConnector = {
scopeTransform?: (scopes: string[]) => string[];
defaultScopes?: string[];
authConnector: AuthConnector<OAuth2Session>;
};
// @public (undocumented)
export type OAuth2Response = {
providerInfo: {
accessToken: string;
idToken: string;
scope: string;
expiresInSeconds?: number;
};
profile: ProfileInfo;
backstageIdentity: {
token: string;
expiresInSeconds?: number;
identity: BackstageUserIdentity;
};
};
// @public
@@ -18,6 +18,14 @@ import OAuth2 from './OAuth2';
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import { mockApis } from '@backstage/test-utils';
import {
OAuth2Session,
AuthConnector,
AuthConnectorRefreshSessionOptions,
openLoginPopup,
// OAuth2Response,
OAuth2CreateOptionsWithAuthConnector,
} from '../../../../index';
const theFuture = new Date(Date.now() + 3600000);
const thePast = new Date(Date.now() - 10);
@@ -37,6 +45,25 @@ jest.mock('../../../../lib/AuthSessionManager', () => ({
const configApi = mockApis.config();
class CustomAuthConnector implements AuthConnector<OAuth2Session> {
async createSession() {
const s: OAuth2Session = {
providerInfo: {
idToken: '',
accessToken: 'accessToken',
scopes: new Set(['myScope']),
},
profile: {},
};
await openLoginPopup({ url: 'http://localhost', name: 'myPopup' });
return Promise.resolve(s);
}
async refreshSession(_?: AuthConnectorRefreshSessionOptions): Promise<any> {}
async removeSession(): Promise<void> {}
}
describe('OAuth2', () => {
it('should get refreshed access token', async () => {
getSession = jest.fn().mockResolvedValue({
@@ -215,4 +242,25 @@ describe('OAuth2', () => {
await expect(promise3).resolves.toBe('token2');
expect(getSession).toHaveBeenCalledTimes(4); // De-duping of session requests happens in client
});
it('should use provided auth provider', async () => {
getSession = jest.fn().mockResolvedValue({
providerInfo: { accessToken: 'access-token', expiresAt: theFuture },
});
const customAuthConnector = new CustomAuthConnector();
const options: OAuth2CreateOptionsWithAuthConnector = {
scopeTransform,
defaultScopes: ['myScope'],
authConnector: customAuthConnector,
};
const oauth2 = OAuth2.create(options);
expect(await oauth2.getAccessToken('my-scope my-scope2')).toBe(
'access-token',
);
expect(getSession).toHaveBeenCalledWith(
expect.objectContaining({ scopes: new Set(['my-scope', 'my-scope2']) }),
);
});
});
@@ -14,53 +14,26 @@
* limitations under the License.
*/
import {
AuthConnector,
DefaultAuthConnector,
PopupOptions,
} from '../../../../lib/AuthConnector';
import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
import { SessionManager } from '../../../../lib/AuthSessionManager/types';
import {
AuthRequestOptions,
BackstageIdentityApi,
BackstageIdentityResponse,
OAuthApi,
OpenIdConnectApi,
ProfileInfo,
ProfileInfoApi,
SessionState,
SessionApi,
BackstageIdentityApi,
BackstageUserIdentity,
SessionState,
} from '@backstage/core-plugin-api';
import { Observable } from '@backstage/types';
import { OAuth2Session } from './types';
import { OAuthApiCreateOptions } from '../types';
/**
* OAuth2 create options.
* @public
*/
export type OAuth2CreateOptions = OAuthApiCreateOptions & {
scopeTransform?: (scopes: string[]) => string[];
popupOptions?: PopupOptions;
authConnector?: AuthConnector<OAuth2Session>;
};
export type OAuth2Response = {
providerInfo: {
accessToken: string;
idToken: string;
scope: string;
expiresInSeconds?: number;
};
profile: ProfileInfo;
backstageIdentity: {
token: string;
expiresInSeconds?: number;
identity: BackstageUserIdentity;
};
};
import {
OAuth2CreateOptions,
OAuth2CreateOptionsWithAuthConnector,
OAuth2Response,
OAuth2Session,
} from './types';
const DEFAULT_PROVIDER = {
id: 'oauth2',
@@ -81,61 +54,67 @@ export default class OAuth2
BackstageIdentityApi,
SessionApi
{
static create(options: OAuth2CreateOptions) {
private static createAuthConnector(
options: OAuth2CreateOptions | OAuth2CreateOptionsWithAuthConnector,
) {
if ('authConnector' in options) {
return options.authConnector;
}
const {
scopeTransform = x => x,
configApi,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
defaultScopes = [],
scopeTransform = x => x,
popupOptions,
} = options;
const connector =
options.authConnector ??
new DefaultAuthConnector({
configApi,
discoveryApi,
environment,
provider,
oauthRequestApi: oauthRequestApi,
sessionTransform({
backstageIdentity,
...res
}: OAuth2Response): OAuth2Session {
const session: OAuth2Session = {
...res,
providerInfo: {
idToken: res.providerInfo.idToken,
accessToken: res.providerInfo.accessToken,
scopes: OAuth2.normalizeScopes(
scopeTransform,
res.providerInfo.scope,
),
expiresAt: res.providerInfo.expiresInSeconds
? new Date(
Date.now() + res.providerInfo.expiresInSeconds * 1000,
)
: undefined,
},
return new DefaultAuthConnector({
configApi,
discoveryApi,
environment,
provider,
oauthRequestApi: oauthRequestApi,
sessionTransform({
backstageIdentity,
...res
}: OAuth2Response): OAuth2Session {
const session: OAuth2Session = {
...res,
providerInfo: {
idToken: res.providerInfo.idToken,
accessToken: res.providerInfo.accessToken,
scopes: OAuth2.normalizeScopes(
scopeTransform,
res.providerInfo.scope,
),
expiresAt: res.providerInfo.expiresInSeconds
? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000)
: undefined,
},
};
if (backstageIdentity) {
session.backstageIdentity = {
token: backstageIdentity.token,
identity: backstageIdentity.identity,
expiresAt: backstageIdentity.expiresInSeconds
? new Date(Date.now() + backstageIdentity.expiresInSeconds * 1000)
: undefined,
};
if (backstageIdentity) {
session.backstageIdentity = {
token: backstageIdentity.token,
identity: backstageIdentity.identity,
expiresAt: backstageIdentity.expiresInSeconds
? new Date(
Date.now() + backstageIdentity.expiresInSeconds * 1000,
)
: undefined,
};
}
return session;
},
popupOptions,
});
}
return session;
},
popupOptions,
});
}
static create(
options: OAuth2CreateOptions | OAuth2CreateOptionsWithAuthConnector,
) {
const { defaultScopes = [], scopeTransform = x => x } = options;
const connector = OAuth2.createAuthConnector(options);
const sessionManager = new RefreshingAuthSessionManager({
connector,
@@ -218,7 +197,10 @@ export default class OAuth2
return session?.profile;
}
private static normalizeScopes(
/**
* @public
*/
public static normalizeScopes(
scopeTransform: (scopes: string[]) => string[],
scopes?: string | string[],
): Set<string> {
@@ -0,0 +1,121 @@
/*
* 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 OAuth2 from './OAuth2';
import {
OAuth2Session,
AuthConnector,
AuthConnectorRefreshSessionOptions,
openLoginPopup,
OAuth2CreateOptionsWithAuthConnector,
OAuth2Response,
} from '../../../../index';
const scopeTransform = (x: string[]) => x;
type Options = {
/**
* Function used to transform an auth response into the session type.
*/
sessionTransform?(response: any): OAuth2Session | Promise<OAuth2Session>;
};
class CustomAuthConnector implements AuthConnector<OAuth2Session> {
private readonly sessionTransform: (response: any) => Promise<OAuth2Session>;
constructor(options: Options) {
const { sessionTransform = id => id } = options;
this.sessionTransform = sessionTransform;
}
async createSession() {
return await this.sessionTransform(
await openLoginPopup({ url: 'http://my-origin', name: 'myPopup' }),
);
}
async refreshSession(_?: AuthConnectorRefreshSessionOptions): Promise<any> {}
async removeSession(): Promise<void> {}
}
describe('OAuth2', () => {
it('should use provided auth provider', async () => {
const popupMock = { closed: false };
jest.spyOn(window, 'open').mockReturnValue(popupMock as Window);
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
jest.spyOn(window, 'removeEventListener');
const customAuthConnector = new CustomAuthConnector({
sessionTransform(res: OAuth2Response): OAuth2Session {
return {
...res,
providerInfo: {
idToken: res.providerInfo.idToken,
accessToken: res.providerInfo.accessToken,
scopes: OAuth2.normalizeScopes(
scopeTransform,
res.providerInfo.scope,
),
expiresAt: res.providerInfo.expiresInSeconds
? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000)
: undefined,
},
};
},
});
const options: OAuth2CreateOptionsWithAuthConnector = {
scopeTransform,
defaultScopes: ['myScope'],
authConnector: customAuthConnector,
};
const oauth2 = OAuth2.create(options);
// so that AuthConnector calls openLoginPopup synchronously (not try to refresh the token)
const accessToken = oauth2.getAccessToken('myScope', {
instantPopup: true,
optional: false,
});
const listener = addEventListenerSpy.mock.calls[0][1] as EventListener;
const accessTokenValue = 'myAccessToken';
const myResponse = {
providerInfo: {
accessToken: accessTokenValue,
scope: 'myScope',
expiresInSeconds: 900,
},
profile: { displayName: 'John Doe' },
};
// A valid sessions response
listener({
source: popupMock,
origin: 'http://my-origin',
data: {
type: 'authorization_response',
response: myResponse,
},
} as MessageEvent);
return expect(accessToken).resolves.toBe(accessTokenValue);
});
});
@@ -15,11 +15,13 @@
*/
import {
ProfileInfo,
BackstageIdentityResponse,
BackstageUserIdentity,
ProfileInfo,
} from '@backstage/core-plugin-api';
import { OAuthApiCreateOptions } from '../types.ts';
import { AuthConnector, PopupOptions } from '../../../../lib';
export type { OAuth2CreateOptions } from './OAuth2';
export type { PopupOptions } from '../../../../lib/AuthConnector';
/**
* Session information for generic OAuth2 auth.
@@ -36,3 +38,40 @@ export type OAuth2Session = {
profile: ProfileInfo;
backstageIdentity?: BackstageIdentityResponse;
};
/**
* @public
*/
export type OAuth2Response = {
providerInfo: {
accessToken: string;
idToken: string;
scope: string;
expiresInSeconds?: number;
};
profile: ProfileInfo;
backstageIdentity: {
token: string;
expiresInSeconds?: number;
identity: BackstageUserIdentity;
};
};
/**
* OAuth2 create options.
* @public
*/
export type OAuth2CreateOptions = OAuthApiCreateOptions & {
scopeTransform?: (scopes: string[]) => string[];
popupOptions?: PopupOptions;
};
/**
* OAuth2 create options with custom auth connector.
* @public
*/
export type OAuth2CreateOptionsWithAuthConnector = {
scopeTransform?: (scopes: string[]) => string[];
defaultScopes?: string[];
authConnector: AuthConnector<OAuth2Session>;
};
+1 -1
View File
@@ -27,6 +27,6 @@ export type {
AuthConnector,
AuthConnectorCreateSessionOptions,
AuthConnectorRefreshSessionOptions,
openLoginPopup,
OpenLoginPopupOptions,
} from './lib';
export { openLoginPopup } from './lib';