From 240ae87cf96c69932a16b2aa2b045f043f292167 Mon Sep 17 00:00:00 2001 From: Ben Waples Date: Fri, 16 Aug 2024 12:50:41 -0700 Subject: [PATCH] Bed 4694: require auth for feature flags request (#788) * fix: enable feature flag request once user is fully authenticated * chore: formatting * fix: weird ts error from makeStyles type dec * refactor: remove unnecessary optional chaining --- cmd/ui/src/App.test.tsx | 57 +++++++++++++++++++++++++++- cmd/ui/src/App.tsx | 7 ++-- cmd/ui/src/ducks/auth/authSlice.ts | 6 +-- cmd/ui/src/hooks/useFeatureFlags.tsx | 3 +- 4 files changed, 64 insertions(+), 9 deletions(-) diff --git a/cmd/ui/src/App.test.tsx b/cmd/ui/src/App.test.tsx index e342469076..64cd92a901 100644 --- a/cmd/ui/src/App.test.tsx +++ b/cmd/ui/src/App.test.tsx @@ -16,8 +16,11 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import App from 'src/App'; -import { render, screen } from 'src/test-utils'; +import App, { Inner } from 'src/App'; +import { act, render, screen } from 'src/test-utils'; +import { DeepPartial, apiClient } from 'bh-shared-ui'; +import * as authSlice from 'src/ducks/auth/authSlice'; +import { AppState } from './store'; const server = setupServer( rest.get('/api/v2/saml/sso', (req, res, ctx) => { @@ -33,6 +36,20 @@ const server = setupServer( data: [], }) ); + }), + rest.get('/api/v2/available-domains', (req, res, ctx) => { + return res( + ctx.json({ + data: [], + }) + ); + }), + rest.get('/api/v2/self', (req, res, ctx) => { + return res( + ctx.json({ + data: [], + }) + ); }) ); @@ -48,4 +65,40 @@ describe('app', () => { render(); expect(await screen.findByText('LOGIN')).toBeInTheDocument(); }); + + describe('', () => { + const setup = async () => { + await act(async () => { + const initialState: DeepPartial = { + auth: { + isInitialized: true, + }, + }; + + render(, { initialState }); + }); + }; + + it('does not make feature-flag request if user is not fully authenticated', async () => { + const featureFlagSpy = vi.spyOn(apiClient, 'getFeatureFlags'); + const fullyAuthenticatedSelectorSpy = vi.spyOn(authSlice, 'fullyAuthenticatedSelector'); + // hard code user as not authenticated + fullyAuthenticatedSelectorSpy.mockReturnValue(false); + + await setup(); + + expect(featureFlagSpy).not.toHaveBeenCalled(); + }); + + it('will request feature-flags when the user is fully authenticated', async () => { + const featureFlagSpy = vi.spyOn(apiClient, 'getFeatureFlags'); + const fullyAuthenticatedSelectorSpy = vi.spyOn(authSlice, 'fullyAuthenticatedSelector'); + // hard code user as fully authenticated + fullyAuthenticatedSelectorSpy.mockReturnValue(true); + + await setup(); + + expect(featureFlagSpy).toHaveBeenCalled(); + }); + }); }); diff --git a/cmd/ui/src/App.tsx b/cmd/ui/src/App.tsx index 968e9796d3..0c42a47783 100644 --- a/cmd/ui/src/App.tsx +++ b/cmd/ui/src/App.tsx @@ -32,7 +32,7 @@ import { ErrorBoundary } from 'react-error-boundary'; import { useQueryClient } from 'react-query'; import { unstable_HistoryRouter as BrowserRouter, useLocation } from 'react-router-dom'; import Header from 'src/components/Header'; -import { initialize } from 'src/ducks/auth/authSlice'; +import { fullyAuthenticatedSelector, initialize } from 'src/ducks/auth/authSlice'; import { ROUTE_EXPIRED_PASSWORD, ROUTE_LOGIN, ROUTE_USER_DISABLED } from 'src/ducks/global/routes'; import { useFeatureFlags } from 'src/hooks/useFeatureFlags'; import { useAppDispatch, useAppSelector } from 'src/store'; @@ -41,12 +41,13 @@ import Content from 'src/views/Content'; import Notifier from './components/Notifier'; import { setDarkMode } from './ducks/global/actions'; -const Inner: React.FC = () => { +export const Inner: React.FC = () => { const dispatch = useAppDispatch(); const authState = useAppSelector((state) => state.auth); const queryClient = useQueryClient(); const location = useLocation(); - const featureFlagsRes = useFeatureFlags({ retry: false }); + const fullyAuthenticated = useAppSelector(fullyAuthenticatedSelector); + const featureFlagsRes = useFeatureFlags({ retry: false, enabled: !!authState.isInitialized && fullyAuthenticated }); const darkMode = useAppSelector((state) => state.global.view.darkMode); diff --git a/cmd/ui/src/ducks/auth/authSlice.ts b/cmd/ui/src/ducks/auth/authSlice.ts index 1df5295cb9..17ef5f7cad 100644 --- a/cmd/ui/src/ducks/auth/authSlice.ts +++ b/cmd/ui/src/ducks/auth/authSlice.ts @@ -80,7 +80,7 @@ export const login = createAsyncThunk( ); export const logout = createAsyncThunk('auth/logout', async () => { - return await apiClient.logout().catch(() => {}); + return await apiClient.logout().catch(() => { }); }); export const initialize = createAsyncThunk< @@ -228,12 +228,12 @@ export const authExpiredSelector = createSelector( export const fullyAuthenticatedSelector = createSelector( (state: AppState) => state.auth, (authState) => { - if (authState.user === null || authState.sessionToken === null || authState.isInitialized === false) { + if (!authState.user || !authState.sessionToken || authState.isInitialized === false) { return false; } const authExpired = - authState.user.AuthSecret !== null && + authState.user.AuthSecret?.expires_at && DateTime.fromISO(authState.user.AuthSecret.expires_at) < DateTime.local(); return !authExpired; diff --git a/cmd/ui/src/hooks/useFeatureFlags.tsx b/cmd/ui/src/hooks/useFeatureFlags.tsx index 6bcdd76869..48431d67ec 100644 --- a/cmd/ui/src/hooks/useFeatureFlags.tsx +++ b/cmd/ui/src/hooks/useFeatureFlags.tsx @@ -15,7 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 import { RequestOptions } from 'js-client-library'; -import { QueryOptions, UseQueryResult, useMutation, useQuery, useQueryClient } from 'react-query'; +import { UseQueryOptions, UseQueryResult, useMutation, useQuery, useQueryClient } from 'react-query'; import { apiClient } from 'bh-shared-ui'; export type Flag = { @@ -39,6 +39,7 @@ export const toggleFeatureFlag = (flagId: string | number, options?: RequestOpti return apiClient.toggleFeatureFlag(flagId, options).then((response) => response.data); }; +type QueryOptions = Omit, 'queryKey' | 'queryFn'>; export const useFeatureFlags = (queryOptions?: QueryOptions): UseQueryResult => useQuery(featureFlagKeys.all, ({ signal }) => getFeatureFlags({ signal }), queryOptions);