1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00
Files
browser/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts
Jared Snider 66f5d90803 PM-5501 - VaultTimeoutSettingsSvc State Provider Migration - Small bugfixes (#9164)
* PM-5501 - VaultTimeoutSettingsSvc - fix setVaultTimeoutOptions condition which needed to use never instead of null.

* PM-5501 - Fix browser and desktop not showing the never lock warning

* PM-5501 - Use true equality.
2024-05-13 17:04:26 -04:00

358 lines
16 KiB
TypeScript

import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, map, of } from "rxjs";
import {
PinServiceAbstraction,
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { FakeAccountService, mockAccountServiceWith, FakeStateProvider } from "../../../spec";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { Policy } from "../../admin-console/models/domain/policy";
import { TokenService } from "../../auth/abstractions/token.service";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { LogService } from "../../platform/abstractions/log.service";
import { BiometricStateService } from "../../platform/biometrics/biometric-state.service";
import {
VAULT_TIMEOUT,
VAULT_TIMEOUT_ACTION,
} from "../../services/vault-timeout/vault-timeout-settings.state";
import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type";
import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service";
describe("VaultTimeoutSettingsService", () => {
let accountService: FakeAccountService;
let pinService: MockProxy<PinServiceAbstraction>;
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
let cryptoService: MockProxy<CryptoService>;
let tokenService: MockProxy<TokenService>;
let policyService: MockProxy<PolicyService>;
const biometricStateService = mock<BiometricStateService>();
let vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction;
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
const mockUserId = Utils.newGuid() as UserId;
let stateProvider: FakeStateProvider;
let logService: MockProxy<LogService>;
beforeEach(() => {
accountService = mockAccountServiceWith(mockUserId);
pinService = mock<PinServiceAbstraction>();
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
cryptoService = mock<CryptoService>();
tokenService = mock<TokenService>();
policyService = mock<PolicyService>();
userDecryptionOptionsSubject = new BehaviorSubject(null);
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
userDecryptionOptionsService.hasMasterPassword$ = userDecryptionOptionsSubject.pipe(
map((options) => options?.hasMasterPassword ?? false),
);
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
userDecryptionOptionsSubject,
);
accountService = mockAccountServiceWith(mockUserId);
stateProvider = new FakeStateProvider(accountService);
logService = mock<LogService>();
const defaultVaultTimeout: VaultTimeout = 15; // default web vault timeout
vaultTimeoutSettingsService = createVaultTimeoutSettingsService(defaultVaultTimeout);
biometricStateService.biometricUnlockEnabled$ = of(false);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("availableVaultTimeoutActions$", () => {
it("always returns LogOut", async () => {
const result = await firstValueFrom(
vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
);
expect(result).toContain(VaultTimeoutAction.LogOut);
});
it("contains Lock when the user has a master password", async () => {
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
const result = await firstValueFrom(
vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
);
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("contains Lock when the user has either a persistent or ephemeral PIN configured", async () => {
pinService.isPinSet.mockResolvedValue(true);
const result = await firstValueFrom(
vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
);
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("contains Lock when the user has biometrics configured", async () => {
biometricStateService.biometricUnlockEnabled$ = of(true);
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true);
const result = await firstValueFrom(
vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
);
expect(result).toContain(VaultTimeoutAction.Lock);
});
it("not contains Lock when the user does not have a master password, PIN, or biometrics", async () => {
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: false }));
pinService.isPinSet.mockResolvedValue(false);
biometricStateService.biometricUnlockEnabled$ = of(false);
const result = await firstValueFrom(
vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
);
expect(result).not.toContain(VaultTimeoutAction.Lock);
});
});
describe("getVaultTimeoutActionByUserId$", () => {
it("should throw an error if no user id is provided", async () => {
expect(() => vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(null)).toThrow(
"User id required. Cannot get vault timeout action.",
);
});
describe("given the user has a master password", () => {
it.each`
policy | userPreference | expected
${null} | ${null} | ${VaultTimeoutAction.Lock}
${null} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.LogOut}
${VaultTimeoutAction.LogOut} | ${null} | ${VaultTimeoutAction.LogOut}
${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut}
`(
"returns $expected when policy is $policy, and user preference is $userPreference",
async ({ policy, userPreference, expected }) => {
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
policyService.getAll$.mockReturnValue(
of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])),
);
await stateProvider.setUserState(VAULT_TIMEOUT_ACTION, userPreference, mockUserId);
const result = await firstValueFrom(
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(mockUserId),
);
expect(result).toBe(expected);
},
);
});
describe("given the user does not have a master password", () => {
it.each`
hasPinUnlock | hasBiometricUnlock | policy | userPreference | expected
${false} | ${false} | ${null} | ${null} | ${VaultTimeoutAction.LogOut}
${false} | ${false} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut}
${false} | ${false} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.LogOut}
${false} | ${true} | ${null} | ${null} | ${VaultTimeoutAction.Lock}
${false} | ${true} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.Lock}
${false} | ${true} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.Lock}
${false} | ${true} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock}
${true} | ${false} | ${null} | ${null} | ${VaultTimeoutAction.Lock}
${true} | ${false} | ${null} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.Lock}
${true} | ${false} | ${VaultTimeoutAction.Lock} | ${null} | ${VaultTimeoutAction.Lock}
${true} | ${false} | ${VaultTimeoutAction.Lock} | ${VaultTimeoutAction.LogOut} | ${VaultTimeoutAction.Lock}
`(
"returns $expected when policy is $policy, has PIN unlock method: $hasPinUnlock or Biometric unlock method: $hasBiometricUnlock, and user preference is $userPreference",
async ({ hasPinUnlock, hasBiometricUnlock, policy, userPreference, expected }) => {
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(hasBiometricUnlock);
pinService.isPinSet.mockResolvedValue(hasPinUnlock);
userDecryptionOptionsSubject.next(
new UserDecryptionOptions({ hasMasterPassword: false }),
);
policyService.getAll$.mockReturnValue(
of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])),
);
await stateProvider.setUserState(VAULT_TIMEOUT_ACTION, userPreference, mockUserId);
const result = await firstValueFrom(
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(mockUserId),
);
expect(result).toBe(expected);
},
);
});
});
describe("getVaultTimeoutByUserId$", () => {
it("should throw an error if no user id is provided", async () => {
expect(() => vaultTimeoutSettingsService.getVaultTimeoutByUserId$(null)).toThrow(
"User id required. Cannot get vault timeout.",
);
});
it.each([
// policy, vaultTimeout, expected
[null, null, 15], // no policy, no vault timeout, falls back to default
[30, 90, 30], // policy overrides vault timeout
[30, 15, 15], // policy doesn't override vault timeout when it's within acceptable range
[90, VaultTimeoutStringType.Never, 90], // policy overrides vault timeout when it's "never"
[null, VaultTimeoutStringType.Never, VaultTimeoutStringType.Never], // no policy, persist "never" vault timeout
[90, 0, 0], // policy doesn't override vault timeout when it's 0 (immediate)
[null, 0, 0], // no policy, persist 0 (immediate) vault timeout
[90, VaultTimeoutStringType.OnRestart, 90], // policy overrides vault timeout when it's "onRestart"
[null, VaultTimeoutStringType.OnRestart, VaultTimeoutStringType.OnRestart], // no policy, persist "onRestart" vault timeout
[90, VaultTimeoutStringType.OnLocked, 90], // policy overrides vault timeout when it's "onLocked"
[null, VaultTimeoutStringType.OnLocked, VaultTimeoutStringType.OnLocked], // no policy, persist "onLocked" vault timeout
[90, VaultTimeoutStringType.OnSleep, 90], // policy overrides vault timeout when it's "onSleep"
[null, VaultTimeoutStringType.OnSleep, VaultTimeoutStringType.OnSleep], // no policy, persist "onSleep" vault timeout
[90, VaultTimeoutStringType.OnIdle, 90], // policy overrides vault timeout when it's "onIdle"
[null, VaultTimeoutStringType.OnIdle, VaultTimeoutStringType.OnIdle], // no policy, persist "onIdle" vault timeout
])(
"when policy is %s, and vault timeout is %s, returns %s",
async (policy, vaultTimeout, expected) => {
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
policyService.getAll$.mockReturnValue(
of(policy === null ? [] : ([{ data: { minutes: policy } }] as unknown as Policy[])),
);
await stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, mockUserId);
const result = await firstValueFrom(
vaultTimeoutSettingsService.getVaultTimeoutByUserId$(mockUserId),
);
expect(result).toBe(expected);
},
);
});
describe("setVaultTimeoutOptions", () => {
const mockAccessToken = "mockAccessToken";
const mockRefreshToken = "mockRefreshToken";
const mockClientId = "mockClientId";
const mockClientSecret = "mockClientSecret";
it("should throw an error if no user id is provided", async () => {
// note: don't await here because we want to test the error
const result = vaultTimeoutSettingsService.setVaultTimeoutOptions(null, null, null);
// Assert
await expect(result).rejects.toThrow("User id required. Cannot set vault timeout settings.");
});
it("should not throw an error if 0 is provided as the timeout", async () => {
// note: don't await here because we want to test the error
const result = vaultTimeoutSettingsService.setVaultTimeoutOptions(
mockUserId,
0,
VaultTimeoutAction.Lock,
);
// Assert
await expect(result).resolves.not.toThrow();
});
it("should throw an error if a null vault timeout is provided", async () => {
// note: don't await here because we want to test the error
const result = vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, null, null);
// Assert
await expect(result).rejects.toThrow("Vault Timeout cannot be null.");
});
it("should throw an error if a null vault timout action is provided", async () => {
// note: don't await here because we want to test the error
const result = vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, 30, null);
// Assert
await expect(result).rejects.toThrow("Vault Timeout Action cannot be null.");
});
it("should set the vault timeout options for the given user", async () => {
// Arrange
tokenService.getAccessToken.mockResolvedValue(mockAccessToken);
tokenService.getRefreshToken.mockResolvedValue(mockRefreshToken);
tokenService.getClientId.mockResolvedValue(mockClientId);
tokenService.getClientSecret.mockResolvedValue(mockClientSecret);
const action = VaultTimeoutAction.Lock;
const timeout = 30;
// Act
await vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, timeout, action);
// Assert
expect(tokenService.setTokens).toHaveBeenCalledWith(
mockAccessToken,
action,
timeout,
mockRefreshToken,
[mockClientId, mockClientSecret],
);
expect(
stateProvider.singleUser.getFake(mockUserId, VAULT_TIMEOUT_ACTION).nextMock,
).toHaveBeenCalledWith(action);
expect(
stateProvider.singleUser.getFake(mockUserId, VAULT_TIMEOUT).nextMock,
).toHaveBeenCalledWith(timeout);
expect(cryptoService.refreshAdditionalKeys).toHaveBeenCalled();
});
it("should clear the tokens when the timeout is not never and the action is log out", async () => {
// Arrange
const action = VaultTimeoutAction.LogOut;
const timeout = 30;
// Act
await vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, timeout, action);
// Assert
expect(tokenService.clearTokens).toHaveBeenCalled();
});
it("should not clear the tokens when the timeout is never and the action is log out", async () => {
// Arrange
const action = VaultTimeoutAction.LogOut;
const timeout = VaultTimeoutStringType.Never;
// Act
await vaultTimeoutSettingsService.setVaultTimeoutOptions(mockUserId, timeout, action);
// Assert
expect(tokenService.clearTokens).not.toHaveBeenCalled();
});
});
function createVaultTimeoutSettingsService(
defaultVaultTimeout: VaultTimeout,
): VaultTimeoutSettingsService {
return new VaultTimeoutSettingsService(
accountService,
pinService,
userDecryptionOptionsService,
cryptoService,
tokenService,
policyService,
biometricStateService,
stateProvider,
logService,
defaultVaultTimeout,
);
}
});