From e849fda285413d5dc67cf6f4ab72b7724fb5da2c Mon Sep 17 00:00:00 2001 From: aidynoJ Date: Tue, 4 Feb 2025 17:17:34 +0500 Subject: [PATCH 1/7] STCOR-942: migrate away from reading stripes-config::okapi.url --- src/components/Login/LoginCtrl.js | 6 +- src/components/Root/FFetch.js | 10 +- src/components/Root/FFetch.test.js | 100 ++++++++++++++---- src/components/Root/FXHR.js | 3 +- src/components/Root/FXHR.test.js | 2 +- src/components/Root/Root.js | 1 + src/components/SSOLanding/useSSOSession.js | 4 +- .../SSOLanding/useSSOSession.test.js | 16 ++- src/loginServices.js | 30 +++--- src/loginServices.test.js | 3 - 10 files changed, 115 insertions(+), 60 deletions(-) diff --git a/src/components/Login/LoginCtrl.js b/src/components/Login/LoginCtrl.js index 0d49ebb1a..1da375663 100644 --- a/src/components/Login/LoginCtrl.js +++ b/src/components/Login/LoginCtrl.js @@ -35,9 +35,9 @@ class LoginCtrl extends Component { constructor(props) { super(props); - this.sys = require('stripes-config'); // eslint-disable-line global-require - this.okapiUrl = this.sys.okapi.url; - this.tenant = this.sys.okapi.tenant; + // store is already available on login page with okapi data + this.okapiUrl = this.constructor.contextType.store.getState().okapi.url; + this.tenant = this.constructor.contextType.store.getState().okapi.tenant; if (props.autoLogin && props.autoLogin.username) { this.handleSubmit(props.autoLogin); } diff --git a/src/components/Root/FFetch.js b/src/components/Root/FFetch.js index 658139dc2..7b0563a43 100644 --- a/src/components/Root/FFetch.js +++ b/src/components/Root/FFetch.js @@ -42,7 +42,6 @@ */ import ms from 'ms'; -import { okapi as okapiConfig } from 'stripes-config'; import { setRtrTimeout, setRtrFlsTimeout, @@ -77,10 +76,11 @@ const OKAPI_FETCH_OPTIONS = { }; export class FFetch { - constructor({ logger, store, rtrConfig }) { + constructor({ logger, store, rtrConfig, okapi }) { this.logger = logger; this.store = store; this.rtrConfig = rtrConfig; + this.okapi = okapi; } /** @@ -232,12 +232,12 @@ export class FFetch { */ ffetch = async (resource, options = {}) => { // FOLIO API requests are subject to RTR - if (isFolioApiRequest(resource, okapiConfig.url)) { + if (isFolioApiRequest(resource, this.okapi.url)) { this.logger.log('rtrv', 'will fetch', resource); // on authentication, grab the response to kick of the rotation cycle, // then return the response - if (isAuthenticationRequest(resource, okapiConfig.url)) { + if (isAuthenticationRequest(resource, this.okapi.url)) { this.logger.log('rtr', 'authn request', resource); return this.nativeFetch.apply(global, [resource, options && { ...options, ...OKAPI_FETCH_OPTIONS }]) .then(res => { @@ -266,7 +266,7 @@ export class FFetch { // tries to logout, the logout request will fail. And that's fine, just // fine. We will let them fail, capturing the response and swallowing it // to avoid getting stuck in an error loop. - if (isLogoutRequest(resource, okapiConfig.url)) { + if (isLogoutRequest(resource, this.okapi.url)) { this.logger.log('rtr', 'logout request'); return this.nativeFetch.apply(global, [resource, options && { ...options, ...OKAPI_FETCH_OPTIONS }]) diff --git a/src/components/Root/FFetch.test.js b/src/components/Root/FFetch.test.js index 461414e91..2c486b55f 100644 --- a/src/components/Root/FFetch.test.js +++ b/src/components/Root/FFetch.test.js @@ -20,16 +20,6 @@ jest.mock('../../loginServices', () => ({ getTokenExpiry: jest.fn(() => Promise.resolve()) })); -jest.mock('stripes-config', () => ({ - url: 'okapiUrl', - tenant: 'okapiTenant', - okapi: { - url: 'okapiUrl', - tenant: 'okapiTenant' - } -}), -{ virtual: true }); - const log = jest.fn(); const mockFetch = jest.fn(); @@ -50,7 +40,11 @@ describe('FFetch class', () => { describe('Calling a non-FOLIO API', () => { it('calls native fetch once', async () => { mockFetch.mockResolvedValueOnce('non-okapi-success'); - const testFfetch = new FFetch({ logger: { log } }); + const testFfetch = new FFetch({ logger: { log }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -63,7 +57,11 @@ describe('FFetch class', () => { describe('Calling a FOLIO API fetch', () => { it('calls native fetch once', async () => { mockFetch.mockResolvedValueOnce('okapi-success'); - const testFfetch = new FFetch({ logger: { log } }); + const testFfetch = new FFetch({ logger: { log }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); const response = await global.fetch('okapiUrl/whatever', { testOption: 'test' }); @@ -91,6 +89,10 @@ describe('FFetch class', () => { logger: { log }, store: { dispatch: jest.fn(), + }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' } }); testFfetch.replaceFetch(); @@ -110,7 +112,11 @@ describe('FFetch class', () => { describe('logging out', () => { it('calls native fetch once to log out', async () => { mockFetch.mockResolvedValueOnce('logged out'); - const testFfetch = new FFetch({ logger: { log } }); + const testFfetch = new FFetch({ logger: { log }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -124,7 +130,11 @@ describe('FFetch class', () => { it('fetch failure is silently trapped', async () => { mockFetch.mockRejectedValueOnce('logged out FAIL'); - const testFfetch = new FFetch({ logger: { log } }); + const testFfetch = new FFetch({ logger: { log }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -143,7 +153,11 @@ describe('FFetch class', () => { describe('logging out', () => { it('Calling an okapi fetch with valid token...', async () => { mockFetch.mockResolvedValueOnce('okapi success'); - const testFfetch = new FFetch({ logger: { log } }); + const testFfetch = new FFetch({ logger: { log }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -188,6 +202,10 @@ describe('FFetch class', () => { rtrConfig: { fixedLengthSessionWarningTTL: '1m', }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -244,6 +262,10 @@ describe('FFetch class', () => { rtrConfig: { fixedLengthSessionWarningTTL: '1m', }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -282,6 +304,10 @@ describe('FFetch class', () => { rtrConfig: { fixedLengthSessionWarningTTL: '1m', }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -317,6 +343,10 @@ describe('FFetch class', () => { logger: { log }, store: { dispatch: jest.fn(), + }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' } }); testFfetch.replaceFetch(); @@ -361,6 +391,10 @@ describe('FFetch class', () => { rtrConfig: { fixedLengthSessionWarningTTL: '1m', }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -389,7 +423,11 @@ describe('FFetch class', () => { it('returns the error', async () => { mockFetch.mockResolvedValue('success') .mockResolvedValueOnce('failure'); - const testFfetch = new FFetch({ logger: { log } }); + const testFfetch = new FFetch({ logger: { log }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -411,7 +449,11 @@ describe('FFetch class', () => { }, } )); - const testFfetch = new FFetch({ logger: { log } }); + const testFfetch = new FFetch({ logger: { log }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -435,7 +477,11 @@ describe('FFetch class', () => { } )) .mockRejectedValueOnce(new Error('token error message')); - const testFfetch = new FFetch({ logger: { log } }); + const testFfetch = new FFetch({ logger: { log }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -470,7 +516,11 @@ describe('FFetch class', () => { } } )); - const testFfetch = new FFetch({ logger: { log } }); + const testFfetch = new FFetch({ logger: { log }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -500,7 +550,11 @@ describe('FFetch class', () => { } } )); - const testFfetch = new FFetch({ logger: { log } }); + const testFfetch = new FFetch({ logger: { log }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); @@ -530,7 +584,11 @@ describe('FFetch class', () => { } } )); - const testFfetch = new FFetch({ logger: { log } }); + const testFfetch = new FFetch({ logger: { log }, + okapi:{ + url: 'okapiUrl', + tenant: 'okapiTenant' + } }); testFfetch.replaceFetch(); testFfetch.replaceXMLHttpRequest(); diff --git a/src/components/Root/FXHR.js b/src/components/Root/FXHR.js index c5232fc97..c4f189d81 100644 --- a/src/components/Root/FXHR.js +++ b/src/components/Root/FXHR.js @@ -1,4 +1,3 @@ -import { okapi } from 'stripes-config'; import { getPromise, isFolioApiRequest } from './token-util'; import { RTR_ERROR_EVENT, @@ -15,7 +14,7 @@ export default (deps) => { open = (method, url) => { this.FFetchContext.logger?.log('rtr', 'capture XHR.open'); - this.shouldEnsureToken = isFolioApiRequest(url, okapi.url); + this.shouldEnsureToken = isFolioApiRequest(url, this.FFetchContext.okapi.url); super.open(method, url); } diff --git a/src/components/Root/FXHR.test.js b/src/components/Root/FXHR.test.js index 5c52ee602..712d9aa51 100644 --- a/src/components/Root/FXHR.test.js +++ b/src/components/Root/FXHR.test.js @@ -24,7 +24,7 @@ describe('FXHR', () => { let testXHR; beforeEach(() => { jest.clearAllMocks(); - FakeXHR = FXHR({ tokenExpiration: { atExpires: Date.now(), rtExpires: Date.now() + 5000 }, logger: { log: () => {} } }); + FakeXHR = FXHR({ tokenExpiration: { atExpires: Date.now(), rtExpires: Date.now() + 5000 }, logger: { log: () => {} }, okapi:{ url:'okapiUrl' } }); testXHR = new FakeXHR(); }); diff --git a/src/components/Root/Root.js b/src/components/Root/Root.js index caf9680dd..36ebbcccf 100644 --- a/src/components/Root/Root.js +++ b/src/components/Root/Root.js @@ -73,6 +73,7 @@ class Root extends Component { logger: this.props.logger, store, rtrConfig, + okapi }); this.ffetch.replaceFetch(); this.ffetch.replaceXMLHttpRequest(); diff --git a/src/components/SSOLanding/useSSOSession.js b/src/components/SSOLanding/useSSOSession.js index 8e71b01c4..9290917e6 100644 --- a/src/components/SSOLanding/useSSOSession.js +++ b/src/components/SSOLanding/useSSOSession.js @@ -4,7 +4,7 @@ import { useLocation } from 'react-router-dom'; import { useCookies } from 'react-cookie'; import queryString from 'query-string'; -import { config, okapi } from 'stripes-config'; +import { config } from 'stripes-config'; import { defaultErrors } from '../../constants'; import { setAuthError } from '../../okapiActions'; @@ -45,7 +45,7 @@ const useSSOSession = () => { const tenant = getTenant(params, token, store); useEffect(() => { - requestUserWithPerms(okapi.url, store, tenant, token) + requestUserWithPerms(store.getState().okapi.url, store, tenant, token) .then(() => { if (store.getState()?.okapi?.authFailure) { return Promise.reject(new Error('SSO Failed')); diff --git a/src/components/SSOLanding/useSSOSession.test.js b/src/components/SSOLanding/useSSOSession.test.js index 06eac936e..07e516ec6 100644 --- a/src/components/SSOLanding/useSSOSession.test.js +++ b/src/components/SSOLanding/useSSOSession.test.js @@ -4,7 +4,7 @@ import { useDispatch, useStore } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { useCookies } from 'react-cookie'; -import { config, okapi } from 'stripes-config'; +import { config } from 'stripes-config'; import { defaultErrors } from '../../constants'; import { setAuthError } from '../../okapiActions'; @@ -30,13 +30,11 @@ jest.mock('stripes-config', () => ({ config: { useSecureTokens: true, }, - okapi: { - url: 'okapiUrl', - tenant: 'okapiTenant' - } }), { virtual: true }); +jest.mock(''); + jest.mock('../../loginServices', () => ({ requestUserWithPerms: jest.fn() })); @@ -75,7 +73,7 @@ describe('SSOLanding', () => { renderHook(() => useSSOSession()); - expect(requestUserWithPerms).toHaveBeenCalledWith(okapi.url, store, okapi.tenant, ssoTokenValue); + expect(requestUserWithPerms).toHaveBeenCalledWith(store.getState().okapi.url, store, store.getState().okapi.tenant, ssoTokenValue); }); it('should request user session when RTR is disabled with token from cookies', () => { @@ -86,7 +84,7 @@ describe('SSOLanding', () => { renderHook(() => useSSOSession()); - expect(requestUserWithPerms).toHaveBeenCalledWith(okapi.url, store, okapi.tenant, ssoTokenValue); + expect(requestUserWithPerms).toHaveBeenCalledWith(store.getState().okapi.url, store, 'okapiTenant', ssoTokenValue); }); it('should request user session when RTR is disabled and right tenant from ssoToken', () => { @@ -99,7 +97,7 @@ describe('SSOLanding', () => { renderHook(() => useSSOSession()); - expect(requestUserWithPerms).toHaveBeenCalledWith(okapi.url, store, tokenTenant, ssoTokenValue); + expect(requestUserWithPerms).toHaveBeenCalledWith(store.getState().okapi.url, store, tokenTenant, ssoTokenValue); }); it('should request user session when RTR is enabled and right tenant from query params', () => { @@ -111,7 +109,7 @@ describe('SSOLanding', () => { renderHook(() => useSSOSession()); - expect(requestUserWithPerms).toHaveBeenCalledWith(okapi.url, store, queryTenant, undefined); + expect(requestUserWithPerms).toHaveBeenCalledWith(store.getState().okapi.url, store, queryTenant, undefined); }); it('should display error when session request failed', async () => { diff --git a/src/loginServices.js b/src/loginServices.js index 71670c706..827d64527 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -1,5 +1,5 @@ import localforage from 'localforage'; -import { config, okapi, translations } from 'stripes-config'; +import { config, translations } from 'stripes-config'; import rtlDetect from 'rtl-detect'; import moment from 'moment'; import { loadDayJSLocale } from '@folio/stripes-components'; @@ -400,11 +400,12 @@ function loadResources(store, tenant, userId) { // in mod-configuration so we can only retrieve them if the user has // read-permission for configuration entries. if (canReadConfig(store)) { + const okapiUrl = store.getState()?.okapi?.url; promises = [ - getLocale(okapi.url, store, tenant), - getUserLocale(okapi.url, store, tenant, userId), - getPlugins(okapi.url, store, tenant), - getBindings(okapi.url, store, tenant), + getLocale(okapiUrl, store, tenant), + getUserLocale(okapiUrl, store, tenant, userId), + getPlugins(okapiUrl, store, tenant), + getBindings(okapiUrl, store, tenant), ]; } @@ -783,7 +784,7 @@ export function processOkapiSession(store, tenant, resp, ssoToken) { */ export function validateUser(okapiUrl, store, tenant, session) { const { token, tenant: sessionTenant = tenant } = session; - const usersPath = okapi.authnUrl ? 'users-keycloak' : 'bl-users'; + const usersPath = store.getState()?.okapi?.authnUrl ? 'users-keycloak' : 'bl-users'; return fetch(`${okapiUrl}/${usersPath}/_self?expandPermissions=true`, { headers: getHeaders(sessionTenant, token), credentials: 'include', @@ -869,7 +870,7 @@ export function checkOkapiSession(okapiUrl, store, tenant) { * requestLogin * authenticate with a username and password. return a promise that posts the values * and then processes the result to begin a session. - * @param {object} okapi object from stripes.config.js + * @param {string} okapiUrl * @param {redux store} store * @param {string} tenant * @param {object} data @@ -878,14 +879,13 @@ export function checkOkapiSession(okapiUrl, store, tenant) { */ export function requestLogin(okapiUrl, store, tenant, data) { // got Keycloak? - if (okapi.authnUrl) { - return fetch(okapi.authnUrl, { + if (store.getState().okapi.authnUrl) { + return fetch(store.getState().okapi.authnUrl, { method: 'POST', body: new URLSearchParams({ ...data, 'grant_type': 'password', - 'client_id': okapi.clientId, - 'client_secret': okapi.clientSecret, + 'client_id': store.getState()?.okapi?.clientId, }) }) .then(resp => processOkapiSession(store, tenant, resp)); @@ -910,11 +910,12 @@ export function requestLogin(okapiUrl, store, tenant, data) { * @param {string} tenant * @param {string} token * @param {boolean} rtrIgnore + * @param {boolean} isKeycloak * * @returns {Promise} Promise resolving to the response of the request */ -function fetchUserWithPerms(okapiUrl, tenant, token, rtrIgnore = false) { - const usersPath = okapi.authnUrl ? 'users-keycloak' : 'bl-users'; +function fetchUserWithPerms(okapiUrl, tenant, token, rtrIgnore = false, isKeycloak) { + const usersPath = isKeycloak ? 'users-keycloak' : 'bl-users'; return fetch( `${okapiUrl}/${usersPath}/_self?expandPermissions=true&fullPermissions=true`, { @@ -934,7 +935,8 @@ function fetchUserWithPerms(okapiUrl, tenant, token, rtrIgnore = false) { * @returns {Promise} Promise resolving to the response-body (JSON) of the request */ export function requestUserWithPerms(okapiUrl, store, tenant, token) { - return fetchUserWithPerms(okapiUrl, tenant, token, !token) + const isKeycloak = store.getState().okapi?.authnUrl; + return fetchUserWithPerms(okapiUrl, tenant, token, !token, isKeycloak) .then((resp) => { if (resp.ok) { return processOkapiSession(store, tenant, resp, token); diff --git a/src/loginServices.test.js b/src/loginServices.test.js index da048d851..a55bf9bb5 100644 --- a/src/loginServices.test.js +++ b/src/loginServices.test.js @@ -62,9 +62,6 @@ jest.mock('stripes-config', () => ({ remus: { name: 'remus', clientId: 'remus-application' }, } }, - okapi: { - authnUrl: 'https://authn.url', - }, translations: {} })); From acb53daaa0fdd26242be6d5fd990af2b36f15368 Mon Sep 17 00:00:00 2001 From: aidynoJ Date: Wed, 5 Feb 2025 14:53:32 +0500 Subject: [PATCH 2/7] fix unit tests --- src/loginServices.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/loginServices.test.js b/src/loginServices.test.js index a55bf9bb5..61ad1d20a 100644 --- a/src/loginServices.test.js +++ b/src/loginServices.test.js @@ -95,6 +95,7 @@ describe('createOkapiSession', () => { getState: () => ({ okapi: { currentPerms: [], + url:'okapiUrl' } }), }; @@ -267,6 +268,7 @@ describe('validateUser', () => { it('handles fetch failure from "_self"', async () => { const store = { dispatch: jest.fn(), + getState: () => ({ okapi: { tenant: 'monkeyURL' } }), }; mockFetchError(); @@ -279,6 +281,7 @@ describe('validateUser', () => { it('handles valid user with empty tenant in session', async () => { const store = { dispatch: jest.fn(), + getState: () => ({ okapi: { tenant: 'monkeyURL' } }), }; const tenant = 'tenant'; @@ -307,6 +310,7 @@ describe('validateUser', () => { it('handles valid user with tenant in session', async () => { const store = { dispatch: jest.fn(), + getState: () => ({ okapi: { tenant: 'monkeyURL' } }), }; const tenant = 'tenant'; @@ -332,6 +336,7 @@ describe('validateUser', () => { it('overwrites session data with new values from _self', async () => { const store = { dispatch: jest.fn(), + getState: () => ({ okapi: { tenant: 'monkeyURL' } }), }; const tenant = 'tenant'; From 5ce3d7a5df88a370259bde72c1d1eaf43d872459 Mon Sep 17 00:00:00 2001 From: aidynoJ Date: Wed, 5 Feb 2025 16:06:54 +0500 Subject: [PATCH 3/7] use tenant, okapiUrl from store in LoginCtrl.js --- src/components/Login/LoginCtrl.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Login/LoginCtrl.js b/src/components/Login/LoginCtrl.js index 1da375663..246c1f160 100644 --- a/src/components/Login/LoginCtrl.js +++ b/src/components/Login/LoginCtrl.js @@ -18,6 +18,8 @@ class LoginCtrl extends Component { static propTypes = { authFailure: PropTypes.arrayOf(PropTypes.object), ssoEnabled: PropTypes.bool, + okapiUrl: PropTypes.string.isRequired, + tenant: PropTypes.string.isRequired, autoLogin: PropTypes.shape({ username: PropTypes.string.isRequired, password: PropTypes.string.isRequired, @@ -35,9 +37,6 @@ class LoginCtrl extends Component { constructor(props) { super(props); - // store is already available on login page with okapi data - this.okapiUrl = this.constructor.contextType.store.getState().okapi.url; - this.tenant = this.constructor.contextType.store.getState().okapi.tenant; if (props.autoLogin && props.autoLogin.username) { this.handleSubmit(props.autoLogin); } @@ -54,7 +53,7 @@ class LoginCtrl extends Component { } handleSubmit = (data) => { - return requestLogin(this.okapiUrl, this.context.store, this.tenant, data) + return requestLogin(this.props.okapiUrl, this.context.store, this.tenant, data) .then(this.handleSuccessfulLogin) .catch(e => { console.error(e); // eslint-disable-line no-console @@ -62,7 +61,7 @@ class LoginCtrl extends Component { } handleSSOLogin = () => { - requestSSOLogin(this.okapiUrl, this.tenant); + requestSSOLogin(this.props.okapiUrl, this.props.tenant); } render() { @@ -82,6 +81,8 @@ class LoginCtrl extends Component { const mapStateToProps = state => ({ authFailure: state.okapi.authFailure, ssoEnabled: state.okapi.ssoEnabled, + okapiUrl: state.okapi.url, + tenant: state.okapi.tenant, }); const mapDispatchToProps = dispatch => ({ clearAuthErrors: () => dispatch(setAuthError([])), From 804e289aed44ee03234ada6dc05c9e5cef26352b Mon Sep 17 00:00:00 2001 From: aidynoJ Date: Wed, 5 Feb 2025 16:15:27 +0500 Subject: [PATCH 4/7] leave signature for load resources --- src/loginServices.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/loginServices.js b/src/loginServices.js index 827d64527..c6c9f5f6d 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -400,12 +400,12 @@ function loadResources(store, tenant, userId) { // in mod-configuration so we can only retrieve them if the user has // read-permission for configuration entries. if (canReadConfig(store)) { - const okapiUrl = store.getState()?.okapi?.url; + const okapi = store.getState()?.okapi; promises = [ - getLocale(okapiUrl, store, tenant), - getUserLocale(okapiUrl, store, tenant, userId), - getPlugins(okapiUrl, store, tenant), - getBindings(okapiUrl, store, tenant), + getLocale(okapi.url, store, tenant), + getUserLocale(okapi.url, store, tenant, userId), + getPlugins(okapi.url, store, tenant), + getBindings(okapi.url, store, tenant), ]; } From bf977b0d1b9a69f0f080e7bc2afed0b2a47b9d3f Mon Sep 17 00:00:00 2001 From: aidynoJ Date: Wed, 5 Feb 2025 17:49:44 +0500 Subject: [PATCH 5/7] cover loginServices.test.js --- src/loginServices.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/loginServices.test.js b/src/loginServices.test.js index 61ad1d20a..3e3e975ca 100644 --- a/src/loginServices.test.js +++ b/src/loginServices.test.js @@ -268,7 +268,7 @@ describe('validateUser', () => { it('handles fetch failure from "_self"', async () => { const store = { dispatch: jest.fn(), - getState: () => ({ okapi: { tenant: 'monkeyURL' } }), + getState: () => ({ okapi: { tenant: 'monkey', url: 'monkeyUrl' } }), }; mockFetchError(); @@ -281,7 +281,7 @@ describe('validateUser', () => { it('handles valid user with empty tenant in session', async () => { const store = { dispatch: jest.fn(), - getState: () => ({ okapi: { tenant: 'monkeyURL' } }), + getState: () => ({ okapi: { tenant: 'monkey', url: 'monkeyUrl', currentPerms: { 'configuration.entries.collection.get': true } } }), }; const tenant = 'tenant'; @@ -310,7 +310,7 @@ describe('validateUser', () => { it('handles valid user with tenant in session', async () => { const store = { dispatch: jest.fn(), - getState: () => ({ okapi: { tenant: 'monkeyURL' } }), + getState: () => ({ okapi: { tenant: 'monkey', url: 'monkeyUrl' } }), }; const tenant = 'tenant'; @@ -336,7 +336,7 @@ describe('validateUser', () => { it('overwrites session data with new values from _self', async () => { const store = { dispatch: jest.fn(), - getState: () => ({ okapi: { tenant: 'monkeyURL' } }), + getState: () => ({ okapi: { tenant: 'monkey', url: 'monkeyUrl' } }), }; const tenant = 'tenant'; @@ -385,7 +385,7 @@ describe('validateUser', () => { it('handles invalid user', async () => { const store = { dispatch: jest.fn(), - getState: () => ({ okapi: { tenant: 'monkey' } }), + getState: () => ({ okapi: { tenant: 'monkey', url: 'monkeyUrl' } }), }; global.fetch = jest.fn().mockImplementation(() => { From 77aa3cee080c725dd87cac1f6d313df4d4cf7e16 Mon Sep 17 00:00:00 2001 From: aidynoJ Date: Thu, 6 Feb 2025 12:13:54 +0500 Subject: [PATCH 6/7] cover loginServices.test.js second round) --- src/loginServices.js | 5 +-- src/loginServices.test.js | 66 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/loginServices.js b/src/loginServices.js index c6c9f5f6d..053bb796c 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -914,7 +914,7 @@ export function requestLogin(okapiUrl, store, tenant, data) { * * @returns {Promise} Promise resolving to the response of the request */ -function fetchUserWithPerms(okapiUrl, tenant, token, rtrIgnore = false, isKeycloak) { +function fetchUserWithPerms(okapiUrl, tenant, token, rtrIgnore = false, isKeycloak = false) { const usersPath = isKeycloak ? 'users-keycloak' : 'bl-users'; return fetch( `${okapiUrl}/${usersPath}/_self?expandPermissions=true&fullPermissions=true`, @@ -931,11 +931,12 @@ function fetchUserWithPerms(okapiUrl, tenant, token, rtrIgnore = false, isKeyclo * @param {string} okapiUrl * @param {redux store} store * @param {string} tenant + * @param {string} token * * @returns {Promise} Promise resolving to the response-body (JSON) of the request */ export function requestUserWithPerms(okapiUrl, store, tenant, token) { - const isKeycloak = store.getState().okapi?.authnUrl; + const isKeycloak = Boolean(store.getState().okapi?.authnUrl); return fetchUserWithPerms(okapiUrl, tenant, token, !token, isKeycloak) .then((resp) => { if (resp.ok) { diff --git a/src/loginServices.test.js b/src/loginServices.test.js index 3e3e975ca..0d9169005 100644 --- a/src/loginServices.test.js +++ b/src/loginServices.test.js @@ -23,7 +23,8 @@ import { updateUser, validateUser, IS_LOGGING_OUT, - SESSION_NAME, getStoredTenant + SESSION_NAME, getStoredTenant, + requestLogin, } from './loginServices'; import { @@ -207,6 +208,7 @@ describe('processOkapiSession', () => { getState: () => ({ okapi: { currentPerms: [], + authnUrl: 'keycloakURL' } }), }; @@ -729,4 +731,66 @@ describe('unauthorizedPath functions', () => { expect(parsedTenant).toStrictEqual(value); }); }); + + describe('requestLogin', () => { + afterEach(() => { + mockFetchCleanUp(); + }); + it('should authenticate via Keycloak when authnUrl is configured', async () => { + const mockStore = { + getState: () => ({ + okapi: { + authnUrl: 'https://keycloakUrl.com', + clientId: 'monkey-id', + tenant:'monkey', + + url: 'monkey' + } + }), + dispatch: jest.fn() + }; + + mockFetchSuccess({}); + + await requestLogin( + 'https://okapiUrl.com', + mockStore, + 'test-tenant', + { username: 'testuser', password: 'testpass' } + ); + + expect(global.fetch).toHaveBeenCalledWith( + 'https://keycloakUrl.com', + { method: 'POST', body: expect.any(URLSearchParams) } + ); + }); + + it('should use legacy authentication when Keycloak is not available', async () => { + const mockStore = { + getState: () => ({ + okapi: {}, + }), + dispatch: jest.fn() + }; + mockFetchSuccess({}); + + await requestLogin( + 'http://okapi-url', + mockStore, + 'test-tenant', + { username: 'testuser', password: 'testpass' } + ); + + expect(global.fetch).toHaveBeenCalledWith( + 'http://okapi-url/bl-users/login?expandPermissions=true&fullPermissions=true', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + 'X-Okapi-Tenant': 'test-tenant', + 'Content-Type': 'application/json' + }) + }) + ); + }); + }); }); From df70f5bf65426c73df5a7900c96455558c7d3a38 Mon Sep 17 00:00:00 2001 From: aidynoJ Date: Thu, 13 Feb 2025 18:37:48 +0500 Subject: [PATCH 7/7] STCOR-942: remove redundant condition for loginRequest --- src/loginServices.js | 32 +++++++++----------------------- src/loginServices.test.js | 30 +----------------------------- 2 files changed, 10 insertions(+), 52 deletions(-) diff --git a/src/loginServices.js b/src/loginServices.js index 053bb796c..3f8d0530a 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -878,29 +878,15 @@ export function checkOkapiSession(okapiUrl, store, tenant) { * @returns {Promise} */ export function requestLogin(okapiUrl, store, tenant, data) { - // got Keycloak? - if (store.getState().okapi.authnUrl) { - return fetch(store.getState().okapi.authnUrl, { - method: 'POST', - body: new URLSearchParams({ - ...data, - 'grant_type': 'password', - 'client_id': store.getState()?.okapi?.clientId, - }) - }) - .then(resp => processOkapiSession(store, tenant, resp)); - } else { - // legacy built-in authentication - const loginPath = config.useSecureTokens ? 'login-with-expiry' : 'login'; - return fetch(`${okapiUrl}/bl-users/${loginPath}?expandPermissions=true&fullPermissions=true`, { - body: JSON.stringify(data), - credentials: 'include', - headers: { 'X-Okapi-Tenant': tenant, 'Content-Type': 'application/json' }, - method: 'POST', - mode: 'cors', - }) - .then(resp => processOkapiSession(store, tenant, resp)); - } + const loginPath = config.useSecureTokens ? 'login-with-expiry' : 'login'; + return fetch(`${okapiUrl}/bl-users/${loginPath}?expandPermissions=true&fullPermissions=true`, { + body: JSON.stringify(data), + credentials: 'include', + headers: { 'X-Okapi-Tenant': tenant, 'Content-Type': 'application/json' }, + method: 'POST', + mode: 'cors', + }) + .then(resp => processOkapiSession(store, tenant, resp)); } /** diff --git a/src/loginServices.test.js b/src/loginServices.test.js index 0d9169005..286d71242 100644 --- a/src/loginServices.test.js +++ b/src/loginServices.test.js @@ -736,36 +736,8 @@ describe('unauthorizedPath functions', () => { afterEach(() => { mockFetchCleanUp(); }); - it('should authenticate via Keycloak when authnUrl is configured', async () => { - const mockStore = { - getState: () => ({ - okapi: { - authnUrl: 'https://keycloakUrl.com', - clientId: 'monkey-id', - tenant:'monkey', - - url: 'monkey' - } - }), - dispatch: jest.fn() - }; - - mockFetchSuccess({}); - - await requestLogin( - 'https://okapiUrl.com', - mockStore, - 'test-tenant', - { username: 'testuser', password: 'testpass' } - ); - - expect(global.fetch).toHaveBeenCalledWith( - 'https://keycloakUrl.com', - { method: 'POST', body: expect.any(URLSearchParams) } - ); - }); - it('should use legacy authentication when Keycloak is not available', async () => { + it('should authenticate and create session when valid credentials provided', async () => { const mockStore = { getState: () => ({ okapi: {},