Skip to content

Commit a50a872

Browse files
mwclemyClement Mwimo
andauthored
Show demo connect guard for demo users launching the widget on non-demo institutions (#328)
* feat: show Connect Guard for demo users launching widgets on non-demo institutions * used a thunk for the to get user profile * added high level component test for demo connect guard * no need to pass in store in the test --------- Co-authored-by: Clement Mwimo <clement.mwimo@mx.com>
1 parent 67b4d42 commit a50a872

5 files changed

Lines changed: 182 additions & 28 deletions

File tree

src/__tests__/Connect-test.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from 'react'
2+
import { describe, it, expect, vi } from 'vitest'
3+
import { screen } from '@testing-library/react'
4+
5+
import { Connect } from '../Connect'
6+
import { render } from 'src/utilities/testingLibrary'
7+
import { apiValue as apiValueMock } from 'src/const/apiProviderMock'
8+
import { masterData, institutionData } from 'src/services/mockedData'
9+
10+
describe('Connect - Demo Connect Guard', () => {
11+
const defaultProps = {
12+
clientConfig: { current_institution_guid: 'INS-123' } as ClientConfigType,
13+
onShowConnectSuccessSurvey: () => undefined,
14+
onSubmitConnectSuccessSurvey: () => {},
15+
profiles: { ...masterData, loading: false },
16+
}
17+
18+
const nonDemoInstitution = { ...institutionData.institution, is_demo: false }
19+
const demoInstitution = { ...institutionData.institution, is_demo: true }
20+
const demoUser = { ...masterData.user, is_demo: true }
21+
const regularUser = { ...masterData.user, is_demo: false }
22+
23+
it('blocks demo user from accessing non-demo institution', async () => {
24+
const mockApiValue = {
25+
...apiValueMock,
26+
loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution),
27+
loadMembers: vi.fn().mockResolvedValue([]),
28+
}
29+
30+
render(
31+
<Connect {...defaultProps} profiles={{ ...masterData, user: demoUser, loading: false }} />,
32+
{ apiValue: mockApiValue },
33+
)
34+
35+
expect(await screen.findByText(/Demo mode active/i)).toBeInTheDocument()
36+
})
37+
38+
it('allows demo user to access demo institution', async () => {
39+
const mockApiValue = {
40+
...apiValueMock,
41+
loadInstitutionByGuid: vi.fn().mockResolvedValue(demoInstitution),
42+
loadMembers: vi.fn().mockResolvedValue([]),
43+
}
44+
45+
render(
46+
<Connect {...defaultProps} profiles={{ ...masterData, user: demoUser, loading: false }} />,
47+
{ apiValue: mockApiValue },
48+
)
49+
50+
expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument()
51+
expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument()
52+
})
53+
54+
it('allows regular user to access non-demo institution', async () => {
55+
const mockApiValue = {
56+
...apiValueMock,
57+
loadInstitutionByGuid: vi.fn().mockResolvedValue(nonDemoInstitution),
58+
loadMembers: vi.fn().mockResolvedValue([]),
59+
}
60+
61+
render(
62+
<Connect {...defaultProps} profiles={{ ...masterData, user: regularUser, loading: false }} />,
63+
{ apiValue: mockApiValue },
64+
)
65+
66+
expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument()
67+
expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument()
68+
})
69+
})

src/hooks/useLoadConnect.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ import _isEmpty from 'lodash/isEmpty'
88

99
import {
1010
loadConnect as loadConnectStart,
11-
loadConnectSuccess,
11+
loadConnectSuccessWithProfile,
1212
loadConnectError,
1313
} from 'src/redux/actions/Connect'
1414
import { COMBO_JOB_DATA_TYPES } from 'src/const/comboJobDataTypes'
1515
import { VERIFY_MODE } from 'src/const/Connect'
1616
import { useApi, ApiContextTypes } from 'src/context/ApiContext'
1717
import { __ } from 'src/utilities/Intl'
18-
import type { RootState } from 'src/redux/Store'
18+
import type { RootState, AppDispatch } from 'src/redux/Store'
1919
import { instutionSupportRequestedProducts } from 'src/utilities/Institution'
2020
import { getExperimentalFeatures } from 'src/redux/reducers/experimentalFeaturesSlice'
2121

@@ -53,7 +53,7 @@ const useLoadConnect = () => {
5353
return document.querySelector('html')?.getAttribute('lang') || 'en'
5454
}, [document.querySelector('html')?.getAttribute('lang')])
5555
const [config, setConfig] = useState<ClientConfigType>({} as ClientConfigType)
56-
const dispatch = useDispatch()
56+
const dispatch = useDispatch<AppDispatch>()
5757

5858
const loadConnect = useCallback((config: ClientConfigType) => setConfig(config), [config])
5959

@@ -78,7 +78,7 @@ const useLoadConnect = () => {
7878
if (clientSupportRequestedProducts(config, profiles.clientProfile)) {
7979
return from(api.loadMembers(clientLocale)).pipe(
8080
map((members = []) =>
81-
loadConnectSuccess({
81+
loadConnectSuccessWithProfile({
8282
experimentalFeatures,
8383
members,
8484
widgetProfile: profiles.widgetProfile,

src/redux/actions/Connect.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ export const loadConnectSuccess = (dependencies = {}) => ({
5858
type: ActionTypes.LOAD_CONNECT_SUCCESS,
5959
payload: dependencies,
6060
})
61+
export const loadConnectSuccessWithProfile =
62+
(dependencies = {}) =>
63+
(dispatch, getState) => {
64+
const { profiles } = getState()
65+
66+
dispatch(
67+
loadConnectSuccess({
68+
...dependencies,
69+
user: profiles.user,
70+
}),
71+
)
72+
}
6173

6274
export const loadConnectError = (err) => ({
6375
type: ActionTypes.LOAD_CONNECT_ERROR,

src/redux/reducers/Connect.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const loadConnectSuccess = (state, action) => {
6565
institution = {},
6666
experimentalFeatures = {},
6767
widgetProfile,
68+
user = {},
6869
} = action.payload
6970

7071
return {
@@ -83,6 +84,7 @@ const loadConnectSuccess = (state, action) => {
8384
institution,
8485
widgetProfile,
8586
experimentalFeatures,
87+
user,
8688
),
8789
),
8890
selectedInstitution: institution,
@@ -548,6 +550,7 @@ function getStartingStep(
548550
institution,
549551
widgetProfile,
550552
experimentalFeatures = {},
553+
user = {},
551554
) {
552555
// Unavailable institutions experimental feature: Make sure we don't load a user
553556
// directly to an institution that should be unavailable.
@@ -574,9 +577,18 @@ function getStartingStep(
574577
(institution && institutionIsBlockedForCostReasons(institution)) ||
575578
(member && memberIsBlockedForCostReasons(member)) ||
576579
!institutionIsAvailable
580+
const shouldStepToDemoConnectGuard =
581+
user?.is_demo &&
582+
institution &&
583+
!institution?.is_demo &&
584+
(config.current_institution_guid ||
585+
config.current_institution_code ||
586+
config.current_member_guid)
577587

578588
if (shouldStepToInstitutionStatusDetails) {
579589
return STEPS.INSTITUTION_STATUS_DETAILS
590+
} else if (shouldStepToDemoConnectGuard) {
591+
return STEPS.DEMO_CONNECT_GUARD
580592
} else if (shouldStepToMFA)
581593
// They configured connect to resolve MFA on a member.
582594
return STEPS.MFA

src/redux/reducers/__tests__/Connect-test.js

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,85 @@ describe('Connect redux store', () => {
389389
STEPS.ENTER_CREDENTIALS,
390390
)
391391
})
392+
393+
it('should set the step to DEMO_CONNECT_GUARD when launching with current_institution_guid and user is demo but institution is not', () => {
394+
const institution = { guid: 'INS-1', is_demo: false, credentials }
395+
const user = { guid: 'USR-1', is_demo: true }
396+
const config = { current_institution_guid: 'INS-1' }
397+
const afterState = reducer(
398+
defaultState,
399+
loadConnectSuccess({ institution, config, widgetProfile, user }),
400+
)
401+
402+
expect(afterState.location[afterState.location.length - 1].step).toEqual(
403+
STEPS.DEMO_CONNECT_GUARD,
404+
)
405+
})
406+
407+
it('should set the step to DEMO_CONNECT_GUARD when launching with current_institution_code and user is demo but institution is not', () => {
408+
const institution = { guid: 'INS-1', code: 'bank_code', is_demo: false, credentials }
409+
const user = { guid: 'USR-1', is_demo: true }
410+
const config = { current_institution_code: 'bank_code' }
411+
const afterState = reducer(
412+
defaultState,
413+
loadConnectSuccess({ institution, config, widgetProfile, user }),
414+
)
415+
416+
expect(afterState.location[afterState.location.length - 1].step).toEqual(
417+
STEPS.DEMO_CONNECT_GUARD,
418+
)
419+
})
420+
421+
it('should set the step to DEMO_CONNECT_GUARD when launching with current_member_guid and user is demo but institution is not', () => {
422+
const institution = { guid: 'INS-1', is_demo: false, credentials }
423+
const user = { guid: 'USR-1', is_demo: true }
424+
const member = genMember({ guid: 'MBR-1', connection_status: ReadableStatuses.CONNECTED })
425+
const config = { current_member_guid: 'MBR-1' }
426+
const afterState = reducer(
427+
defaultState,
428+
loadConnectSuccess({ member, institution, config, widgetProfile, user }),
429+
)
430+
431+
expect(afterState.location[afterState.location.length - 1].step).toEqual(
432+
STEPS.DEMO_CONNECT_GUARD,
433+
)
434+
})
435+
436+
it('should NOT set the step to DEMO_CONNECT_GUARD when launching with current_institution_guid but user is not demo', () => {
437+
const institution = { guid: 'INS-1', is_demo: false, credentials }
438+
const user = { guid: 'USR-1', is_demo: false }
439+
const config = { current_institution_guid: 'INS-1' }
440+
const afterState = reducer(
441+
defaultState,
442+
loadConnectSuccess({ institution, config, widgetProfile, user }),
443+
)
444+
445+
expect(afterState.location[afterState.location.length - 1].step).toEqual(
446+
STEPS.ENTER_CREDENTIALS,
447+
)
448+
})
449+
450+
it('should NOT set the step to DEMO_CONNECT_GUARD when launching with current_institution_guid and both user and institution are demo', () => {
451+
const institution = { guid: 'INS-1', is_demo: true, credentials }
452+
const user = { guid: 'USR-1', is_demo: true }
453+
const config = { current_institution_guid: 'INS-1' }
454+
const afterState = reducer(
455+
defaultState,
456+
loadConnectSuccess({ institution, config, widgetProfile, user }),
457+
)
458+
459+
expect(afterState.location[afterState.location.length - 1].step).toEqual(
460+
STEPS.ENTER_CREDENTIALS,
461+
)
462+
})
463+
464+
it('should NOT set the step to DEMO_CONNECT_GUARD when user is demo but no institution parameters are provided', () => {
465+
const user = { guid: 'USR-1', is_demo: true }
466+
const config = {}
467+
const afterState = reducer(defaultState, loadConnectSuccess({ config, widgetProfile, user }))
468+
469+
expect(afterState.location[afterState.location.length - 1].step).toEqual(STEPS.SEARCH)
470+
})
392471
})
393472

394473
describe('loadConnectError', () => {
@@ -455,10 +534,7 @@ describe('Connect redux store', () => {
455534
const config = { mode: VERIFY_MODE }
456535
const afterState = reducer(
457536
{ ...defaultState, isComponentLoading: true },
458-
{
459-
type: ActionTypes.LOAD_CONNECT_SUCCESS,
460-
payload: { config, members: [], widgetProfile },
461-
},
537+
loadConnectSuccess({ config, members: [], widgetProfile }),
462538
)
463539
expect(afterState.location[afterState.location.length - 1].step).toEqual(STEPS.SEARCH)
464540
})
@@ -473,10 +549,7 @@ describe('Connect redux store', () => {
473549
const members = [member]
474550
const afterState = reducer(
475551
{ ...defaultState, isComponentLoading: true },
476-
{
477-
type: ActionTypes.LOAD_CONNECT_SUCCESS,
478-
payload: { config, member, members, widgetProfile },
479-
},
552+
loadConnectSuccess({ config, member, members, widgetProfile }),
480553
)
481554
expect(afterState.location[afterState.location.length - 1].step).toEqual(
482555
STEPS.ACTIONABLE_ERROR,
@@ -500,10 +573,7 @@ describe('Connect redux store', () => {
500573
const members = [member]
501574
const afterState = reducer(
502575
{ ...defaultState, isComponentLoading: true },
503-
{
504-
type: ActionTypes.LOAD_CONNECT_SUCCESS,
505-
payload: { config, member, members, widgetProfile },
506-
},
576+
loadConnectSuccess({ config, member, members, widgetProfile }),
507577
)
508578
expect(afterState.location[afterState.location.length - 1].step).toEqual(
509579
STEPS.ACTIONABLE_ERROR,
@@ -527,10 +597,7 @@ describe('Connect redux store', () => {
527597
const members = [member]
528598
const afterState = reducer(
529599
{ ...defaultState, isComponentLoading: true },
530-
{
531-
type: ActionTypes.LOAD_CONNECT_SUCCESS,
532-
payload: { config, member, members, widgetProfile },
533-
},
600+
loadConnectSuccess({ config, member, members, widgetProfile }),
534601
)
535602
expect(afterState.location[afterState.location.length - 1].step).toEqual(
536603
STEPS.ENTER_CREDENTIALS,
@@ -554,10 +621,7 @@ describe('Connect redux store', () => {
554621
const members = [member]
555622
const afterState = reducer(
556623
{ ...defaultState, isComponentLoading: true },
557-
{
558-
type: ActionTypes.LOAD_CONNECT_SUCCESS,
559-
payload: { config, member, members, widgetProfile },
560-
},
624+
loadConnectSuccess({ config, member, members, widgetProfile }),
561625
)
562626
expect(afterState.location[afterState.location.length - 1].step).toEqual(STEPS.MFA)
563627
})
@@ -578,10 +642,7 @@ describe('Connect redux store', () => {
578642
const members = [member]
579643
const afterState = reducer(
580644
{ ...defaultState, isComponentLoading: true },
581-
{
582-
type: ActionTypes.LOAD_CONNECT_SUCCESS,
583-
payload: { config, member, members, accounts: [], widgetProfile },
584-
},
645+
loadConnectSuccess({ config, member, members, accounts: [], widgetProfile }),
585646
)
586647
expect(afterState.location[afterState.location.length - 1].step).toEqual(
587648
STEPS.ACTIONABLE_ERROR,

0 commit comments

Comments
 (0)