Skip to content

Commit

Permalink
Merge branch 'master' into test-AuthErrorsContainer
Browse files Browse the repository at this point in the history
  • Loading branch information
zburke committed May 30, 2024
2 parents 8a02575 + c58e079 commit 7befb96
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Idle-session timeout and "Keep working?" modal. Refs STCOR-776.
* Implement password validation for Login Page. Refs STCOR-741.
* Avoid deprecated `defaultProps` for functional components. Refs STCOR-844..
* Update session data with values from `_self` request on reload. Refs STCOR-846.

## [10.1.0](https://github.com/folio-org/stripes-core/tree/v10.1.0) (2024-03-12)
[Full Changelog](https://github.com/folio-org/stripes-core/compare/v10.0.0...v10.1.0)
Expand Down
75 changes: 75 additions & 0 deletions src/components/TitleManager/TitleManager.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { render, waitFor } from '@folio/jest-config-stripes/testing-library/react';
import { createMemoryHistory } from 'history';

import TitleManager from './TitleManager';
import Harness from '../../../test/jest/helpers/harness';

describe('TitleManager', () => {
it('renders a title with a default postfix', async () => {
const stripes = {
config: {},
hasPerm: jest.fn(),
};

const page = 'record-application';

const history = createMemoryHistory();
render(
<Harness history={history} stripes={stripes}>
<TitleManager page={page}>
<>thunder - chicken</>
</TitleManager>
</Harness>
);

await waitFor(() => expect(document.title).toBe(`${page} - FOLIO`));
});

it('renders prefix, page, record, postfix', async () => {
const stripes = {
config: {
platformName: 'two mile',
},
hasPerm: jest.fn(),
};

const prefix = 'pre';
const page = 'steve';
const record = '8:41.5';

const history = createMemoryHistory();
render(
<Harness history={history} stripes={stripes}>
<TitleManager page={page} prefix={prefix} record={record}>
<>thunder - chicken</>
</TitleManager>
</Harness>
);

await waitFor(() => expect(document.title).toBe(`${prefix}${page} - ${record} - ${stripes.config.platformName}`));
});


it('renders prefix, record, postfix', async () => {
const stripes = {
config: {
platformName: 'two mile',
},
hasPerm: jest.fn(),
};

const prefix = 'pre';
const record = '8:41.5';

const history = createMemoryHistory();
render(
<Harness history={history} stripes={stripes}>
<TitleManager prefix={prefix} record={record}>
<>thunder - chicken</>
</TitleManager>
</Harness>
);

await waitFor(() => expect(document.title).toBe(`${prefix}${record} - ${stripes.config.platformName}`));
});
});
60 changes: 30 additions & 30 deletions src/loginServices.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,11 +390,16 @@ function loadResources(okapiUrl, store, tenant, userId) {

/**
* spreadUserWithPerms
* return an object { user, perms } based on response from bl-users/self.
* Restructure the response from `bl-users/self?expandPermissions=true`
* to return an object shaped like
* {
* user: { id, username, ...personal }
* perms: { foo: true, bar: true, ... }
* }
*
* @param {object} userWithPerms
*
* @returns {object}
* @returns {object} { user, perms }
*/
export function spreadUserWithPerms(userWithPerms) {
const user = {
Expand All @@ -403,23 +408,16 @@ export function spreadUserWithPerms(userWithPerms) {
...userWithPerms?.user?.personal,
};

// remap data's array of permission-names to set with
// permission-names for keys and `true` for values.
//
// userWithPerms is shaped differently depending on whether
// it comes from a login call or a `.../_self` call, which
// is just totally totally awesome. :|
// we'll parse it differently depending on what it looks like.
let perms = {};
// remap userWithPerms.permissions.permissions from an array shaped like
// [{ "permissionName": "foo", ... }]
// to an object shaped like
// { foo: true, ...}
const perms = {};
const list = userWithPerms?.permissions?.permissions;
if (list && Array.isArray(list) && list.length > 0) {
// _self sends data like ["foo", "bar", "bat"]
// login sends data like [{ "permissionName": "foo" }]
if (typeof list[0] === 'string') {
perms = Object.assign({}, ...list.map(p => ({ [p]: true })));
} else {
perms = Object.assign({}, ...list.map(p => ({ [p.permissionName]: true })));
}
list.forEach(p => {
perms[p.permissionName] = true;
});
}

return { user, perms };
Expand Down Expand Up @@ -694,8 +692,8 @@ export function processOkapiSession(okapiUrl, store, tenant, resp, ssoToken) {
* @returns {Promise}
*/
export function validateUser(okapiUrl, store, tenant, session) {
const { token, user, perms, tenant: sessionTenant = tenant } = session;
return fetch(`${okapiUrl}/bl-users/_self`, {
const { token, tenant: sessionTenant = tenant } = session;
return fetch(`${okapiUrl}/bl-users/_self?expandPermissions=true`, {
headers: getHeaders(sessionTenant, token),
credentials: 'include',
mode: 'cors',
Expand All @@ -706,23 +704,25 @@ export function validateUser(okapiUrl, store, tenant, session) {
store.dispatch(setAuthError(null));
store.dispatch(setLoginData(data));

// If the request succeeded, we know the AT must be valid, but the
// response body from this endpoint doesn't include token-expiration
// data. So ... we set a near-future RT and an already-expired AT.
// On the next request, the expired AT will prompt an RTR cycle and
// we'll get real expiration values then.
const tokenExpiration = {
atExpires: -1,
rtExpires: Date.now() + (10 * 60 * 1000),
};

const { user, perms } = spreadUserWithPerms(data);
store.dispatch(setCurrentPerms(perms));

// update the session data with values from the response from _self
// in case they have changed since the previous login. this allows
// permissions changes to take effect immediately, without needing to
// re-authenticate.
//
// tenant and tokenExpiration data are still pulled from the session,
// tenant because the user may have switched the session-tenant to
// something other than their default and tokenExpiration because that
// data isn't provided by _self.
store.dispatch(setSessionData({
isAuthenticated: true,
user: data.user,
perms,
tenant: sessionTenant,
token,
tokenExpiration,
tokenExpiration: session.tokenExpiration
}));

return loadResources(okapiUrl, store, sessionTenant, user.id);
Expand Down
46 changes: 45 additions & 1 deletion src/loginServices.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
setIsAuthenticated,
setOkapiReady,
setServerDown,
// setSessionData,
setSessionData,
// setTokenExpiration,
setLoginData,
updateCurrentUser,
Expand Down Expand Up @@ -311,6 +311,50 @@ describe('validateUser', () => {
mockFetchCleanUp();
});

it('overwrites session data with new values from _self', async () => {
const store = {
dispatch: jest.fn(),
};

const tenant = 'tenant';
const sessionTenant = 'sessionTenant';
const data = {
user: {
id: 'ego',
username: 'superego',
},
permissions: {
permissions: [{ permissionName: 'ask' }, { permissionName: 'tell' }],
}
};

const session = {
user: { id: 'id', username: 'username' },
perms: { foo: true },
tenant: sessionTenant,
token: 'token',
};

mockFetchSuccess(data);

await validateUser('url', store, tenant, session);

const updatedSession = {
user: data.user,
isAuthenticated: true,
perms: { ask: true, tell: true },
tenant: session.tenant,
token: session.token,
};

expect(store.dispatch).toHaveBeenNthCalledWith(1, setAuthError(null));
expect(store.dispatch).toHaveBeenNthCalledWith(2, setLoginData(data));
expect(store.dispatch).toHaveBeenNthCalledWith(3, setCurrentPerms({ ask: true, tell: true }));
expect(store.dispatch).toHaveBeenNthCalledWith(4, setSessionData(updatedSession));

mockFetchCleanUp();
});

it('handles invalid user', async () => {
const store = {
dispatch: jest.fn(),
Expand Down

0 comments on commit 7befb96

Please sign in to comment.