Skip to content

Commit 6011f52

Browse files
Merge pull request #1992 from daveajrussell/force-password-refactor
Updates force password change flow to use Cypher first, falling back to dbms function call
2 parents b4d39ab + f0a69d2 commit 6011f52

File tree

8 files changed

+490
-116
lines changed

8 files changed

+490
-116
lines changed

src/browser/modules/Stream/Auth/ConnectionFormController.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ import {
4747
setActiveConnection,
4848
updateConnection
4949
} from 'shared/modules/connections/connectionsDuck'
50-
import { AuthenticationMethod } from 'shared/modules/connections/connectionsDuck'
51-
import { FORCE_CHANGE_PASSWORD } from 'shared/modules/cypher/cypherDuck'
50+
import {
51+
AuthenticationMethod,
52+
FORCE_CHANGE_PASSWORD
53+
} from 'shared/modules/connections/connectionsDuck'
5254
import { shouldRetainConnectionCredentials } from 'shared/modules/dbMeta/dbMetaDuck'
5355
import { CONNECTION_ID } from 'shared/modules/discovery/discoveryDuck'
5456
import { fetchBrowserDiscoveryDataFromUrl } from 'shared/modules/discovery/discoveryHelpers'

src/shared/modules/connections/connectionsDuck.test.ts

+312-1
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ import {
2828
DONE as DISCOVERY_DONE,
2929
updateDiscoveryConnection
3030
} from 'shared/modules/discovery/discoveryDuck'
31+
import forceResetPasswordQueryHelper, {
32+
MultiDatabaseNotSupportedError
33+
} from './forceResetPasswordQueryHelper'
3134

3235
jest.mock('services/bolt/bolt', () => {
3336
return {
3437
closeConnection: jest.fn(),
35-
openConnection: jest.fn()
38+
openConnection: jest.fn(),
39+
directConnect: jest.fn()
3640
}
3741
})
3842

@@ -457,3 +461,310 @@ describe('switchConnectionEpic', () => {
457461
return p
458462
})
459463
})
464+
465+
describe('handleForcePasswordChangeEpic', () => {
466+
const bus = createBus()
467+
const epicMiddleware = createEpicMiddleware(
468+
connections.handleForcePasswordChangeEpic
469+
)
470+
const mockStore = configureMockStore([
471+
epicMiddleware,
472+
createReduxMiddleware(bus)
473+
])
474+
475+
let store: any
476+
477+
const $$responseChannel = 'test-channel'
478+
const action = {
479+
host: 'bolt://localhost:7687',
480+
type: connections.FORCE_CHANGE_PASSWORD,
481+
password: 'changeme',
482+
newPassword: 'password1',
483+
$$responseChannel
484+
}
485+
486+
const executePasswordResetQuerySpy = jest.spyOn(
487+
forceResetPasswordQueryHelper,
488+
'executePasswordResetQuery'
489+
)
490+
491+
const executeAlterCurrentUserQuerySpy = jest.spyOn(
492+
forceResetPasswordQueryHelper,
493+
'executeAlterCurrentUserQuery'
494+
)
495+
496+
const executeCallChangePasswordQuerySpy = jest.spyOn(
497+
forceResetPasswordQueryHelper,
498+
'executeCallChangePasswordQuery'
499+
)
500+
501+
const mockSessionClose = jest.fn()
502+
const mockSessionExecuteWrite = jest.fn()
503+
504+
const mockDriver = {
505+
session: jest.fn().mockReturnValue({
506+
close: mockSessionClose,
507+
executeWrite: mockSessionExecuteWrite
508+
}),
509+
close: jest.fn().mockReturnValue(true)
510+
}
511+
512+
beforeAll(() => {
513+
store = mockStore({})
514+
})
515+
516+
beforeEach(() => {
517+
;(bolt.directConnect as jest.Mock).mockResolvedValue(mockDriver)
518+
})
519+
520+
afterEach(() => {
521+
store.clearActions()
522+
bus.reset()
523+
jest.clearAllMocks()
524+
})
525+
526+
test('handleForcePasswordChangeEpic resolves with an error if directConnect fails', () => {
527+
// Given
528+
const message = 'An error occurred.'
529+
;(bolt.directConnect as jest.Mock).mockRejectedValue(new Error(message))
530+
531+
const p = new Promise<void>((resolve, reject) => {
532+
bus.take($$responseChannel, currentAction => {
533+
// Then
534+
const actions = store.getActions()
535+
try {
536+
expect(actions).toEqual([action, currentAction])
537+
538+
expect(executeAlterCurrentUserQuerySpy).not.toHaveBeenCalled()
539+
540+
expect(executeCallChangePasswordQuerySpy).not.toHaveBeenCalled()
541+
542+
expect(executePasswordResetQuerySpy).not.toHaveBeenCalled()
543+
544+
expect(currentAction).toEqual({
545+
error: expect.objectContaining({
546+
message
547+
}),
548+
success: false,
549+
type: $$responseChannel
550+
})
551+
552+
resolve()
553+
554+
expect(mockDriver.close).not.toHaveBeenCalled()
555+
expect(mockSessionClose).not.toHaveBeenCalled()
556+
} catch (e) {
557+
reject(e)
558+
}
559+
})
560+
})
561+
562+
// When
563+
epicMiddleware.replaceEpic(connections.handleForcePasswordChangeEpic)
564+
store.clearActions()
565+
store.dispatch(action)
566+
567+
// Return
568+
return p
569+
})
570+
571+
test('handleForcePasswordChangeEpic resolves when successfully executing cypher query', () => {
572+
// Given
573+
mockSessionExecuteWrite.mockResolvedValue(true)
574+
575+
const p = new Promise<void>((resolve, reject) => {
576+
bus.take($$responseChannel, currentAction => {
577+
// Then
578+
const actions = store.getActions()
579+
try {
580+
expect(actions).toEqual([action, currentAction])
581+
582+
expect(executeAlterCurrentUserQuerySpy).toHaveBeenCalledTimes(1)
583+
584+
expect(executeCallChangePasswordQuerySpy).not.toHaveBeenCalled()
585+
586+
expect(executePasswordResetQuerySpy).toHaveBeenCalledTimes(1)
587+
588+
expect(executePasswordResetQuerySpy).toHaveBeenCalledWith(
589+
expect.anything(),
590+
expect.objectContaining({
591+
parameters: { newPw: 'password1', oldPw: 'changeme' },
592+
query: 'ALTER CURRENT USER SET PASSWORD FROM $oldPw TO $newPw'
593+
}),
594+
expect.anything(),
595+
{ database: 'system' }
596+
)
597+
598+
expect(currentAction).toEqual({
599+
result: { meta: 'bolt://localhost:7687' },
600+
success: true,
601+
type: $$responseChannel
602+
})
603+
604+
resolve()
605+
606+
expect(mockDriver.close).toHaveBeenCalledTimes(1)
607+
expect(mockSessionClose).toHaveBeenCalledTimes(1)
608+
} catch (e) {
609+
reject(e)
610+
}
611+
})
612+
})
613+
614+
// When
615+
epicMiddleware.replaceEpic(connections.handleForcePasswordChangeEpic)
616+
store.clearActions()
617+
store.dispatch(action)
618+
619+
// Return
620+
return p
621+
})
622+
623+
test('handleForcePasswordChangeEpic resolves with an error if cypher query fails', () => {
624+
// Given
625+
const message = 'A password must be at least 8 characters.'
626+
mockSessionExecuteWrite
627+
.mockRejectedValueOnce(new Error(message))
628+
.mockResolvedValue(true)
629+
630+
const p = new Promise<void>((resolve, reject) => {
631+
bus.take($$responseChannel, currentAction => {
632+
// Then
633+
const actions = store.getActions()
634+
try {
635+
expect(actions).toEqual([action, currentAction])
636+
637+
expect(executeAlterCurrentUserQuerySpy).toHaveBeenCalledTimes(1)
638+
639+
expect(executeCallChangePasswordQuerySpy).not.toHaveBeenCalled()
640+
641+
expect(executePasswordResetQuerySpy).toHaveBeenCalledTimes(1)
642+
643+
expect(currentAction).toEqual({
644+
error: expect.objectContaining({
645+
message
646+
}),
647+
success: false,
648+
type: $$responseChannel
649+
})
650+
651+
resolve()
652+
653+
expect(mockDriver.close).toHaveBeenCalledTimes(1)
654+
expect(mockSessionClose).toHaveBeenCalledTimes(1)
655+
} catch (e) {
656+
reject(e)
657+
}
658+
})
659+
})
660+
661+
// When
662+
epicMiddleware.replaceEpic(connections.handleForcePasswordChangeEpic)
663+
store.clearActions()
664+
store.dispatch(action)
665+
666+
// Return
667+
return p
668+
})
669+
670+
test('handleForcePasswordChangeEpic resolves when successfully falling back to dbms function call', () => {
671+
// Given
672+
mockSessionExecuteWrite
673+
.mockRejectedValueOnce(new MultiDatabaseNotSupportedError())
674+
.mockResolvedValue(true)
675+
676+
const p = new Promise<void>((resolve, reject) => {
677+
bus.take($$responseChannel, currentAction => {
678+
// Then
679+
const actions = store.getActions()
680+
try {
681+
expect(actions).toEqual([action, currentAction])
682+
683+
expect(executeAlterCurrentUserQuerySpy).toHaveBeenCalledTimes(1)
684+
685+
expect(executeCallChangePasswordQuerySpy).toHaveBeenCalledTimes(1)
686+
687+
expect(executePasswordResetQuerySpy).toHaveBeenCalledTimes(2)
688+
689+
expect(executePasswordResetQuerySpy).toHaveBeenLastCalledWith(
690+
expect.anything(),
691+
expect.objectContaining({
692+
parameters: { password: 'password1' },
693+
query: 'CALL dbms.security.changePassword($password)'
694+
}),
695+
expect.anything(),
696+
undefined
697+
)
698+
699+
expect(currentAction).toEqual({
700+
result: { meta: 'bolt://localhost:7687' },
701+
success: true,
702+
type: $$responseChannel
703+
})
704+
705+
resolve()
706+
707+
expect(mockDriver.close).toHaveBeenCalledTimes(1)
708+
expect(mockSessionClose).toHaveBeenCalledTimes(2)
709+
} catch (e) {
710+
reject(e)
711+
}
712+
})
713+
})
714+
715+
// When
716+
epicMiddleware.replaceEpic(connections.handleForcePasswordChangeEpic)
717+
store.clearActions()
718+
store.dispatch(action)
719+
720+
// Return
721+
return p
722+
})
723+
724+
test('handleForcePasswordChangeEpic resolves with an error if dbms function call fails', () => {
725+
// Given
726+
const message = 'A password must be at least 8 characters.'
727+
mockSessionExecuteWrite
728+
.mockRejectedValueOnce(new MultiDatabaseNotSupportedError())
729+
.mockRejectedValue(new Error(message))
730+
731+
const p = new Promise<void>((resolve, reject) => {
732+
bus.take($$responseChannel, currentAction => {
733+
// Then
734+
const actions = store.getActions()
735+
try {
736+
expect(actions).toEqual([action, currentAction])
737+
738+
expect(executeAlterCurrentUserQuerySpy).toHaveBeenCalledTimes(1)
739+
740+
expect(executeCallChangePasswordQuerySpy).toHaveBeenCalledTimes(1)
741+
742+
expect(executePasswordResetQuerySpy).toHaveBeenCalledTimes(2)
743+
744+
expect(currentAction).toEqual({
745+
error: expect.objectContaining({
746+
message
747+
}),
748+
success: false,
749+
type: $$responseChannel
750+
})
751+
752+
resolve()
753+
754+
expect(mockDriver.close).toHaveBeenCalledTimes(1)
755+
expect(mockSessionClose).toHaveBeenCalledTimes(2)
756+
} catch (e) {
757+
reject(e)
758+
}
759+
})
760+
})
761+
762+
// When
763+
epicMiddleware.replaceEpic(connections.handleForcePasswordChangeEpic)
764+
store.clearActions()
765+
store.dispatch(action)
766+
767+
// Return
768+
return p
769+
})
770+
})

0 commit comments

Comments
 (0)