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.
This commit is contained in:
parent
48c6033792
commit
3451ee8133
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -71,7 +71,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableActions = await firstValueFrom(
|
const availableActions = await firstValueFrom(
|
||||||
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$()
|
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId)
|
||||||
);
|
);
|
||||||
const supportsLock = availableActions.includes(VaultTimeoutAction.Lock);
|
const supportsLock = availableActions.includes(VaultTimeoutAction.Lock);
|
||||||
if (!supportsLock) {
|
if (!supportsLock) {
|
||||||
|
|
Loading…
Reference in New Issue