-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Vault Timeout Service Tests & Bug Fix (#7021)
* Add Tests to VaultTimeoutService * Fix Bug Get availableTimeoutActions for the user actually locking instead of getting the timeout actions for the active user.
- Loading branch information
1 parent
48c6033
commit 3451ee8
Showing
2 changed files
with
314 additions
and
1 deletion.
There are no files selected for viewing
313 changes: 313 additions & 0 deletions
313
libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
import { MockProxy, any, mock } from "jest-mock-extended"; | ||
import { BehaviorSubject } from "rxjs"; | ||
|
||
import { SearchService } from "../../abstractions/search.service"; | ||
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; | ||
import { AuthService } from "../../auth/abstractions/auth.service"; | ||
import { AuthenticationStatus } from "../../auth/enums/authentication-status"; | ||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; | ||
import { CryptoService } from "../../platform/abstractions/crypto.service"; | ||
import { MessagingService } from "../../platform/abstractions/messaging.service"; | ||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; | ||
import { StateService } from "../../platform/abstractions/state.service"; | ||
import { Account } from "../../platform/models/domain/account"; | ||
import { CipherService } from "../../vault/abstractions/cipher.service"; | ||
import { CollectionService } from "../../vault/abstractions/collection.service"; | ||
import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; | ||
|
||
import { VaultTimeoutService } from "./vault-timeout.service"; | ||
|
||
describe("VaultTimeoutService", () => { | ||
let cipherService: MockProxy<CipherService>; | ||
let folderService: MockProxy<FolderService>; | ||
let collectionService: MockProxy<CollectionService>; | ||
let cryptoService: MockProxy<CryptoService>; | ||
let platformUtilsService: MockProxy<PlatformUtilsService>; | ||
let messagingService: MockProxy<MessagingService>; | ||
let searchService: MockProxy<SearchService>; | ||
let stateService: MockProxy<StateService>; | ||
let authService: MockProxy<AuthService>; | ||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>; | ||
let lockedCallback: jest.Mock<Promise<void>, [userId: string]>; | ||
let loggedOutCallback: jest.Mock<Promise<void>, [expired: boolean, userId?: string]>; | ||
|
||
let accountsSubject: BehaviorSubject<Record<string, Account>>; | ||
let vaultTimeoutActionSubject: BehaviorSubject<VaultTimeoutAction>; | ||
let availableVaultTimeoutActionsSubject: BehaviorSubject<VaultTimeoutAction[]>; | ||
|
||
let vaultTimeoutService: VaultTimeoutService; | ||
|
||
beforeEach(() => { | ||
cipherService = mock(); | ||
folderService = mock(); | ||
collectionService = mock(); | ||
cryptoService = mock(); | ||
platformUtilsService = mock(); | ||
messagingService = mock(); | ||
searchService = mock(); | ||
stateService = mock(); | ||
authService = mock(); | ||
vaultTimeoutSettingsService = mock(); | ||
|
||
lockedCallback = jest.fn(); | ||
loggedOutCallback = jest.fn(); | ||
|
||
accountsSubject = new BehaviorSubject(null); | ||
|
||
stateService.accounts$ = accountsSubject; | ||
|
||
vaultTimeoutActionSubject = new BehaviorSubject(VaultTimeoutAction.Lock); | ||
|
||
vaultTimeoutSettingsService.vaultTimeoutAction$.mockReturnValue(vaultTimeoutActionSubject); | ||
|
||
availableVaultTimeoutActionsSubject = new BehaviorSubject<VaultTimeoutAction[]>([]); | ||
|
||
vaultTimeoutService = new VaultTimeoutService( | ||
cipherService, | ||
folderService, | ||
collectionService, | ||
cryptoService, | ||
platformUtilsService, | ||
messagingService, | ||
searchService, | ||
stateService, | ||
authService, | ||
vaultTimeoutSettingsService, | ||
lockedCallback, | ||
loggedOutCallback | ||
); | ||
}); | ||
|
||
// Helper for setting up mocks for multiple users | ||
const setupAccounts = ( | ||
accounts: Record< | ||
string, | ||
{ | ||
authStatus?: AuthenticationStatus; | ||
isAuthenticated?: boolean; | ||
lastActive?: number; | ||
vaultTimeout?: number; | ||
timeoutAction?: VaultTimeoutAction; | ||
availableTimeoutActions?: VaultTimeoutAction[]; | ||
} | ||
>, | ||
userId?: string | ||
) => { | ||
// Both are available by default and the specific test can change this per test | ||
availableVaultTimeoutActionsSubject.next([VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut]); | ||
|
||
authService.getAuthStatus.mockImplementation((userId) => { | ||
return Promise.resolve(accounts[userId]?.authStatus); | ||
}); | ||
stateService.getIsAuthenticated.mockImplementation((options) => { | ||
return Promise.resolve(accounts[options.userId]?.isAuthenticated); | ||
}); | ||
|
||
vaultTimeoutSettingsService.getVaultTimeout.mockImplementation((userId) => { | ||
return Promise.resolve(accounts[userId]?.vaultTimeout); | ||
}); | ||
|
||
stateService.getLastActive.mockImplementation((options) => { | ||
return Promise.resolve(accounts[options.userId]?.lastActive); | ||
}); | ||
|
||
stateService.getUserId.mockResolvedValue(userId); | ||
|
||
vaultTimeoutSettingsService.vaultTimeoutAction$.mockImplementation((userId) => { | ||
return new BehaviorSubject<VaultTimeoutAction>(accounts[userId]?.timeoutAction); | ||
}); | ||
|
||
vaultTimeoutSettingsService.availableVaultTimeoutActions$.mockImplementation((userId) => { | ||
return new BehaviorSubject<VaultTimeoutAction[]>( | ||
// Default to both options if it wasn't supplied at all | ||
accounts[userId]?.availableTimeoutActions ?? [ | ||
VaultTimeoutAction.Lock, | ||
VaultTimeoutAction.LogOut, | ||
] | ||
); | ||
}); | ||
|
||
const accountsSubjectValue: Record<string, Account> = Object.keys(accounts).reduce( | ||
(agg, key) => { | ||
const newPartial: Record<string, unknown> = {}; | ||
newPartial[key] = null; // No values actually matter on this other than the key | ||
return Object.assign(agg, newPartial); | ||
}, | ||
{} as Record<string, Account> | ||
); | ||
accountsSubject.next(accountsSubjectValue); | ||
}; | ||
|
||
const expectUserToHaveLocked = (userId: string) => { | ||
// This does NOT assert all the things that the lock process does | ||
expect(stateService.getIsAuthenticated).toHaveBeenCalledWith({ userId: userId }); | ||
expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId); | ||
expect(stateService.setEverBeenUnlocked).toHaveBeenCalledWith(true, { userId: userId }); | ||
expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId }); | ||
expect(cryptoService.clearUserKey).toHaveBeenCalledWith(false, userId); | ||
expect(cryptoService.clearMasterKey).toHaveBeenCalledWith(userId); | ||
expect(cipherService.clearCache).toHaveBeenCalledWith(userId); | ||
expect(lockedCallback).toHaveBeenCalledWith(userId); | ||
}; | ||
|
||
const expectUserToHaveLoggedOut = (userId: string) => { | ||
expect(loggedOutCallback).toHaveBeenCalledWith(false, userId); | ||
}; | ||
|
||
const expectNoAction = (userId: string) => { | ||
expect(lockedCallback).not.toHaveBeenCalledWith(userId); | ||
expect(loggedOutCallback).not.toHaveBeenCalledWith(any(), userId); | ||
}; | ||
|
||
describe("checkVaultTimeout", () => { | ||
it.each([AuthenticationStatus.Locked, AuthenticationStatus.LoggedOut])( | ||
"should not try to log out or lock any user that has authStatus === %s.", | ||
async (authStatus) => { | ||
platformUtilsService.isViewOpen.mockResolvedValue(false); | ||
setupAccounts({ | ||
1: { | ||
authStatus: authStatus, | ||
isAuthenticated: true, | ||
}, | ||
}); | ||
|
||
expectNoAction("1"); | ||
} | ||
); | ||
|
||
it.each([ | ||
null, // never | ||
-1, // onRestart | ||
-2, // onLocked | ||
-3, // onSleep | ||
-4, // onIdle | ||
])( | ||
"does not log out or lock a user who has %s as their vault timeout", | ||
async (vaultTimeout) => { | ||
setupAccounts({ | ||
1: { | ||
authStatus: AuthenticationStatus.Unlocked, | ||
vaultTimeout: vaultTimeout, | ||
isAuthenticated: true, | ||
}, | ||
}); | ||
|
||
await vaultTimeoutService.checkVaultTimeout(); | ||
|
||
expectNoAction("1"); | ||
} | ||
); | ||
|
||
it.each([undefined, null])( | ||
"should not log out or lock a user who has %s lastActive value", | ||
async (lastActive) => { | ||
setupAccounts({ | ||
1: { | ||
authStatus: AuthenticationStatus.Unlocked, | ||
vaultTimeout: 1, // One minute | ||
lastActive: lastActive, | ||
}, | ||
}); | ||
|
||
await vaultTimeoutService.checkVaultTimeout(); | ||
|
||
expectNoAction("1"); | ||
} | ||
); | ||
|
||
it("should lock an account that isn't active and has immediate as their timeout when view is not open", async () => { | ||
// Arrange | ||
platformUtilsService.isViewOpen.mockResolvedValue(false); | ||
|
||
setupAccounts({ | ||
1: { | ||
authStatus: AuthenticationStatus.Unlocked, | ||
isAuthenticated: true, | ||
vaultTimeout: 0, // Immediately | ||
lastActive: new Date().getTime() - 10 * 1000, // Last active 10 seconds ago | ||
}, | ||
2: { | ||
authStatus: AuthenticationStatus.Unlocked, | ||
isAuthenticated: true, | ||
vaultTimeout: 1, // One minute | ||
lastActive: new Date().getTime() - 10 * 1000, // Last active 10 seconds ago | ||
}, | ||
}); | ||
|
||
// Act | ||
await vaultTimeoutService.checkVaultTimeout(); | ||
|
||
// Assert | ||
expectUserToHaveLocked("1"); | ||
expectNoAction("2"); | ||
}); | ||
|
||
it("should run action on an account that hasn't been active for greater than 1 minute and has a vault timeout for 1 minutes", async () => { | ||
platformUtilsService.isViewOpen.mockResolvedValue(false); | ||
|
||
setupAccounts( | ||
{ | ||
1: { | ||
authStatus: AuthenticationStatus.Unlocked, | ||
isAuthenticated: true, | ||
vaultTimeout: 1, // One minute | ||
lastActive: new Date().getTime() - 10 * 1000, | ||
}, | ||
2: { | ||
authStatus: AuthenticationStatus.Unlocked, | ||
isAuthenticated: true, | ||
vaultTimeout: 1, // One minute | ||
lastActive: new Date().getTime() - 61 * 1000, // Last active 61 seconds ago | ||
}, | ||
3: { | ||
authStatus: AuthenticationStatus.Unlocked, | ||
isAuthenticated: true, | ||
vaultTimeout: 1, // One minute | ||
lastActive: new Date().getTime() - 120 * 1000, // Last active 2 minutes ago | ||
timeoutAction: VaultTimeoutAction.LogOut, | ||
availableTimeoutActions: [VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut], | ||
}, | ||
4: { | ||
authStatus: AuthenticationStatus.Unlocked, | ||
isAuthenticated: true, | ||
vaultTimeout: 1, // One minute | ||
lastActive: new Date().getTime() - 100 * 1000, // Last active 100 seconds ago | ||
timeoutAction: VaultTimeoutAction.Lock, | ||
availableTimeoutActions: [VaultTimeoutAction.LogOut], | ||
}, | ||
}, | ||
"2" // Treat user 2 as the active user | ||
); | ||
|
||
await vaultTimeoutService.checkVaultTimeout(); | ||
|
||
expectNoAction("1"); | ||
expectUserToHaveLocked("2"); | ||
|
||
// Active users should have additional steps ran | ||
expect(searchService.clearIndex).toHaveBeenCalled(); | ||
expect(folderService.clearCache).toHaveBeenCalled(); | ||
|
||
expectUserToHaveLoggedOut("3"); // They have chosen logout as their action and it's available, log them out | ||
expectUserToHaveLoggedOut("4"); // They may have had lock as their chosen action but it's not available to them so logout | ||
}); | ||
|
||
it("should not lock any accounts as long as a view is known to be open, no matter if they haven't been active since before their timeout", async () => { | ||
platformUtilsService.isViewOpen.mockResolvedValue(true); | ||
|
||
setupAccounts({ | ||
1: { | ||
// Neither of these setup values ever get called | ||
authStatus: AuthenticationStatus.Unlocked, | ||
isAuthenticated: true, | ||
lastActive: new Date().getTime() - 80 * 1000, // Last active 80 seconds ago | ||
vaultTimeout: 1, // Vault timeout of 1 minute ago | ||
}, | ||
}); | ||
|
||
await vaultTimeoutService.checkVaultTimeout(); | ||
|
||
expectNoAction("1"); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters