From 06d4cc9553995eb22c42352da92bb2785455ef43 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Wed, 20 Dec 2023 05:39:31 -0500 Subject: [PATCH 1/7] US1200527 show legacy or application-based discovery info Discovery needs to remain backwards compatible with Okapi APIs; ditto the `/settings/about` page that displays module and interface version information. There is not really as much work here as it appears. All the new components were split out of `About` in order to allow sub-sections to be reused with both application-based and module-based discovery information. Likewise, `loginServices.js` and `discoveryServices.js` were modestly refactored to handled both APIs. And there are Jest/RTL tests to replace the BTOG test that could not be easily updated to handle the new APIs since its `stripes-config` stub is part of `@folio/stripes-cli` instead of being declared locally. Refs US1200527 --- src/components/About/About.js | 253 ++++-------------- src/components/About/About.test.js | 115 ++++++++ src/components/About/AboutAPIGateway.js | 39 +++ src/components/About/AboutAPIGateway.test.js | 27 ++ .../About/AboutApplicationVersions.js | 43 +++ .../About/AboutApplicationVersions.test.js | 36 +++ src/components/About/AboutEnabledModules.js | 4 +- .../About/AboutEnabledModules.test.js | 39 +++ src/components/About/AboutInstallMessages.js | 20 +- src/components/About/AboutModules.js | 48 ++++ src/components/About/AboutModules.test.js | 24 ++ src/components/About/AboutOkapi.js | 91 +++++++ src/components/About/AboutOkapi.test.js | 76 ++++++ src/components/About/AboutStripes.js | 62 +++++ src/components/About/AboutStripes.test.js | 26 ++ src/components/About/AboutUIDependencies.js | 66 +++++ .../About/AboutUIDependencies.test.js | 54 ++++ src/components/About/AboutUIModuleDetails.js | 48 ++++ src/components/About/WarningBanner.test.js | 47 ++++ src/discoverServices.js | 75 +++++- src/loginServices.js | 41 ++- test/bigtest/tests/about-test.js | 41 --- test/jest/__mock__/intl.mock.js | 6 +- test/jest/__mock__/stripesComponents.mock.js | 5 + 24 files changed, 1011 insertions(+), 275 deletions(-) create mode 100644 src/components/About/About.test.js create mode 100644 src/components/About/AboutAPIGateway.js create mode 100644 src/components/About/AboutAPIGateway.test.js create mode 100644 src/components/About/AboutApplicationVersions.js create mode 100644 src/components/About/AboutApplicationVersions.test.js create mode 100644 src/components/About/AboutEnabledModules.test.js create mode 100644 src/components/About/AboutModules.js create mode 100644 src/components/About/AboutModules.test.js create mode 100644 src/components/About/AboutOkapi.js create mode 100644 src/components/About/AboutOkapi.test.js create mode 100644 src/components/About/AboutStripes.js create mode 100644 src/components/About/AboutStripes.test.js create mode 100644 src/components/About/AboutUIDependencies.js create mode 100644 src/components/About/AboutUIDependencies.test.js create mode 100644 src/components/About/AboutUIModuleDetails.js create mode 100644 src/components/About/WarningBanner.test.js delete mode 100644 test/bigtest/tests/about-test.js diff --git a/src/components/About/About.js b/src/components/About/About.js index 95c377acc..2835e8e20 100644 --- a/src/components/About/About.js +++ b/src/components/About/About.js @@ -1,26 +1,30 @@ -import _ from 'lodash'; import React, { useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; -import stripesConnect from '@folio/stripes-connect/package'; -import stripesComponents from '@folio/stripes-components/package'; -import stripesLogger from '@folio/stripes-logger/package'; - +import { okapi as okapiConfig } from 'stripes-config'; import { - Pane, Headline, - List, - Loading + Loading, + Pane, } from '@folio/stripes-components'; + import AboutInstallMessages from './AboutInstallMessages'; import WarningBanner from './WarningBanner'; import { withModules } from '../Modules'; -import stripesCore from '../../../package'; import css from './About.css'; +import { useStripes } from '../../StripesContext'; +import AboutOkapi from './AboutOkapi'; +import AboutApplicationVersions from './AboutApplicationVersions'; +import AboutStripes from './AboutStripes'; +import AboutAPIGateway from './AboutAPIGateway'; +import AboutUIDependencies from './AboutUIDependencies'; +import AboutUIModuleDetails from './AboutUIModuleDetails'; +import stripesCore from '../../../package'; const About = (props) => { const titleRef = useRef(null); const bannerRef = useRef(null); + const stripes = useStripes(); useEffect(() => { if (bannerRef.current) { @@ -30,83 +34,11 @@ const About = (props) => { } }, []); - function renderDependencies(m, interfaces) { - const base = `${m.module} ${m.version}`; - if (!interfaces) { - return base; - } - - const okapiInterfaces = m.okapiInterfaces; - if (!okapiInterfaces) { - return ; - } - - const itemFormatter = (key) => { - const text = okapiInterfaces[key]; - - return ( -
  • - {key} - {' '} - {text} -
  • - ); - }; - - return ( - - - - - - - ); - } - - function listModules(caption, list, interfaces) { - const itemFormatter = m => (
  • {renderDependencies(m, interfaces)}
  • ); - let headlineMsg; - switch (caption) { - case 'app': - headlineMsg = ; - break; - case 'settings': - headlineMsg = ; - break; - case 'plugin': - headlineMsg = ; - break; - default: - headlineMsg = ; - } - - list.sort(); - - return ( -
    - {headlineMsg} -
    - -
    -
    - ); - } - - const applications = - _.get(props.stripes, ['discovery', 'applications']) || {}; - const interfaces = _.get(props.stripes, ['discovery', 'interfaces']) || {}; - const isLoadingFinished = _.get(props.stripes, ['discovery', 'isFinished']); + const applications = stripes.discovery?.applications || {}; + const interfaces = stripes.discovery?.interfaces || {}; + const isLoadingFinished = stripes.discovery?.isFinished; const na = Object.keys(applications).length; - const unknownMsg = ; const numApplicationsMsg = ( { /> ); - const renderInterfaces = (list) => { - return ( -
  • {item.name}
  • } - /> - ); - }; - const renderModules = (list) => { - return ( - { - return ( -
  • - - {item.name} - - {renderInterfaces(item.interfaces)} -
  • - ); - }} - /> - ); - }; - return ( { bannerRef={bannerRef} /> )} - +
    -
    - - - - {numApplicationsMsg} - {Object.values(applications) - .map((app) => { - return ( -
      -
    • - {app.name} - {renderModules(app.modules)} -
    • -
    - ); - })} -
    - -
    - - - - - - - - (
  • {item.value}
  • )} - /> - -
    - - - - - - - (
  • {item}
  • )} - items={[ - , - , - - ]} - /> -
    - - - - - - - {renderDependencies(Object.assign({}, stripesCore.stripes || {}, { module: 'stripes-core' }), interfaces)} -
    - {Object.keys(props.modules).map(key => listModules(key, props.modules[key], interfaces))} -
    + {okapiConfig.tenantEntitlementUrl ? ( + <> +
    + +
    +
    + +
    + +
    + + + + + + + + +
    + + ) : ( + + )}
    ); @@ -251,14 +100,6 @@ const About = (props) => { About.propTypes = { modules: PropTypes.object, - stripes: PropTypes.shape({ - discovery: PropTypes.shape({ - modules: PropTypes.object, - interfaces: PropTypes.object, - }), - connect: PropTypes.func, - hasPerm: PropTypes.func.isRequired, - }).isRequired, }; export default withModules(About); diff --git a/src/components/About/About.test.js b/src/components/About/About.test.js new file mode 100644 index 000000000..2d36e5463 --- /dev/null +++ b/src/components/About/About.test.js @@ -0,0 +1,115 @@ +import { + QueryClient, + QueryClientProvider, +} from 'react-query'; +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; + +import AboutAPIGateway from './AboutAPIGateway'; +import AboutApplicationVersions from './AboutApplicationVersions'; +import AboutEnabledModules from './AboutEnabledModules'; +import AboutInstallMessages from './AboutInstallMessages'; +import AboutOkapi from './AboutOkapi'; +import AboutStripes from './AboutStripes'; +import AboutUIDependencies from './AboutUIDependencies'; +import AboutUIModuleDetails from './AboutUIModuleDetails'; +import WarningBanner from './WarningBanner'; + +import { okapi as okapiConfig } from 'stripes-config'; + +import { useStripes } from '../../StripesContext'; +import About from './About'; + +jest.mock('./AboutAPIGateway', () => () => 'AboutAPIGateway'); +jest.mock('./AboutApplicationVersions', () => () => 'AboutApplicationVersions'); +jest.mock('./AboutEnabledModules', () => () => 'AboutEnabledModules'); +jest.mock('./AboutInstallMessages', () => () => 'AboutInstallMessages'); +jest.mock('./AboutOkapi', () => () => 'AboutOkapi'); +jest.mock('./AboutStripes', () => () => 'AboutStripes'); +jest.mock('./AboutUIDependencies', () => () => 'AboutUIDependencies'); +jest.mock('./AboutUIModuleDetails', () => () => 'AboutUIModuleDetails'); +jest.mock('./WarningBanner', () => () => 'WarningBanner'); + +jest.mock('stripes-config', () => ({ + okapi: { tenantEntitlementUrl: true }, +})); + + +// set query retries to false. otherwise, react-query will thoughtfully +// (but unhelpfully, in the context of testing) retry a failed query +// several times causing the test to timeout when what we really want +// is for it to throw so we can catch and test the exception. +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false + } + } +}); + +jest.mock('../../StripesContext'); + +describe('About', () => { + it('displays application discovery details', async () => { + const modules = { + app: [ + { + module: 'app-alpha', + version: '1.2.3', + okapiInterfaces: { + iAlpha: '1.0', + } + }, + { module: 'app-beta', version: '2.3.4' } + ], + settings: [ + { module: 'settings-alpha', version: '3.4.5' }, + { module: 'settings-beta', version: '4.5.6' } + ], + plugin: [ + { module: 'plugin-alpha', version: '5.6.7' }, + { module: 'plugin-beta', version: '6.7.8' } + ], + typeThatHasNotBeenInventedYet: [ + { module: 'typeThatHasNotBeenInventedYet-alpha', version: '7.8.9' }, + { module: 'typeThatHasNotBeenInventedYet-beta', version: '8.9.10' } + ], + }; + + const stripes = { + okapi: { + tenant: 'barbie', + url: 'https://oppie.edu', + }, + discovery: { + modules, + interfaces: { + bar: '1.0', + bat: '2.0', + }, + isFinished: true, + } + }; + + const mockUseStripes = useStripes; + mockUseStripes.mockReturnValue(stripes); + + render( + + + + ); + + expect(screen.getByText(/WarningBanner/)).toBeInTheDocument(); + expect(screen.getByText(/AboutInstallMessages/)).toBeInTheDocument(); + expect(screen.getByText(/AboutApplicationVersions/)).toBeInTheDocument(); + expect(screen.getByText(/AboutStripes/)).toBeInTheDocument(); + expect(screen.getByText(/AboutAPIGateway/)).toBeInTheDocument(); + expect(screen.getByText(/about.uiOrServiceDependencies/)).toBeInTheDocument(); + expect(screen.getByText(/AboutUIModuleDetails/)).toBeInTheDocument(); + expect(screen.getByText(/AboutUIDependencies/)).toBeInTheDocument(); + expect(screen.queryByText(/AboutOkapi/)).toBe(null); + }); +}); diff --git a/src/components/About/AboutAPIGateway.js b/src/components/About/AboutAPIGateway.js new file mode 100644 index 000000000..676280f40 --- /dev/null +++ b/src/components/About/AboutAPIGateway.js @@ -0,0 +1,39 @@ +import _ from 'lodash'; +import { FormattedMessage } from 'react-intl'; + +import { + Headline, + List, +} from '@folio/stripes-components'; +import { useStripes } from '../../StripesContext'; + +/** + * AboutAPIGateway + * Display API gateway details including version, tenant, gateway URL + * @returns + */ +const AboutAPIGateway = () => { + const stripes = useStripes(); + const unknownMsg = ; + return ( + <> + + + + + + + (
  • {item}
  • )} + items={[ + , + , + + ]} + /> + + ); +}; + +export default AboutAPIGateway; diff --git a/src/components/About/AboutAPIGateway.test.js b/src/components/About/AboutAPIGateway.test.js new file mode 100644 index 000000000..f621b2b0c --- /dev/null +++ b/src/components/About/AboutAPIGateway.test.js @@ -0,0 +1,27 @@ +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; + +import { useStripes } from '../../StripesContext'; +import AboutAPIGateway from './AboutAPIGateway'; + +jest.mock('../../StripesContext'); + +describe('AboutAPIGateway', () => { + it('displays API gateway details', async () => { + const okapi = { + tenant: 'barbie', + url: 'https://oppie.com' + }; + + const mockUseStripes = useStripes; + mockUseStripes.mockReturnValue({ okapi }); + + render(); + + expect(screen.getByText(/about.version/)).toBeInTheDocument(); + expect(screen.getByText(/about.forTenant/)).toBeInTheDocument(); + expect(screen.getByText(/about.onUrl/)).toBeInTheDocument(); + }); +}); diff --git a/src/components/About/AboutApplicationVersions.js b/src/components/About/AboutApplicationVersions.js new file mode 100644 index 000000000..7de809407 --- /dev/null +++ b/src/components/About/AboutApplicationVersions.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { + Headline, +} from '@folio/stripes-components'; + +import css from './About.css'; +import AboutModules from './AboutModules'; + +/** + * AboutApplicationVersions + * Applications listed by discovery + * @param {*} param0 + * @returns + */ +const AboutApplicationVersions = ({ message, applications }) => { + return ( +
    + + + + {message} + {Object.values(applications) + .map((app) => { + return ( +
      +
    • + {app.name} + +
    • +
    + ); + })} +
    + ); +}; + +AboutApplicationVersions.propTypes = { + applications: PropTypes.object, + message: PropTypes.object, +}; + +export default AboutApplicationVersions; diff --git a/src/components/About/AboutApplicationVersions.test.js b/src/components/About/AboutApplicationVersions.test.js new file mode 100644 index 000000000..f30dd0d0b --- /dev/null +++ b/src/components/About/AboutApplicationVersions.test.js @@ -0,0 +1,36 @@ +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; +import { FormattedMessage } from 'react-intl'; + +import AboutApplicationVersions from './AboutApplicationVersions'; + +describe('AboutApplicationVersions', () => { + it('displays application version details', async () => { + const applications = { + a: { + name: 'Albus', + modules: [{ name: 'apple' }, { name: 'banana' }, { name: 'cherry' }] + }, + b: { + name: 'Beetlejuice', + modules: [{ name: 'alpha' }, { name: 'barvo' }, { name: 'charlie' }] + } + + }; + const id = 'All passing tests are alike; each failing test fails in its own way.'; + const message = ; + + render(); + + expect(screen.getByText(/about.applicationsVersionsTitle/)).toBeInTheDocument(); + expect(screen.getByText(id)).toBeInTheDocument(); + Object.keys(applications).forEach((i) => { + expect(screen.getByText(applications[i].name)).toBeInTheDocument(); + applications[i].modules.forEach((j) => { + expect(screen.getByText(j.name)).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/src/components/About/AboutEnabledModules.js b/src/components/About/AboutEnabledModules.js index 3df0bf0ae..e2d8cf3e3 100644 --- a/src/components/About/AboutEnabledModules.js +++ b/src/components/About/AboutEnabledModules.js @@ -3,6 +3,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { List } from '@folio/stripes-components'; +import stripesConnect from '../../stripesConnect'; + class AboutEnabledModules extends React.Component { static manifest = Object.freeze({ enabledModules: { @@ -50,4 +52,4 @@ class AboutEnabledModules extends React.Component { } } -export default AboutEnabledModules; +export default stripesConnect(AboutEnabledModules); diff --git a/src/components/About/AboutEnabledModules.test.js b/src/components/About/AboutEnabledModules.test.js new file mode 100644 index 000000000..2afe589ee --- /dev/null +++ b/src/components/About/AboutEnabledModules.test.js @@ -0,0 +1,39 @@ +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; + +import stripesConnect from '../../stripesConnect'; + +import AboutEnabledModules from './AboutEnabledModules'; + +jest.mock('../../stripesConnect', () => (Component) => Component); + +describe('AboutEnabledModules', () => { + it('displays application version details', async () => { + const availableModules = { + amy: 'angelo', + beth: 'baldwin', + camilla: 'claude' + }; + const resources = { + enabledModules: { + records: [ + { id: 'amy' }, + { id: 'beth' }, + ] + } + }; + const tenantid = 'monkey'; + + render(); + + Object.keys(availableModules).forEach((i) => { + expect(screen.getByText(availableModules[i])).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/About/AboutInstallMessages.js b/src/components/About/AboutInstallMessages.js index d808f8e53..cce8395f2 100644 --- a/src/components/About/AboutInstallMessages.js +++ b/src/components/About/AboutInstallMessages.js @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { FormattedDate } from 'react-intl'; import { @@ -9,6 +8,7 @@ import { useConfigurations, useOkapiEnv, } from '../../queries'; +import { useStripes } from '../../StripesContext'; export function entryFor(config, code) { @@ -75,20 +75,21 @@ export function installMessage(env, conf, stripesConf) { * @param {} props * @returns */ -const AboutInstallMessages = (props) => { +const AboutInstallMessages = () => { + const stripes = useStripes(); const aboutEnv = useOkapiEnv(); const aboutConfig = useConfigurations({ module: '@folio/stripes-core', configName: 'aboutInstall', }); - const version = installVersion(aboutEnv.data, aboutConfig.data, props.stripes.config); - const date = installDate(aboutEnv.data, aboutConfig.data, props.stripes.config); - const message = installMessage(aboutEnv.data, aboutConfig.data, props.stripes.config); + const version = installVersion(aboutEnv.data, aboutConfig.data, stripes.config); + const date = installDate(aboutEnv.data, aboutConfig.data, stripes.config); + const message = installMessage(aboutEnv.data, aboutConfig.data, stripes.config); let formattedDate = ''; if (date) { - formattedDate = <>(); + formattedDate = ; } return ( @@ -99,11 +100,4 @@ const AboutInstallMessages = (props) => { ); }; -AboutInstallMessages.propTypes = { - stripes: PropTypes.shape({ - config: PropTypes.object, - hasPerm: PropTypes.func.isRequired, - }).isRequired, -}; - export default AboutInstallMessages; diff --git a/src/components/About/AboutModules.js b/src/components/About/AboutModules.js new file mode 100644 index 000000000..5fc9ca7d5 --- /dev/null +++ b/src/components/About/AboutModules.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; +import { + Headline, + List, +} from '@folio/stripes-components'; + +import css from './About.css'; + + +const AboutInterfaces = ({ list }) => { + return ( +
  • {item.name}
  • } + /> + ); +}; + +AboutInterfaces.propTypes = { + list: PropTypes.arrayOf(PropTypes.object), +}; + +const AboutModules = ({ list }) => { + return ( + { + return ( +
  • + + {item.name} + + +
  • + ); + }} + /> + ); +}; + +AboutModules.propTypes = { + list: PropTypes.arrayOf(PropTypes.object), +}; + +export default AboutModules; diff --git a/src/components/About/AboutModules.test.js b/src/components/About/AboutModules.test.js new file mode 100644 index 000000000..cc070d640 --- /dev/null +++ b/src/components/About/AboutModules.test.js @@ -0,0 +1,24 @@ +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; + +import AboutModules from './AboutModules'; + +describe('AboutModules', () => { + it('displays application version details', async () => { + const list = [ + { name: 'Abigail', interfaces: [{ name: 'iAnnabeth' }, { name: 'iAlice' }] }, + { name: 'Betsy', interfaces: [{ name: 'iBelle' }, { name: 'iBrea' }] }, + ]; + + render(); + + Object.keys(list).forEach((i) => { + expect(screen.getByText(list[i].name)).toBeInTheDocument(); + list[i].interfaces.forEach((j) => { + expect(screen.getByText(j.name)).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/src/components/About/AboutOkapi.js b/src/components/About/AboutOkapi.js new file mode 100644 index 000000000..04b334b4a --- /dev/null +++ b/src/components/About/AboutOkapi.js @@ -0,0 +1,91 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { Headline, List } from '@folio/stripes-components'; + +import AboutAPIGateway from './AboutAPIGateway'; +import AboutStripes from './AboutStripes'; +import AboutUIModuleDetails from './AboutUIModuleDetails'; +import AboutUIDependencies from './AboutUIDependencies'; + +import { withModules } from '../Modules'; +import css from './About.css'; +import stripesCore from '../../../package'; +import { useStripes } from '../../StripesContext'; +import AboutEnabledModules from './AboutEnabledModules'; + +const AboutOkapi = ({ modules }) => { + const stripes = useStripes(); + + const dmodules = stripes.discovery.modules || {}; + const dinterfaces = stripes.discovery.interfaces || {}; + + const nm = Object.keys(dmodules).length; + const ni = Object.keys(dinterfaces).length; + + const unknownMsg = ; + const numModulesMsg = ; + const numInterfacesMsg = ; + + + return ( + <> +
    + + +
    + +
    + + + {numModulesMsg} + + + + + {chunks} + }} + /> +
    + {numInterfacesMsg} + ( +
  • + {`${key} ${dinterfaces[key]}`} +
  • + )} + /> +
    + +
    + + + + + + + + +
    + + ); +}; + +AboutOkapi.propTypes = { + modules: PropTypes.arrayOf(PropTypes.object), +}; + +export default withModules(AboutOkapi); diff --git a/src/components/About/AboutOkapi.test.js b/src/components/About/AboutOkapi.test.js new file mode 100644 index 000000000..989a20de0 --- /dev/null +++ b/src/components/About/AboutOkapi.test.js @@ -0,0 +1,76 @@ +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; + +import AboutOkapi from './AboutOkapi'; +import { useStripes } from '../../StripesContext'; +import stripesConnect from '../../stripesConnect'; +import AboutEnabledModules from './AboutEnabledModules'; + +jest.mock('../../StripesContext'); +jest.mock('../../stripesConnect'); +jest.mock('./AboutEnabledModules', () => () => <>); + +describe('AboutOkapi', () => { + const modules = { + app: [ + { + module: 'app-alpha', + version: '1.2.3', + okapiInterfaces: { + iAlpha: '1.0', + } + }, + { module: 'app-beta', version: '2.3.4' } + ], + settings: [ + { module: 'settings-alpha', version: '3.4.5' }, + { module: 'settings-beta', version: '4.5.6' } + ], + plugin: [ + { module: 'plugin-alpha', version: '5.6.7' }, + { module: 'plugin-beta', version: '6.7.8' } + ], + typeThatHasNotBeenInventedYet: [ + { module: 'typeThatHasNotBeenInventedYet-alpha', version: '7.8.9' }, + { module: 'typeThatHasNotBeenInventedYet-beta', version: '8.9.10' } + ], + }; + + const stripes = { + okapi: { + tenant: 'barbie', + url: 'https://oppie.edu', + }, + discovery: { + modules, + interfaces: { + bar: '1.0', + bat: '2.0', + } + } + }; + const mockUseStripes = useStripes; + mockUseStripes.mockReturnValue(stripes); + + it('displays application version details', async () => { + render(); + + expect(screen.getByText(/about.userInterface/)).toBeInTheDocument(); + expect(screen.getByText(/stripes-connect/)).toBeInTheDocument(); + expect(screen.getByText(/stripes-components/)).toBeInTheDocument(); + expect(screen.getByText(/stripes-logger/)).toBeInTheDocument(); + + expect(screen.getByText(/about.okapiServices/)).toBeInTheDocument(); + expect(screen.getByText(/about.version/)).toBeInTheDocument(); + expect(screen.getByText(/about.forTenant/)).toBeInTheDocument(); + expect(screen.getByText(/about.onUrl/)).toBeInTheDocument(); + + expect(screen.getByText(/about.moduleCount/)).toBeInTheDocument(); + expect(screen.getByText(/about.interfaceCount/)).toBeInTheDocument(); + + expect(screen.getByText(/about.uiOrServiceDependencies/)).toBeInTheDocument(); + // expect(screen.getByText(/about.moduleDependsOn/)).toBeInTheDocument(); + }); +}); diff --git a/src/components/About/AboutStripes.js b/src/components/About/AboutStripes.js new file mode 100644 index 000000000..b86a36443 --- /dev/null +++ b/src/components/About/AboutStripes.js @@ -0,0 +1,62 @@ +import _ from 'lodash'; +import { FormattedMessage } from 'react-intl'; +import stripesConnect from '@folio/stripes-connect/package'; +import stripesComponents from '@folio/stripes-components/package'; +import stripesLogger from '@folio/stripes-logger/package'; + +import { + Headline, + List, +} from '@folio/stripes-components'; +import stripesCore from '../../../package'; +import { useStripes } from '../../StripesContext'; + + +const AboutStripes = () => { + const stripes = useStripes(); + const unknownMsg = ; + const stripesModules = [ + { + key: 'stripes-core', + value: `stripes-core ${stripesCore.version}`, + }, + { + key: 'stripes-connect', + value: `stripes-connect ${stripesConnect.version}`, + }, + { + key: 'stripes-components', + value: `stripes-components ${stripesComponents.version}`, + }, + { + key: 'stripes-logger', + value: `stripes-logger ${stripesLogger.version}`, + }, + ]; + + return ( + <> + + + + + + + + (
  • {item.value}
  • )} + /> + + ); +}; + +export default AboutStripes; diff --git a/src/components/About/AboutStripes.test.js b/src/components/About/AboutStripes.test.js new file mode 100644 index 000000000..ebb614859 --- /dev/null +++ b/src/components/About/AboutStripes.test.js @@ -0,0 +1,26 @@ +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; + +import AboutStripes from './AboutStripes'; + +jest.mock('@folio/stripes-connect/package', () => ({ version: '1.2.3' })); +jest.mock('@folio/stripes-components/package', () => ({ version: '4.5.6' })); +jest.mock('@folio/stripes-logger/package', () => ({ version: '7.8.9' })); +jest.mock('../../../package', () => ({ version: '10.11.12' })); + +describe('AboutStripes', () => { + it('displays stripes-* version details', async () => { + render(); + + expect(screen.getByText(/about.userInterface/)).toBeInTheDocument(); + expect(screen.getByText(/about.foundation/)).toBeInTheDocument(); + + expect(screen.getByText(/stripes-core 10.11.12/)).toBeInTheDocument(); + expect(screen.getByText(/stripes-connect 1.2.3/)).toBeInTheDocument(); + expect(screen.getByText(/stripes-components 4.5.6/)).toBeInTheDocument(); + expect(screen.getByText(/stripes-logger 7.8.9/)).toBeInTheDocument(); + }); +}); + diff --git a/src/components/About/AboutUIDependencies.js b/src/components/About/AboutUIDependencies.js new file mode 100644 index 000000000..2a7bb51d9 --- /dev/null +++ b/src/components/About/AboutUIDependencies.js @@ -0,0 +1,66 @@ +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { + Headline, + List, +} from '@folio/stripes-components'; + +import AboutUIModuleDetails from './AboutUIModuleDetails'; + +/** + * AboutUIDependencies + * Display + * + * @param {object} modules { app[], settings[], plugin[], handler[] } + * array entries contain module details from discovery + * @param {bool} showDependencies true to display interface dependencies + * @returns + */ +const AboutUIDependencies = ({ modules, showDependencies }) => { + const itemFormatter = (m) => ( +
  • + +
  • + ); + + const headlineFor = (key, count) => { + let headlineMsg; + switch (key) { + case 'app': + headlineMsg = ; + break; + case 'settings': + headlineMsg = ; + break; + case 'plugin': + headlineMsg = ; + break; + default: + headlineMsg = ; + } + return headlineMsg; + }; + + return Object.keys(modules).map((key) => { + const items = modules[key].sort((a, b) => a.module.localeCompare(b.module)); + return ( +
    + {headlineFor(key, items.length)} +
    + +
    +
    + ); + }); +}; + +AboutUIDependencies.propTypes = { + modules: PropTypes.object, + showDependencies: PropTypes.bool, +}; + +export default AboutUIDependencies; diff --git a/src/components/About/AboutUIDependencies.test.js b/src/components/About/AboutUIDependencies.test.js new file mode 100644 index 000000000..90a8343f5 --- /dev/null +++ b/src/components/About/AboutUIDependencies.test.js @@ -0,0 +1,54 @@ +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; + +import AboutUIDependencies from './AboutUIDependencies'; + +describe('AboutUIDependencies', () => { + const modules = { + app: [ + { + module: 'app-alpha', + version: '1.2.3', + okapiInterfaces: { + iAlpha: '1.0', + } + }, + { module: 'app-beta', version: '2.3.4' } + ], + settings: [ + { module: 'settings-alpha', version: '3.4.5' }, + { module: 'settings-beta', version: '4.5.6' } + ], + plugin: [ + { module: 'plugin-alpha', version: '5.6.7' }, + { module: 'plugin-beta', version: '6.7.8' } + ], + typeThatHasNotBeenInventedYet: [ + { module: 'typeThatHasNotBeenInventedYet-alpha', version: '7.8.9' }, + { module: 'typeThatHasNotBeenInventedYet-beta', version: '8.9.10' } + ], + }; + + it('displays UI module details', async () => { + render(); + + expect(screen.queryByText(/about.noDependencies/)).toBe(null); + expect(screen.queryByText(/iAlpha/)).toBe(null); + + Object.keys(modules).forEach((i) => { + modules[i].forEach((j) => { + expect(screen.getByText(j.module, { exact: false })).toBeInTheDocument(); + expect(screen.getByText(j.version, { exact: false })).toBeInTheDocument(); + }); + }); + }); + + it('displays required interfaces', async () => { + render(); + + expect(screen.getAllByText(/about.noDependencies/).length).toBeGreaterThan(1); + expect(screen.getByText(/iAlpha/)).toBeInTheDocument(); + }); +}); diff --git a/src/components/About/AboutUIModuleDetails.js b/src/components/About/AboutUIModuleDetails.js new file mode 100644 index 000000000..2e3a69ec4 --- /dev/null +++ b/src/components/About/AboutUIModuleDetails.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import { + List, +} from '@folio/stripes-components'; + +/** + * AboutUIModuleDetails + * Given a UI module, display its name, version; optionally show the interfaces + * it module depends on. + * + * @param {object} module + * @param {array} showDependencies + * @returns + */ +const AboutUIModuleDetails = ({ module, showDependencies }) => { + const base = `${module.module} ${module.version}`; + + if (!showDependencies) { + return base; + } + + if (!module.okapiInterfaces) { + return ; + } + + const items = Object.keys(module.okapiInterfaces).sort().map( + (item) => `${item} ${module.okapiInterfaces[item]}` + ); + + return ( + <> + + + + ); +}; + +AboutUIModuleDetails.propTypes = { + module: PropTypes.object, + showDependencies: PropTypes.bool, +}; + +export default AboutUIModuleDetails; diff --git a/src/components/About/WarningBanner.test.js b/src/components/About/WarningBanner.test.js new file mode 100644 index 000000000..3ae1e270e --- /dev/null +++ b/src/components/About/WarningBanner.test.js @@ -0,0 +1,47 @@ +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; + +import WarningBanner from './WarningBanner'; + +describe('WarningBanner', () => { + const modules = { + app: [ + { + module: 'app-alpha', + version: '1.2.3', + okapiInterfaces: { + alpha: '1.0', + beta: '2.0', + gamma: '3.1', + } + }, + { module: 'app-beta', version: '2.3.4' } + ], + }; + + it('displays missing interfaces', async () => { + const interfaces = { + alpha: '1.0', + beta: '2.0', + }; + + render(); + + expect(screen.getByText(/about.missingModuleCount/)).toBeInTheDocument(); + expect(screen.getByText(/gamma/)).toBeInTheDocument(); + }); + + it('displays incompatible interfaces', async () => { + const interfaces = { + alpha: '1.0', + beta: '2.0', + gamma: '3.0', + }; + render(); + + expect(screen.getByText(/about.incompatibleModuleCount/)).toBeInTheDocument(); + expect(screen.getByText(/gamma/)).toBeInTheDocument(); + }); +}); diff --git a/src/discoverServices.js b/src/discoverServices.js index 987883f9f..02c66ddfb 100644 --- a/src/discoverServices.js +++ b/src/discoverServices.js @@ -3,9 +3,9 @@ import { okapi as okapiConfig } from 'stripes-config'; function getHeaders(tenant, token) { return { - 'X-Okapi-Token': token, 'X-Okapi-Tenant': tenant, 'Content-Type': 'application/json', + ...(token && { 'X-Okapi-Token': token }), }; } @@ -99,7 +99,9 @@ function fetchApplicationDetails(store) { const okapi = store.getState().okapi; return fetch(`${okapi.url}/entitlements/${okapi.tenant}/applications?limit=${APP_MAX_COUNT}`, { - headers: getHeaders(okapi.tenant, okapi.token) + credentials: 'include', + headers: getHeaders(okapi.tenant, okapi.token), + mode: 'cors', }) .then((response) => { if (response.ok) { @@ -144,7 +146,9 @@ function fetchGatewayVersion(store) { const okapi = store.getState().okapi; return fetch(`${okapi.url}/version`, { - headers: getHeaders(okapi.tenant, okapi.token) + credentials: 'include', + headers: getHeaders(okapi.tenant, okapi.token), + mode: 'cors', }).then((response) => { // eslint-disable-line consistent-return if (response.status >= 400) { store.dispatch({ type: 'DISCOVERY_FAILURE', code: response.status }); @@ -159,14 +163,77 @@ function fetchGatewayVersion(store) { }); } +function fetchOkapiVersion(store) { + const okapi = store.getState().okapi; + + return fetch(`${okapi.url}/_/version`, { + credentials: 'include', + headers: getHeaders(okapi.tenant, okapi.token), + mode: 'cors', + }).then((response) => { // eslint-disable-line consistent-return + if (response.status >= 400) { + store.dispatch({ type: 'DISCOVERY_FAILURE', code: response.status }); + return response; + } else { + return response.text().then((text) => { + store.dispatch({ type: 'DISCOVERY_OKAPI', version: text }); + }); + } + }).catch((reason) => { + store.dispatch({ type: 'DISCOVERY_FAILURE', message: reason }); + }); +} + +function fetchModules(store) { + const okapi = store.getState().okapi; + + return fetch(`${okapi.url}/_/proxy/tenants/${okapi.tenant}/modules?full=true`, { + credentials: 'include', + headers: getHeaders(okapi.tenant, okapi.token), + mode: 'cors', + }).then((response) => { // eslint-disable-line consistent-return + if (response.status >= 400) { + store.dispatch({ type: 'DISCOVERY_FAILURE', code: response.status }); + return response; + } else { + return response.json().then((json) => { + store.dispatch({ type: 'DISCOVERY_SUCCESS', data: json }); + return Promise.all( + json.map(entry => Promise.all([ + store.dispatch({ type: 'DISCOVERY_INTERFACES', data: entry }), + store.dispatch({ type: 'DISCOVERY_PROVIDERS', data: entry }), + ])) + ); + }); + } + }).catch((reason) => { + store.dispatch({ type: 'DISCOVERY_FAILURE', message: reason }); + }); +} + +/* + * This function probes Okapi to discover what versions of what + * interfaces are supported by the services that it is proxying + * for. This information can be used to configure the UI at run-time + * (e.g. not attempting to fetch loan information for a + * non-circulating library that doesn't provide the circ interface) + */ export function discoverServices(store) { - const promises = [fetchApplicationDetails(store), fetchGatewayVersion(store)]; + const promises = []; + if (okapiConfig.tenantEntitlementUrl) { + promises.push(fetchApplicationDetails(store)); + promises.push(fetchGatewayVersion(store)); + } else { + promises.push(fetchOkapiVersion(store)); + promises.push(fetchModules(store)); + } return Promise.all(promises).then(() => { store.dispatch({ type: 'DISCOVERY_FINISHED' }); }); } + export function discoveryReducer(state = {}, action) { switch (action.type) { case 'DISCOVERY_APPLICATIONS': diff --git a/src/loginServices.js b/src/loginServices.js index 4fdf8e0af..a3e3c5600 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -73,8 +73,8 @@ export const userLocaleConfig = { function getHeaders(tenant, token) { return { 'X-Okapi-Tenant': tenant, - 'X-Okapi-Token': token, 'Content-Type': 'application/json', + ...(token && { 'X-Okapi-Token': token }), }; } @@ -166,7 +166,11 @@ export function loadTranslations(store, locale, defaultTranslations = {}) { */ function dispatchLocale(url, store, tenant) { return fetch(url, - { headers: getHeaders(tenant, store.getState().okapi.token) }) + { + credentials: 'include', + headers: getHeaders(tenant, store.getState().okapi.token), + mode: 'cors', + }) .then((response) => { if (response.status === 200) { response.json().then((json) => { @@ -242,7 +246,11 @@ export function getUserLocale(okapiUrl, store, tenant, userId) { */ export function getPlugins(okapiUrl, store, tenant) { return fetch(`${okapiUrl}/configurations/entries?query=(module==PLUGINS)`, - { headers: getHeaders(tenant, store.getState().okapi.token) }) + { + credentials: 'include', + headers: getHeaders(tenant, store.getState().okapi.token), + mode: 'cors', + }) .then((response) => { if (response.status < 400) { response.json().then((json) => { @@ -268,7 +276,11 @@ export function getPlugins(okapiUrl, store, tenant) { */ export function getBindings(okapiUrl, store, tenant) { return fetch(`${okapiUrl}/configurations/entries?query=(module==ORG and configName==bindings)`, - { headers: getHeaders(tenant, store.getState().okapi.token) }) + { + credentials: 'include', + headers: getHeaders(tenant, store.getState().okapi.token), + mode: 'cors', + }) .then((response) => { let bindings = {}; if (response.status >= 400) { @@ -414,7 +426,12 @@ export function validateUser(okapiUrl, store, tenant, session) { const { token, user, perms, tenant: sessionTenant = tenant } = session; const usersPath = okapi.authnUrl ? 'users-keycloak' : 'bl-users'; - return fetch(`${okapiUrl}/${usersPath}/_self`, { headers: getHeaders(sessionTenant, token) }).then((resp) => { + return fetch(`${okapiUrl}/${usersPath}/_self`, + { + credentials: 'include', + headers: getHeaders(sessionTenant, token), + mode: 'cors', + }).then((resp) => { if (resp.ok) { return resp.json().then((data) => { store.dispatch(setLoginData(data)); @@ -543,7 +560,7 @@ export function processOkapiSession(store, tenant, resp, ssoToken) { if (resp.ok) { return resp.json() .then(json => { - const token = json.access_token || ssoToken; + const token = resp.headers.get('X-Okapi-Token') || json.access_token || ssoToken; return createOkapiSession(store, tenant, token, json) .then(() => json); }) @@ -606,9 +623,11 @@ export function requestLogin(_junk, store, tenant, data) { } else { // legacy built-in authentication return fetch(`${okapi.url}/bl-users/login?expandPermissions=true&fullPermissions=true`, { - method: 'POST', - headers: { 'X-Okapi-Tenant': tenant, 'Content-Type': 'application/json' }, 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)); } @@ -627,7 +646,11 @@ function fetchUserWithPerms(okapiUrl, tenant, token) { const usersPath = okapi.authnUrl ? 'users-keycloak' : 'bl-users'; return fetch( `${okapiUrl}/${usersPath}/_self?expandPermissions=true&fullPermissions=true`, - { headers: getHeaders(tenant, token) }, + { + credentials: 'include', + headers: getHeaders(tenant, token), + mode: 'cors', + }, ); } diff --git a/test/bigtest/tests/about-test.js b/test/bigtest/tests/about-test.js deleted file mode 100644 index d791eb428..000000000 --- a/test/bigtest/tests/about-test.js +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, beforeEach, it } from '@bigtest/mocha'; -import { expect } from 'chai'; - -import React, { Component } from 'react'; - -import setupApplication from '../helpers/setup-core-application'; -import AboutInteractor from '../interactors/about'; - -describe('About', () => { - const about = new AboutInteractor(); - - class DummyApp extends Component { - render() { - return (

    Hello Stripes!

    ); - } - } - - setupApplication({ - modules: [{ - type: 'app', - name: '@folio/ui-dummy', - displayName: 'dummy.title', - route: '/dummy', - hasSettings: true, - module: DummyApp - }], - translations: { - 'dummy.title': 'Dummy' - } - }); - - describe('viewing the about page', () => { - beforeEach(function () { - this.visit('/settings/about'); - }); - - it('has one installed app', function () { - expect(about.installedApps()).to.have.lengthOf(1); - }); - }); -}); diff --git a/test/jest/__mock__/intl.mock.js b/test/jest/__mock__/intl.mock.js index b42572d27..6ac92cebb 100644 --- a/test/jest/__mock__/intl.mock.js +++ b/test/jest/__mock__/intl.mock.js @@ -7,11 +7,15 @@ jest.mock('react-intl', () => { return { ...jest.requireActual('react-intl'), - FormattedMessage: jest.fn(({ id, children }) => { + FormattedMessage: jest.fn(({ id, values, children }) => { if (children) { return children([id]); } + if (values) { + return `${id} ${Object.keys(values).map(key => `${key}:${values[key]}`).join(', ')}`; + } + return id; }), FormattedTime: jest.fn(({ value, children }) => { diff --git a/test/jest/__mock__/stripesComponents.mock.js b/test/jest/__mock__/stripesComponents.mock.js index 96900ed51..938544834 100644 --- a/test/jest/__mock__/stripesComponents.mock.js +++ b/test/jest/__mock__/stripesComponents.mock.js @@ -67,6 +67,11 @@ jest.mock('@folio/stripes-components', () => ({ Label: jest.fn(({ children, ...rest }) => ( {children} )), + List: jest.fn(({ items, itemFormatter = (item, i) => (
  • {item}
  • )}) => ( +
      + [{items?.map((item, i) => itemFormatter(item, i))}] +
    + )), Loading: () =>
    Loading
    , MessageBanner: jest.fn(({ show, children }) => { return show ? <>{children} : <>; }), From df132af0aec08d837d988cbf41a7576a10fded9c Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Wed, 20 Dec 2023 12:35:43 -0500 Subject: [PATCH 2/7] handle legacy logout (no external redirect) Keycloak-based logout redirects to an external URL. This commit restores support for legacy logout, which is an internal redirect to `/`. --- src/RootWithIntl.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/RootWithIntl.js b/src/RootWithIntl.js index affc76c35..a18a6c106 100644 --- a/src/RootWithIntl.js +++ b/src/RootWithIntl.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Router, Switch, + Redirect as InternalRedirect } from 'react-router-dom'; import { Provider } from 'react-redux'; @@ -84,6 +85,16 @@ class RootWithIntl extends React.Component { return `${okapi.authnUrl}/realms/${okapi.tenant}/protocol/openid-connect/auth?client_id=${okapi.clientId}&response_type=code&redirect_uri=${redirectUri}&scope=openid`; } + renderLogoutComponent() { + const { okapi } = this.props.stripes; + + if (okapi.authnUrl) { + return ; + } + + return ; + } + renderLoginComponent() { const { config, okapi } = this.props.stripes; @@ -112,7 +123,6 @@ class RootWithIntl extends React.Component { const connect = connectFor('@folio/core', this.props.stripes.epics, this.props.stripes.logger); const stripes = this.props.stripes.clone({ connect }); - const logoutUrl = `${stripes.okapi.authnUrl}/realms/${stripes.okapi.tenant}/protocol/openid-connect/logout?client_id=${stripes.okapi.clientId}&post_logout_redirect_uri=${window.location.protocol}//${window.location.host}`; const LoginComponent = stripes.okapi.authnUrl ? } + component={this.renderLogoutComponent()} /> Date: Wed, 20 Dec 2023 12:45:58 -0500 Subject: [PATCH 3/7] correct proptypes cuz it's always proptypes. I mean, if it's not lint. except when it's tests. or ally. or i18n. --- src/components/About/AboutOkapi.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/About/AboutOkapi.js b/src/components/About/AboutOkapi.js index 04b334b4a..cb6589a98 100644 --- a/src/components/About/AboutOkapi.js +++ b/src/components/About/AboutOkapi.js @@ -85,7 +85,12 @@ const AboutOkapi = ({ modules }) => { }; AboutOkapi.propTypes = { - modules: PropTypes.arrayOf(PropTypes.object), + modules: PropTypes.shape({ + app: PropTypes.arrayOf(PropTypes.object), + plugin: PropTypes.arrayOf(PropTypes.object), + settings: PropTypes.arrayOf(PropTypes.object), + handler: PropTypes.arrayOf(PropTypes.object), + }), }; export default withModules(AboutOkapi); From ce5fce82f52ee309e8bab864957ce7d470c2f260 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Wed, 20 Dec 2023 12:56:31 -0500 Subject: [PATCH 4/7] be deliberate about sort, even though the default (UTF-16 string compare) is exactly what I want SonarDad :eyeroll: --- src/components/About/AboutOkapi.js | 2 +- src/components/About/AboutUIModuleDetails.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/About/AboutOkapi.js b/src/components/About/AboutOkapi.js index cb6589a98..fd38a8657 100644 --- a/src/components/About/AboutOkapi.js +++ b/src/components/About/AboutOkapi.js @@ -54,7 +54,7 @@ const AboutOkapi = ({ modules }) => { {numInterfacesMsg} a.localeCompare(b))} itemFormatter={key => (
  • {`${key} ${dinterfaces[key]}`} diff --git a/src/components/About/AboutUIModuleDetails.js b/src/components/About/AboutUIModuleDetails.js index 2e3a69ec4..cff2965fc 100644 --- a/src/components/About/AboutUIModuleDetails.js +++ b/src/components/About/AboutUIModuleDetails.js @@ -25,9 +25,10 @@ const AboutUIModuleDetails = ({ module, showDependencies }) => { return ; } - const items = Object.keys(module.okapiInterfaces).sort().map( - (item) => `${item} ${module.okapiInterfaces[item]}` - ); + const items = Object + .keys(module.okapiInterfaces) + .sort((a, b) => a.localeCompare(b)) + .map((item) => `${item} ${module.okapiInterfaces[item]}`); return ( <> From c448589a6e297b02ddd8a28a3b1f4cbbe75794e5 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Wed, 20 Dec 2023 15:47:05 -0500 Subject: [PATCH 5/7] lint --- src/components/About/About.test.js | 8 +++++--- src/components/About/AboutEnabledModules.test.js | 4 +++- src/components/About/AboutOkapi.test.js | 3 +++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/About/About.test.js b/src/components/About/About.test.js index 2d36e5463..e50950ba6 100644 --- a/src/components/About/About.test.js +++ b/src/components/About/About.test.js @@ -1,3 +1,6 @@ +/* shhhh, eslint, it's ok. we need "unused" imports for mocks */ +/* eslint-disable no-unused-vars */ + import { QueryClient, QueryClientProvider, @@ -7,6 +10,8 @@ import { screen, } from '@folio/jest-config-stripes/testing-library/react'; +import { okapi as okapiConfig } from 'stripes-config'; + import AboutAPIGateway from './AboutAPIGateway'; import AboutApplicationVersions from './AboutApplicationVersions'; import AboutEnabledModules from './AboutEnabledModules'; @@ -17,8 +22,6 @@ import AboutUIDependencies from './AboutUIDependencies'; import AboutUIModuleDetails from './AboutUIModuleDetails'; import WarningBanner from './WarningBanner'; -import { okapi as okapiConfig } from 'stripes-config'; - import { useStripes } from '../../StripesContext'; import About from './About'; @@ -36,7 +39,6 @@ jest.mock('stripes-config', () => ({ okapi: { tenantEntitlementUrl: true }, })); - // set query retries to false. otherwise, react-query will thoughtfully // (but unhelpfully, in the context of testing) retry a failed query // several times causing the test to timeout when what we really want diff --git a/src/components/About/AboutEnabledModules.test.js b/src/components/About/AboutEnabledModules.test.js index 2afe589ee..4c4392960 100644 --- a/src/components/About/AboutEnabledModules.test.js +++ b/src/components/About/AboutEnabledModules.test.js @@ -1,10 +1,12 @@ +/* shhhh, eslint, it's ok. we need "unused" imports for mocks */ +/* eslint-disable no-unused-vars */ + import { render, screen, } from '@folio/jest-config-stripes/testing-library/react'; import stripesConnect from '../../stripesConnect'; - import AboutEnabledModules from './AboutEnabledModules'; jest.mock('../../stripesConnect', () => (Component) => Component); diff --git a/src/components/About/AboutOkapi.test.js b/src/components/About/AboutOkapi.test.js index 989a20de0..2165b8ee5 100644 --- a/src/components/About/AboutOkapi.test.js +++ b/src/components/About/AboutOkapi.test.js @@ -1,3 +1,6 @@ +/* shhhh, eslint, it's ok. we need "unused" imports for mocks */ +/* eslint-disable no-unused-vars */ + import { render, screen, From 6c939d5cc8d2d0318495279847bfba2da6cdebff Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Wed, 20 Dec 2023 15:47:27 -0500 Subject: [PATCH 6/7] externalize functions for easier testing --- src/RootWithIntl.js | 91 ++++++++++++++++------------------------ src/RootWithIntl.test.js | 74 ++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 54 deletions(-) create mode 100644 src/RootWithIntl.test.js diff --git a/src/RootWithIntl.js b/src/RootWithIntl.js index a18a6c106..cbd0c7d84 100644 --- a/src/RootWithIntl.js +++ b/src/RootWithIntl.js @@ -44,14 +44,47 @@ import { CalloutContext } from './CalloutContext'; import PreLoginLanding from './components/PreLoginLanding'; import { setOkapiTenant } from './okapiActions'; +export const renderLogoutComponent = (stripes) => { + const { okapi } = stripes; + + if (okapi.authnUrl) { + return ; + } + + return ; +}; + +export const renderLoginComponent = (stripes) => { + const { config, okapi } = stripes; + + if (okapi.authnUrl) { + if (config.isSingleTenant) { + const redirectUri = `${window.location.protocol}//${window.location.host}/oidc-landing`; + const authnUri = `${okapi.authnUrl}/realms/${okapi.tenant}/protocol/openid-connect/auth?client_id=${okapi.clientId}&response_type=code&redirect_uri=${redirectUri}&scope=openid`; + return ; + } + + const handleSelectTenant = (tenant, clientId) => { + localStorage.setItem('tenant', JSON.stringify({ tenantName: tenant, clientId })); + stripes.store.dispatch(setOkapiTenant({ tenant, clientId })); + }; + + return ; + } + + return ; +}; + class RootWithIntl extends React.Component { static propTypes = { stripes: PropTypes.shape({ + clone: PropTypes.func.isRequired, config: PropTypes.object, epics: PropTypes.object, logger: PropTypes.object.isRequired, - clone: PropTypes.func.isRequired, - config: PropTypes.object.isRequired, okapi: PropTypes.object.isRequired, store: PropTypes.object.isRequired }).isRequired, @@ -67,52 +100,12 @@ class RootWithIntl extends React.Component { state = { callout: null }; - handleSelectTenant = (tenant, clientId) => { - localStorage.setItem('tenant', JSON.stringify({ tenantName: tenant, clientId })); - this.props.stripes.store.dispatch(setOkapiTenant({ clientId, tenant })); - } - setCalloutRef = (ref) => { this.setState({ callout: ref, }); } - singleTenantAuthnUrl = () => { - const { okapi } = this.props.stripes; - const redirectUri = `${window.location.protocol}//${window.location.host}/oidc-landing`; - - return `${okapi.authnUrl}/realms/${okapi.tenant}/protocol/openid-connect/auth?client_id=${okapi.clientId}&response_type=code&redirect_uri=${redirectUri}&scope=openid`; - } - - renderLogoutComponent() { - const { okapi } = this.props.stripes; - - if (okapi.authnUrl) { - return ; - } - - return ; - } - - renderLoginComponent() { - const { config, okapi } = this.props.stripes; - - if (okapi.authnUrl) { - if (config.isSingleTenant) { - return ; - } - return ; - } - - return ; - } - render() { const { token, @@ -123,16 +116,6 @@ class RootWithIntl extends React.Component { const connect = connectFor('@folio/core', this.props.stripes.epics, this.props.stripes.logger); const stripes = this.props.stripes.clone({ connect }); - const LoginComponent = stripes.okapi.authnUrl ? - - : - ; - return ( @@ -228,11 +211,11 @@ class RootWithIntl extends React.Component { } diff --git a/src/RootWithIntl.test.js b/src/RootWithIntl.test.js new file mode 100644 index 000000000..1a3c7a0b3 --- /dev/null +++ b/src/RootWithIntl.test.js @@ -0,0 +1,74 @@ +/* shhhh, eslint, it's ok. we need "unused" imports for mocks */ +/* eslint-disable no-unused-vars */ + +import { render, screen } from '@folio/jest-config-stripes/testing-library/react'; + +import { Redirect as InternalRedirect } from 'react-router-dom'; +import Redirect from './components/Redirect'; +import { Login } from './components'; +import PreLoginLanding from './components/PreLoginLanding'; + +import { + renderLoginComponent, + renderLogoutComponent +} from './RootWithIntl'; + +jest.mock('react-router-dom', () => ({ + Redirect: () => '', + withRouter: (Component) => Component, +})); +jest.mock('./components/Redirect', () => () => ''); +jest.mock('./components/Login', () => () => ''); +jest.mock('./components/PreLoginLanding', () => () => ''); + +describe('RootWithIntl', () => { + describe('renderLoginComponent', () => { + it('handles legacy login', () => { + const stripes = { okapi: {}, config: {} }; + render(renderLoginComponent(stripes)); + + expect(screen.getByText(//)).toBeInTheDocument(); + }); + + describe('handles third-party login', () => { + it('handles single-tenant', () => { + const stripes = { + okapi: { authnUrl: 'https://barbie.com' }, + config: { isSingleTenant: true } + }; + render(renderLoginComponent(stripes)); + + expect(screen.getByText(//)).toBeInTheDocument(); + }); + + it('handles multi-tenant', () => { + const stripes = { + okapi: { authnUrl: 'https://oppie.com' }, + config: { }, + }; + render(renderLoginComponent(stripes)); + + expect(screen.getByText(//)).toBeInTheDocument(); + }); + }); + }); + + describe('renderLogoutComponent', () => { + it('handles legacy logout', () => { + const stripes = { okapi: {}, config: {} }; + render(renderLogoutComponent(stripes)); + + expect(screen.getByText(//)).toBeInTheDocument(); + }); + + it('handles third-party logout', () => { + const stripes = { + okapi: { authnUrl: 'https://oppie.com' }, + config: { }, + }; + render(renderLogoutComponent(stripes)); + + expect(screen.getByText(//)).toBeInTheDocument(); + }); + }); +}); From 343fff6454f689fb79970ec2764eb4faca92477f Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Wed, 20 Dec 2023 16:24:33 -0500 Subject: [PATCH 7/7] more delicious lint. mmm mmm tasty. --- test/jest/__mock__/stripesComponents.mock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jest/__mock__/stripesComponents.mock.js b/test/jest/__mock__/stripesComponents.mock.js index 938544834..f775a48bc 100644 --- a/test/jest/__mock__/stripesComponents.mock.js +++ b/test/jest/__mock__/stripesComponents.mock.js @@ -67,7 +67,7 @@ jest.mock('@folio/stripes-components', () => ({ Label: jest.fn(({ children, ...rest }) => ( {children} )), - List: jest.fn(({ items, itemFormatter = (item, i) => (
  • {item}
  • )}) => ( + List: jest.fn(({ items, itemFormatter = (item, i) => (
  • {item}
  • ) }) => (
      [{items?.map((item, i) => itemFormatter(item, i))}]