diff --git a/src/features/layout/LeftNavBar.module.scss b/src/features/layout/LeftNavBar.module.scss index a79a15eb..119b4c75 100644 --- a/src/features/layout/LeftNavBar.module.scss +++ b/src/features/layout/LeftNavBar.module.scss @@ -45,6 +45,13 @@ ul.nav_list { background: use-rgba-color("silver", 0.5); } + &.disabled, + &.disabled:hover { + background: use-color("white"); + color: use-color("base-light"); + cursor: not-allowed; + } + .nav_icon { font-size: 2rem; width: 100%; diff --git a/src/features/layout/LeftNavBar.test.tsx b/src/features/layout/LeftNavBar.test.tsx new file mode 100644 index 00000000..6f6f4a28 --- /dev/null +++ b/src/features/layout/LeftNavBar.test.tsx @@ -0,0 +1,64 @@ +import { render, screen } from '@testing-library/react'; +import fetchMock from 'jest-fetch-mock'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import { createTestStore } from '../../app/store'; +import { MfaStatus } from '../../common/types/auth'; +import { TokenInfo } from '../auth/authSlice'; +import LeftNavBar from './LeftNavBar'; + +const renderNav = (customroles: string[], mfa: MfaStatus) => { + fetchMock.mockResponse(async (req) => { + if (req.url.includes('/api/V2/me')) + return JSON.stringify({ user: 'someuser', roles: [], customroles }); + return JSON.stringify({ unseen: { global: 0, user: 0 } }); + }); + const store = createTestStore({ + auth: { + initialized: true, + token: 'sometoken', + username: 'someuser', + tokenInfo: { mfa } as TokenInfo, + }, + }); + render( + + + + + + ); +}; + +describe('LeftNavBar Lakehouse item', () => { + beforeEach(() => { + fetchMock.enableMocks(); + fetchMock.resetMocks(); + }); + afterEach(() => { + fetchMock.disableMocks(); + }); + + test('is absent without the BERDL_USER role', async () => { + renderNav([], 'Used'); + await screen.findByText('Navigator'); + expect(screen.queryByText('Lakehouse')).not.toBeInTheDocument(); + }); + + test('links to the hub redirect for an MFA log in session', async () => { + renderNav(['BERDL_USER'], 'Used'); + const item = await screen.findByText('Lakehouse'); + expect(item.closest('a')).toHaveAttribute('href', '/lakehouse/redirect'); + expect(screen.queryByText('alpha')).not.toBeInTheDocument(); + }); + + test.each(['NotUsed', 'Unknown'])( + 'is disabled for a %s log in session', + async (mfa) => { + renderNav(['BERDL_USER'], mfa); + const item = await screen.findByText('Lakehouse'); + expect(item.closest('a')).toBeNull(); + expect(item.closest('[aria-disabled="true"]')).not.toBeNull(); + } + ); +}); diff --git a/src/features/layout/LeftNavBar.tsx b/src/features/layout/LeftNavBar.tsx index f50c0923..12949145 100644 --- a/src/features/layout/LeftNavBar.tsx +++ b/src/features/layout/LeftNavBar.tsx @@ -23,12 +23,13 @@ import { getFeedsUnseenCount } from '../../common/api/feedsService'; import { useAppSelector } from '../../common/hooks'; import { authToken } from '../auth/authSlice'; import classes from './LeftNavBar.module.scss'; -import { Button, Menu, MenuItem } from '@mui/material'; +import { Button, Menu, MenuItem, Tooltip } from '@mui/material'; import { getMe } from '../../common/api/authService'; import { skipToken } from '@reduxjs/toolkit/dist/query'; const LeftNavBar: FC = () => { const token = useAppSelector(authToken); + const mfa = useAppSelector(({ auth }) => auth.tokenInfo?.mfa); const { data: feeds } = getFeedsUnseenCount.useQuery(undefined, { skip: !token, pollingInterval: 10000, @@ -70,9 +71,13 @@ const LeftNavBar: FC = () => { path={'/lakehouse/redirect'} desc="Lakehouse" icon={faDatabase} - badge={'alpha'} - badgeColor={'warning'} requiredRole="BERDL_USER" + // The hub admits only log in sessions that used a second factor. + disabledReason={ + mfa === 'Used' + ? undefined + : 'The Lakehouse requires two-factor authentication. Log out, then log in with ORCID using 2FA.' + } />