From 06b33b5f9e9cf0eeaa124881b24e0a87d7a26eab Mon Sep 17 00:00:00 2001 From: Clement Mwimo Date: Thu, 28 May 2026 15:35:55 -0600 Subject: [PATCH 1/3] =?UTF-8?q?Revert=20"fix:=20revert=20show=20demo=20con?= =?UTF-8?q?nect=20guard=20for=20demo=20users=20launching=20widgets=20?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit c158c7613dd8fc969a84d799dceb2f160613e688. --- src/__tests__/Connect-test.tsx | 69 ++++++++++++ src/hooks/useLoadConnect.tsx | 8 +- src/redux/actions/Connect.js | 12 ++ src/redux/reducers/Connect.js | 12 ++ src/redux/reducers/__tests__/Connect-test.js | 109 +++++++++++++++---- 5 files changed, 182 insertions(+), 28 deletions(-) create mode 100644 src/__tests__/Connect-test.tsx diff --git a/src/__tests__/Connect-test.tsx b/src/__tests__/Connect-test.tsx new file mode 100644 index 0000000000..d598887759 --- /dev/null +++ b/src/__tests__/Connect-test.tsx @@ -0,0 +1,69 @@ +import React from 'react' +import { describe, it, expect, vi } from 'vitest' +import { screen } from '@testing-library/react' + +import { Connect } from '../Connect' +import { render } from 'src/utilities/testingLibrary' +import { apiValue as apiValueMock } from 'src/const/apiProviderMock' +import { masterData, institutionData } from 'src/services/mockedData' + +describe('Connect - Demo Connect Guard', () => { + const defaultProps = { + clientConfig: { current_institution_guid: 'INS-123' } as ClientConfigType, + onShowConnectSuccessSurvey: () => undefined, + onSubmitConnectSuccessSurvey: () => {}, + profiles: { ...masterData, loading: false }, + } + + const nonDemoInstitution = { ...institutionData.institution, is_demo: false } + const demoInstitution = { ...institutionData.institution, is_demo: true } + const demoUser = { ...masterData.user, is_demo: true } + const regularUser = { ...masterData.user, is_demo: false } + + it('blocks demo user from accessing non-demo institution', async () => { + const mockApiValue = { + ...apiValueMock, + loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution), + loadMembers: vi.fn().mockResolvedValue([]), + } + + render( + , + { apiValue: mockApiValue }, + ) + + expect(await screen.findByText(/Demo mode active/i)).toBeInTheDocument() + }) + + it('allows demo user to access demo institution', async () => { + const mockApiValue = { + ...apiValueMock, + loadInstitutionByGuid: vi.fn().mockResolvedValue(demoInstitution), + loadMembers: vi.fn().mockResolvedValue([]), + } + + render( + , + { apiValue: mockApiValue }, + ) + + expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() + expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() + }) + + it('allows regular user to access non-demo institution', async () => { + const mockApiValue = { + ...apiValueMock, + loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution), + loadMembers: vi.fn().mockResolvedValue([]), + } + + render( + , + { apiValue: mockApiValue }, + ) + + expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() + expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() + }) +}) diff --git a/src/hooks/useLoadConnect.tsx b/src/hooks/useLoadConnect.tsx index 8bbd416c60..f21cc302ee 100644 --- a/src/hooks/useLoadConnect.tsx +++ b/src/hooks/useLoadConnect.tsx @@ -8,14 +8,14 @@ import _isEmpty from 'lodash/isEmpty' import { loadConnect as loadConnectStart, - loadConnectSuccess, + loadConnectSuccessWithProfile, loadConnectError, } from 'src/redux/actions/Connect' import { COMBO_JOB_DATA_TYPES } from 'src/const/comboJobDataTypes' import { VERIFY_MODE } from 'src/const/Connect' import { useApi, ApiContextTypes } from 'src/context/ApiContext' import { __ } from 'src/utilities/Intl' -import type { RootState } from 'src/redux/Store' +import type { RootState, AppDispatch } from 'src/redux/Store' import { instutionSupportRequestedProducts } from 'src/utilities/Institution' import { getExperimentalFeatures } from 'src/redux/reducers/experimentalFeaturesSlice' @@ -53,7 +53,7 @@ const useLoadConnect = () => { return document.querySelector('html')?.getAttribute('lang') || 'en' }, [document.querySelector('html')?.getAttribute('lang')]) const [config, setConfig] = useState({} as ClientConfigType) - const dispatch = useDispatch() + const dispatch = useDispatch() const loadConnect = useCallback((config: ClientConfigType) => setConfig(config), [config]) @@ -78,7 +78,7 @@ const useLoadConnect = () => { if (clientSupportRequestedProducts(config, profiles.clientProfile)) { return from(api.loadMembers(clientLocale)).pipe( map((members = []) => - loadConnectSuccess({ + loadConnectSuccessWithProfile({ experimentalFeatures, members, widgetProfile: profiles.widgetProfile, diff --git a/src/redux/actions/Connect.js b/src/redux/actions/Connect.js index 4ee4dfbbfe..8b365df66b 100644 --- a/src/redux/actions/Connect.js +++ b/src/redux/actions/Connect.js @@ -58,6 +58,18 @@ export const loadConnectSuccess = (dependencies = {}) => ({ type: ActionTypes.LOAD_CONNECT_SUCCESS, payload: dependencies, }) +export const loadConnectSuccessWithProfile = + (dependencies = {}) => + (dispatch, getState) => { + const { profiles } = getState() + + dispatch( + loadConnectSuccess({ + ...dependencies, + user: profiles.user, + }), + ) + } export const loadConnectError = (err) => ({ type: ActionTypes.LOAD_CONNECT_ERROR, diff --git a/src/redux/reducers/Connect.js b/src/redux/reducers/Connect.js index 5844271c30..1674c91268 100644 --- a/src/redux/reducers/Connect.js +++ b/src/redux/reducers/Connect.js @@ -65,6 +65,7 @@ const loadConnectSuccess = (state, action) => { institution = {}, experimentalFeatures = {}, widgetProfile, + user = {}, } = action.payload return { @@ -83,6 +84,7 @@ const loadConnectSuccess = (state, action) => { institution, widgetProfile, experimentalFeatures, + user, ), ), selectedInstitution: institution, @@ -548,6 +550,7 @@ function getStartingStep( institution, widgetProfile, experimentalFeatures = {}, + user = {}, ) { // Unavailable institutions experimental feature: Make sure we don't load a user // directly to an institution that should be unavailable. @@ -574,9 +577,18 @@ function getStartingStep( (institution && institutionIsBlockedForCostReasons(institution)) || (member && memberIsBlockedForCostReasons(member)) || !institutionIsAvailable + const shouldStepToDemoConnectGuard = + user?.is_demo && + institution && + !institution?.is_demo && + (config.current_institution_guid || + config.current_institution_code || + config.current_member_guid) if (shouldStepToInstitutionStatusDetails) { return STEPS.INSTITUTION_STATUS_DETAILS + } else if (shouldStepToDemoConnectGuard) { + return STEPS.DEMO_CONNECT_GUARD } else if (shouldStepToMFA) // They configured connect to resolve MFA on a member. return STEPS.MFA diff --git a/src/redux/reducers/__tests__/Connect-test.js b/src/redux/reducers/__tests__/Connect-test.js index 3751806e11..e3c41c28de 100644 --- a/src/redux/reducers/__tests__/Connect-test.js +++ b/src/redux/reducers/__tests__/Connect-test.js @@ -389,6 +389,85 @@ describe('Connect redux store', () => { STEPS.ENTER_CREDENTIALS, ) }) + + it('should set the step to DEMO_CONNECT_GUARD when launching with current_institution_guid and user is demo but institution is not', () => { + const institution = { guid: 'INS-1', is_demo: false, credentials } + const user = { guid: 'USR-1', is_demo: true } + const config = { current_institution_guid: 'INS-1' } + const afterState = reducer( + defaultState, + loadConnectSuccess({ institution, config, widgetProfile, user }), + ) + + expect(afterState.location[afterState.location.length - 1].step).toEqual( + STEPS.DEMO_CONNECT_GUARD, + ) + }) + + it('should set the step to DEMO_CONNECT_GUARD when launching with current_institution_code and user is demo but institution is not', () => { + const institution = { guid: 'INS-1', code: 'bank_code', is_demo: false, credentials } + const user = { guid: 'USR-1', is_demo: true } + const config = { current_institution_code: 'bank_code' } + const afterState = reducer( + defaultState, + loadConnectSuccess({ institution, config, widgetProfile, user }), + ) + + expect(afterState.location[afterState.location.length - 1].step).toEqual( + STEPS.DEMO_CONNECT_GUARD, + ) + }) + + it('should set the step to DEMO_CONNECT_GUARD when launching with current_member_guid and user is demo but institution is not', () => { + const institution = { guid: 'INS-1', is_demo: false, credentials } + const user = { guid: 'USR-1', is_demo: true } + const member = genMember({ guid: 'MBR-1', connection_status: ReadableStatuses.CONNECTED }) + const config = { current_member_guid: 'MBR-1' } + const afterState = reducer( + defaultState, + loadConnectSuccess({ member, institution, config, widgetProfile, user }), + ) + + expect(afterState.location[afterState.location.length - 1].step).toEqual( + STEPS.DEMO_CONNECT_GUARD, + ) + }) + + it('should NOT set the step to DEMO_CONNECT_GUARD when launching with current_institution_guid but user is not demo', () => { + const institution = { guid: 'INS-1', is_demo: false, credentials } + const user = { guid: 'USR-1', is_demo: false } + const config = { current_institution_guid: 'INS-1' } + const afterState = reducer( + defaultState, + loadConnectSuccess({ institution, config, widgetProfile, user }), + ) + + expect(afterState.location[afterState.location.length - 1].step).toEqual( + STEPS.ENTER_CREDENTIALS, + ) + }) + + it('should NOT set the step to DEMO_CONNECT_GUARD when launching with current_institution_guid and both user and institution are demo', () => { + const institution = { guid: 'INS-1', is_demo: true, credentials } + const user = { guid: 'USR-1', is_demo: true } + const config = { current_institution_guid: 'INS-1' } + const afterState = reducer( + defaultState, + loadConnectSuccess({ institution, config, widgetProfile, user }), + ) + + expect(afterState.location[afterState.location.length - 1].step).toEqual( + STEPS.ENTER_CREDENTIALS, + ) + }) + + it('should NOT set the step to DEMO_CONNECT_GUARD when user is demo but no institution parameters are provided', () => { + const user = { guid: 'USR-1', is_demo: true } + const config = {} + const afterState = reducer(defaultState, loadConnectSuccess({ config, widgetProfile, user })) + + expect(afterState.location[afterState.location.length - 1].step).toEqual(STEPS.SEARCH) + }) }) describe('loadConnectError', () => { @@ -455,10 +534,7 @@ describe('Connect redux store', () => { const config = { mode: VERIFY_MODE } const afterState = reducer( { ...defaultState, isComponentLoading: true }, - { - type: ActionTypes.LOAD_CONNECT_SUCCESS, - payload: { config, members: [], widgetProfile }, - }, + loadConnectSuccess({ config, members: [], widgetProfile }), ) expect(afterState.location[afterState.location.length - 1].step).toEqual(STEPS.SEARCH) }) @@ -473,10 +549,7 @@ describe('Connect redux store', () => { const members = [member] const afterState = reducer( { ...defaultState, isComponentLoading: true }, - { - type: ActionTypes.LOAD_CONNECT_SUCCESS, - payload: { config, member, members, widgetProfile }, - }, + loadConnectSuccess({ config, member, members, widgetProfile }), ) expect(afterState.location[afterState.location.length - 1].step).toEqual( STEPS.ACTIONABLE_ERROR, @@ -500,10 +573,7 @@ describe('Connect redux store', () => { const members = [member] const afterState = reducer( { ...defaultState, isComponentLoading: true }, - { - type: ActionTypes.LOAD_CONNECT_SUCCESS, - payload: { config, member, members, widgetProfile }, - }, + loadConnectSuccess({ config, member, members, widgetProfile }), ) expect(afterState.location[afterState.location.length - 1].step).toEqual( STEPS.ACTIONABLE_ERROR, @@ -527,10 +597,7 @@ describe('Connect redux store', () => { const members = [member] const afterState = reducer( { ...defaultState, isComponentLoading: true }, - { - type: ActionTypes.LOAD_CONNECT_SUCCESS, - payload: { config, member, members, widgetProfile }, - }, + loadConnectSuccess({ config, member, members, widgetProfile }), ) expect(afterState.location[afterState.location.length - 1].step).toEqual( STEPS.ENTER_CREDENTIALS, @@ -554,10 +621,7 @@ describe('Connect redux store', () => { const members = [member] const afterState = reducer( { ...defaultState, isComponentLoading: true }, - { - type: ActionTypes.LOAD_CONNECT_SUCCESS, - payload: { config, member, members, widgetProfile }, - }, + loadConnectSuccess({ config, member, members, widgetProfile }), ) expect(afterState.location[afterState.location.length - 1].step).toEqual(STEPS.MFA) }) @@ -578,10 +642,7 @@ describe('Connect redux store', () => { const members = [member] const afterState = reducer( { ...defaultState, isComponentLoading: true }, - { - type: ActionTypes.LOAD_CONNECT_SUCCESS, - payload: { config, member, members, accounts: [], widgetProfile }, - }, + loadConnectSuccess({ config, member, members, accounts: [], widgetProfile }), ) expect(afterState.location[afterState.location.length - 1].step).toEqual( STEPS.ACTIONABLE_ERROR, From 81f3da983e7891afca1efd4f79fa031422b01826 Mon Sep 17 00:00:00 2001 From: Clement Mwimo Date: Tue, 25 Aug 2026 16:35:35 -0600 Subject: [PATCH 2/3] rename the test file --- src/{Connect-test.tsx => Connect.test.tsx} | 67 ++++++++++++++++++++- src/__tests__/Connect-test.tsx | 69 ---------------------- 2 files changed, 66 insertions(+), 70 deletions(-) rename src/{Connect-test.tsx => Connect.test.tsx} (74%) delete mode 100644 src/__tests__/Connect-test.tsx diff --git a/src/Connect-test.tsx b/src/Connect.test.tsx similarity index 74% rename from src/Connect-test.tsx rename to src/Connect.test.tsx index bb736254e2..d77b7cce22 100644 --- a/src/Connect-test.tsx +++ b/src/Connect.test.tsx @@ -2,7 +2,8 @@ import React from 'react' import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest' import { render, screen, waitFor } from 'src/utilities/testingLibrary' import { Connect } from './Connect' -import { initialState, masterData } from 'src/services/mockedData' +import { apiValue as apiValueMock } from 'src/const/apiProviderMock' +import { initialState, masterData, institutionData } from 'src/services/mockedData' import { STEPS } from 'src/const/Connect' describe('', () => { @@ -261,4 +262,68 @@ describe('', () => { }) }) }) + + describe('Connect - Demo Connect Guard', () => { + const defaultProps = { + clientConfig: { current_institution_guid: 'INS-123' } as ClientConfigType, + onShowConnectSuccessSurvey: () => undefined, + onSubmitConnectSuccessSurvey: () => {}, + profiles: { ...masterData, loading: false }, + } + + const nonDemoInstitution = { ...institutionData.institution, is_demo: false } + const demoInstitution = { ...institutionData.institution, is_demo: true } + const demoUser = { ...masterData.user, is_demo: true } + const regularUser = { ...masterData.user, is_demo: false } + + it('blocks demo user from accessing non-demo institution', async () => { + const mockApiValue = { + ...apiValueMock, + loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution), + loadMembers: vi.fn().mockResolvedValue([]), + } + + render( + , + { apiValue: mockApiValue }, + ) + + expect(await screen.findByText(/Demo mode active/i)).toBeInTheDocument() + }) + + it('allows demo user to access demo institution', async () => { + const mockApiValue = { + ...apiValueMock, + loadInstitutionByGuid: vi.fn().mockResolvedValue(demoInstitution), + loadMembers: vi.fn().mockResolvedValue([]), + } + + render( + , + { apiValue: mockApiValue }, + ) + + expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() + expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() + }) + + it('allows regular user to access non-demo institution', async () => { + const mockApiValue = { + ...apiValueMock, + loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution), + loadMembers: vi.fn().mockResolvedValue([]), + } + + render( + , + { apiValue: mockApiValue }, + ) + + expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() + expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() + }) + }) }) diff --git a/src/__tests__/Connect-test.tsx b/src/__tests__/Connect-test.tsx deleted file mode 100644 index d598887759..0000000000 --- a/src/__tests__/Connect-test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react' -import { describe, it, expect, vi } from 'vitest' -import { screen } from '@testing-library/react' - -import { Connect } from '../Connect' -import { render } from 'src/utilities/testingLibrary' -import { apiValue as apiValueMock } from 'src/const/apiProviderMock' -import { masterData, institutionData } from 'src/services/mockedData' - -describe('Connect - Demo Connect Guard', () => { - const defaultProps = { - clientConfig: { current_institution_guid: 'INS-123' } as ClientConfigType, - onShowConnectSuccessSurvey: () => undefined, - onSubmitConnectSuccessSurvey: () => {}, - profiles: { ...masterData, loading: false }, - } - - const nonDemoInstitution = { ...institutionData.institution, is_demo: false } - const demoInstitution = { ...institutionData.institution, is_demo: true } - const demoUser = { ...masterData.user, is_demo: true } - const regularUser = { ...masterData.user, is_demo: false } - - it('blocks demo user from accessing non-demo institution', async () => { - const mockApiValue = { - ...apiValueMock, - loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution), - loadMembers: vi.fn().mockResolvedValue([]), - } - - render( - , - { apiValue: mockApiValue }, - ) - - expect(await screen.findByText(/Demo mode active/i)).toBeInTheDocument() - }) - - it('allows demo user to access demo institution', async () => { - const mockApiValue = { - ...apiValueMock, - loadInstitutionByGuid: vi.fn().mockResolvedValue(demoInstitution), - loadMembers: vi.fn().mockResolvedValue([]), - } - - render( - , - { apiValue: mockApiValue }, - ) - - expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() - expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() - }) - - it('allows regular user to access non-demo institution', async () => { - const mockApiValue = { - ...apiValueMock, - loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution), - loadMembers: vi.fn().mockResolvedValue([]), - } - - render( - , - { apiValue: mockApiValue }, - ) - - expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument() - expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument() - }) -}) From 0d51178eace3d00443f0451a9109af241777f653 Mon Sep 17 00:00:00 2001 From: Clement Mwimo Date: Tue, 25 Aug 2026 17:01:24 -0600 Subject: [PATCH 3/3] feat: show Connect Guard for demo users launching widgets on non-demo institutions