Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion src/Connect-test.tsx → src/Connect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('<Connect />', () => {
Expand Down Expand Up @@ -261,4 +262,68 @@ describe('<Connect />', () => {
})
})
})

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(
<Connect {...defaultProps} profiles={{ ...masterData, user: demoUser, loading: false }} />,
{ 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(
<Connect {...defaultProps} profiles={{ ...masterData, user: demoUser, loading: false }} />,
{ 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(
<Connect
{...defaultProps}
profiles={{ ...masterData, user: regularUser, loading: false }}
/>,
{ apiValue: mockApiValue },
)

expect(await screen.findByText(/Log in at Test Bank/i)).toBeInTheDocument()
expect(screen.queryByText(/Demo mode active/i)).not.toBeInTheDocument()
})
})
})
8 changes: 4 additions & 4 deletions src/hooks/useLoadConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

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

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

Expand All @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions src/redux/actions/Connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
wesrisenmay-mx marked this conversation as resolved.
}),
)
}

export const loadConnectError = (err) => ({
type: ActionTypes.LOAD_CONNECT_ERROR,
Expand Down
12 changes: 12 additions & 0 deletions src/redux/reducers/Connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const loadConnectSuccess = (state, action) => {
institution = {},
experimentalFeatures = {},
widgetProfile,
user = {},
} = action.payload

return {
Expand All @@ -83,6 +84,7 @@ const loadConnectSuccess = (state, action) => {
institution,
widgetProfile,
experimentalFeatures,
user,
),
),
selectedInstitution: institution,
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
109 changes: 85 additions & 24 deletions src/redux/reducers/__tests__/Connect-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)
})
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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)
})
Expand All @@ -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,
Expand Down
Loading