1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-25 09:03:28 +00:00

Merge branch 'main' into km/auto-kdf-qa

This commit is contained in:
Bernd Schoolmann
2025-12-16 11:05:43 +01:00
212 changed files with 17272 additions and 3736 deletions

View File

@@ -6,19 +6,26 @@ import { ReplaySubject, combineLatest, map, Observable } from "rxjs";
import { Account, AccountInfo, AccountService } from "../src/auth/abstractions/account.service";
import { UserId } from "../src/types/guid";
/**
* Creates a mock AccountInfo object with sensible defaults that can be overridden.
* Use this when you need just an AccountInfo object in tests.
*/
export function mockAccountInfoWith(info: Partial<AccountInfo> = {}): AccountInfo {
return {
name: "name",
email: "email",
emailVerified: true,
creationDate: "2024-01-01T00:00:00.000Z",
...info,
};
}
export function mockAccountServiceWith(
userId: UserId,
info: Partial<AccountInfo> = {},
activity: Record<UserId, Date> = {},
): FakeAccountService {
const fullInfo: AccountInfo = {
...info,
...{
name: "name",
email: "email",
emailVerified: true,
},
};
const fullInfo = mockAccountInfoWith(info);
const fullActivity = { [userId]: new Date(), ...activity };
@@ -104,6 +111,10 @@ export class FakeAccountService implements AccountService {
await this.mock.setAccountEmailVerified(userId, emailVerified);
}
async setAccountCreationDate(userId: UserId, creationDate: string): Promise<void> {
await this.mock.setAccountCreationDate(userId, creationDate);
}
async switchAccount(userId: UserId): Promise<void> {
const next =
userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] };
@@ -127,4 +138,5 @@ const loggedOutInfo: AccountInfo = {
name: undefined,
email: "",
emailVerified: false,
creationDate: undefined,
};

View File

@@ -2,14 +2,11 @@ import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
/**
* Holds information about an account for use in the AccountService
* if more information is added, be sure to update the equality method.
*/
export type AccountInfo = {
email: string;
emailVerified: boolean;
name: string | undefined;
creationDate: string | undefined;
};
export type Account = { id: UserId } & AccountInfo;
@@ -75,6 +72,12 @@ export abstract class AccountService {
* @param emailVerified
*/
abstract setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise<void>;
/**
* updates the `accounts$` observable with the creation date for the account.
* @param userId
* @param creationDate
*/
abstract setAccountCreationDate(userId: UserId, creationDate: string): Promise<void>;
/**
* updates the `accounts$` observable with the new VerifyNewDeviceLogin property for the account.
* @param userId

View File

@@ -6,6 +6,7 @@
import { MockProxy, mock } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { mockAccountInfoWith } from "../../../spec/fake-account-service";
import { FakeGlobalState } from "../../../spec/fake-state";
import {
FakeGlobalStateProvider,
@@ -27,7 +28,7 @@ import {
} from "./account.service";
describe("accountInfoEqual", () => {
const accountInfo: AccountInfo = { name: "name", email: "email", emailVerified: true };
const accountInfo = mockAccountInfoWith();
it("compares nulls", () => {
expect(accountInfoEqual(null, null)).toBe(true);
@@ -64,6 +65,23 @@ describe("accountInfoEqual", () => {
expect(accountInfoEqual(accountInfo, same)).toBe(true);
expect(accountInfoEqual(accountInfo, different)).toBe(false);
});
it("compares creationDate", () => {
const same = { ...accountInfo };
const different = { ...accountInfo, creationDate: "2024-12-31T00:00:00.000Z" };
expect(accountInfoEqual(accountInfo, same)).toBe(true);
expect(accountInfoEqual(accountInfo, different)).toBe(false);
});
it("compares undefined creationDate", () => {
const accountWithoutCreationDate = mockAccountInfoWith({ creationDate: undefined });
const same = { ...accountWithoutCreationDate };
const different = { ...accountWithoutCreationDate, creationDate: "2024-01-01T00:00:00.000Z" };
expect(accountInfoEqual(accountWithoutCreationDate, same)).toBe(true);
expect(accountInfoEqual(accountWithoutCreationDate, different)).toBe(false);
});
});
describe("accountService", () => {
@@ -76,7 +94,10 @@ describe("accountService", () => {
let activeAccountIdState: FakeGlobalState<UserId>;
let accountActivityState: FakeGlobalState<Record<UserId, Date>>;
const userId = Utils.newGuid() as UserId;
const userInfo = { email: "email", name: "name", emailVerified: true };
const userInfo = mockAccountInfoWith({
email: "email",
name: "name",
});
beforeEach(() => {
messagingService = mock();
@@ -253,6 +274,56 @@ describe("accountService", () => {
});
});
describe("setCreationDate", () => {
const initialState = { [userId]: userInfo };
beforeEach(() => {
accountsState.stateSubject.next(initialState);
});
it("should update the account with a new creation date", async () => {
const newCreationDate = "2024-12-31T00:00:00.000Z";
await sut.setAccountCreationDate(userId, newCreationDate);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: { ...userInfo, creationDate: newCreationDate },
});
});
it("should not update if the creation date is the same", async () => {
await sut.setAccountCreationDate(userId, userInfo.creationDate);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual(initialState);
});
it("should update from undefined to a defined creation date", async () => {
const accountWithoutCreationDate = mockAccountInfoWith({
...userInfo,
creationDate: undefined,
});
accountsState.stateSubject.next({ [userId]: accountWithoutCreationDate });
const newCreationDate = "2024-06-15T12:30:00.000Z";
await sut.setAccountCreationDate(userId, newCreationDate);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: { ...accountWithoutCreationDate, creationDate: newCreationDate },
});
});
it("should update to a different creation date string format", async () => {
const newCreationDate = "2023-03-15T08:45:30.123Z";
await sut.setAccountCreationDate(userId, newCreationDate);
const currentState = await firstValueFrom(accountsState.state$);
expect(currentState).toEqual({
[userId]: { ...userInfo, creationDate: newCreationDate },
});
});
});
describe("setAccountVerifyNewDeviceLogin", () => {
const initialState = true;
beforeEach(() => {
@@ -294,6 +365,7 @@ describe("accountService", () => {
email: "",
emailVerified: false,
name: undefined,
creationDate: undefined,
},
});
});

View File

@@ -62,6 +62,7 @@ const LOGGED_OUT_INFO: AccountInfo = {
email: "",
emailVerified: false,
name: undefined,
creationDate: undefined,
};
/**
@@ -167,6 +168,10 @@ export class AccountServiceImplementation implements InternalAccountService {
await this.setAccountInfo(userId, { emailVerified });
}
async setAccountCreationDate(userId: UserId, creationDate: string): Promise<void> {
await this.setAccountInfo(userId, { creationDate });
}
async clean(userId: UserId) {
await this.setAccountInfo(userId, LOGGED_OUT_INFO);
await this.removeAccountActivity(userId);

View File

@@ -15,6 +15,7 @@ import {
SystemNotificationEvent,
SystemNotificationsService,
} from "@bitwarden/common/platform/system-notifications/system-notifications.service";
import { mockAccountInfoWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/user-core";
import { AuthRequestAnsweringService } from "./auth-request-answering.service";
@@ -48,14 +49,16 @@ describe("AuthRequestAnsweringService", () => {
// Common defaults
authService.activeAccountStatus$ = of(AuthenticationStatus.Locked);
accountService.activeAccount$ = of({
id: userId,
const accountInfo = mockAccountInfoWith({
email: "user@example.com",
emailVerified: true,
name: "User",
});
accountService.activeAccount$ = of({
id: userId,
...accountInfo,
});
accountService.accounts$ = of({
[userId]: { email: "user@example.com", emailVerified: true, name: "User" },
[userId]: accountInfo,
});
(masterPasswordService.forceSetPasswordReason$ as jest.Mock).mockReturnValue(
of(ForceSetPasswordReason.None),

View File

@@ -10,6 +10,7 @@ import {
makeStaticByteArray,
mockAccountServiceWith,
trackEmissions,
mockAccountInfoWith,
} from "../../../spec";
import { ApiService } from "../../abstractions/api.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
@@ -58,9 +59,10 @@ describe("AuthService", () => {
const accountInfo = {
status: AuthenticationStatus.Unlocked,
id: userId,
email: "email",
emailVerified: false,
name: "name",
...mockAccountInfoWith({
email: "email",
name: "name",
}),
};
beforeEach(() => {
@@ -112,9 +114,10 @@ describe("AuthService", () => {
const accountInfo2 = {
status: AuthenticationStatus.Unlocked,
id: Utils.newGuid() as UserId,
email: "email2",
emailVerified: false,
name: "name2",
...mockAccountInfoWith({
email: "email2",
name: "name2",
}),
};
const emissions = trackEmissions(sut.activeAccountStatus$);
@@ -131,11 +134,13 @@ describe("AuthService", () => {
it("requests auth status for all known users", async () => {
const userId2 = Utils.newGuid() as UserId;
await accountService.addAccount(userId2, {
email: "email2",
emailVerified: false,
name: "name2",
});
await accountService.addAccount(
userId2,
mockAccountInfoWith({
email: "email2",
name: "name2",
}),
);
const mockFn = jest.fn().mockReturnValue(of(AuthenticationStatus.Locked));
sut.authStatusFor$ = mockFn;

View File

@@ -8,12 +8,13 @@ import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management";
import { mockAccountInfoWith } from "../../../spec/fake-account-service";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { UserId } from "../../types/guid";
import { Account, AccountInfo, AccountService } from "../abstractions/account.service";
import { Account, AccountService } from "../abstractions/account.service";
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
@@ -96,11 +97,10 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
const encryptedKey = { encryptedString: "encryptedString" };
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
const user1AccountInfo: AccountInfo = {
const user1AccountInfo = mockAccountInfoWith({
name: "Test User 1",
email: "test1@email.com",
emailVerified: true,
};
});
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
keyService.userKey$.mockReturnValue(of({ key: "key" } as any));

View File

@@ -0,0 +1,37 @@
import { Observable } from "rxjs";
import { UserId } from "@bitwarden/user-core";
/**
* Abstraction for phishing detection settings
*/
export abstract class PhishingDetectionSettingsServiceAbstraction {
/**
* An observable for whether phishing detection is available for the active user account.
*
* Access is granted only when the PhishingDetection feature flag is enabled and
* at least one of the following is true for the active account:
* - the user has a personal premium subscription
* - the user is a member of a Family org (ProductTierType.Families)
* - the user is a member of an Enterprise org with `usePhishingBlocker` enabled
*
* Note: Non-specified organization types (e.g., Team orgs) do not grant access.
*/
abstract readonly available$: Observable<boolean>;
/**
* An observable for whether phishing detection is on for the active user account
*
* This is true when {@link available$} is true and when {@link enabled$} is true
*/
abstract readonly on$: Observable<boolean>;
/**
* An observable for whether phishing detection is enabled
*/
abstract readonly enabled$: Observable<boolean>;
/**
* Sets whether phishing detection is enabled
*
* @param enabled True to enable, false to disable
*/
abstract setEnabled: (userId: UserId, enabled: boolean) => Promise<void>;
}

View File

@@ -0,0 +1,203 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, Subject } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
import { UserId } from "../../../types/guid";
import { PhishingDetectionSettingsService } from "./phishing-detection-settings.service";
describe("PhishingDetectionSettingsService", () => {
// Mock services
let mockAccountService: MockProxy<AccountService>;
let mockBillingService: MockProxy<BillingAccountProfileStateService>;
let mockConfigService: MockProxy<ConfigService>;
let mockOrganizationService: MockProxy<OrganizationService>;
// RxJS Subjects we control in the tests
let activeAccountSubject: BehaviorSubject<Account | null>;
let featureFlagSubject: BehaviorSubject<boolean>;
let premiumStatusSubject: BehaviorSubject<boolean>;
let organizationsSubject: BehaviorSubject<Organization[]>;
let service: PhishingDetectionSettingsService;
let stateProvider: FakeStateProvider;
// Constant mock data
const familyOrg = mock<Organization>({
canAccess: true,
isMember: true,
usersGetPremium: true,
productTierType: ProductTierType.Families,
usePhishingBlocker: true,
});
const teamOrg = mock<Organization>({
canAccess: true,
isMember: true,
usersGetPremium: true,
productTierType: ProductTierType.Teams,
usePhishingBlocker: true,
});
const enterpriseOrg = mock<Organization>({
canAccess: true,
isMember: true,
usersGetPremium: true,
productTierType: ProductTierType.Enterprise,
usePhishingBlocker: true,
});
const mockUserId = "mock-user-id" as UserId;
const account = mock<Account>({ id: mockUserId });
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
beforeEach(() => {
// Initialize subjects
activeAccountSubject = new BehaviorSubject<Account | null>(null);
featureFlagSubject = new BehaviorSubject<boolean>(false);
premiumStatusSubject = new BehaviorSubject<boolean>(false);
organizationsSubject = new BehaviorSubject<Organization[]>([]);
// Default implementations for required functions
mockAccountService = mock<AccountService>();
mockAccountService.activeAccount$ = activeAccountSubject.asObservable();
mockBillingService = mock<BillingAccountProfileStateService>();
mockBillingService.hasPremiumPersonally$.mockReturnValue(premiumStatusSubject.asObservable());
mockConfigService = mock<ConfigService>();
mockConfigService.getFeatureFlag$.mockReturnValue(featureFlagSubject.asObservable());
mockOrganizationService = mock<OrganizationService>();
mockOrganizationService.organizations$.mockReturnValue(organizationsSubject.asObservable());
stateProvider = new FakeStateProvider(accountService);
service = new PhishingDetectionSettingsService(
mockAccountService,
mockBillingService,
mockConfigService,
mockOrganizationService,
stateProvider,
);
});
// Helper to easily get the result of the observable we are testing
const getAccess = () => firstValueFrom(service.available$);
describe("enabled$", () => {
it("should default to true if an account is logged in", async () => {
activeAccountSubject.next(account);
const result = await firstValueFrom(service.enabled$);
expect(result).toBe(true);
});
it("should return the stored value", async () => {
activeAccountSubject.next(account);
await service.setEnabled(mockUserId, false);
const resultDisabled = await firstValueFrom(service.enabled$);
expect(resultDisabled).toBe(false);
await service.setEnabled(mockUserId, true);
const resultEnabled = await firstValueFrom(service.enabled$);
expect(resultEnabled).toBe(true);
});
});
describe("setEnabled", () => {
it("should update the stored value", async () => {
activeAccountSubject.next(account);
await service.setEnabled(mockUserId, false);
let result = await firstValueFrom(service.enabled$);
expect(result).toBe(false);
await service.setEnabled(mockUserId, true);
result = await firstValueFrom(service.enabled$);
expect(result).toBe(true);
});
});
it("returns false immediately when the feature flag is disabled, regardless of other conditions", async () => {
activeAccountSubject.next(account);
premiumStatusSubject.next(true);
organizationsSubject.next([familyOrg]);
featureFlagSubject.next(false);
await expect(getAccess()).resolves.toBe(false);
});
it("returns false if there is no active account present yet", async () => {
activeAccountSubject.next(null); // No active account
featureFlagSubject.next(true); // Flag is on
await expect(getAccess()).resolves.toBe(false);
});
it("returns true when feature flag is enabled and user has premium personally", async () => {
activeAccountSubject.next(account);
featureFlagSubject.next(true);
organizationsSubject.next([]);
premiumStatusSubject.next(true);
await expect(getAccess()).resolves.toBe(true);
});
it("returns true when feature flag is enabled and user is in a Family Organization", async () => {
activeAccountSubject.next(account);
featureFlagSubject.next(true);
premiumStatusSubject.next(false); // User has no personal premium
organizationsSubject.next([familyOrg]);
await expect(getAccess()).resolves.toBe(true);
});
it("returns true when feature flag is enabled and user is in an Enterprise org with phishing blocker enabled", async () => {
activeAccountSubject.next(account);
featureFlagSubject.next(true);
premiumStatusSubject.next(false);
organizationsSubject.next([enterpriseOrg]);
await expect(getAccess()).resolves.toBe(true);
});
it("returns false when user has no access through personal premium or organizations", async () => {
activeAccountSubject.next(account);
featureFlagSubject.next(true);
premiumStatusSubject.next(false);
organizationsSubject.next([teamOrg]); // Team org does not give access
await expect(getAccess()).resolves.toBe(false);
});
it("shares/caches the available$ result between multiple subscribers", async () => {
// Use a plain Subject for this test so we control when the premium observable emits
// and avoid the BehaviorSubject's initial emission which can race with subscriptions.
// Provide the Subject directly as the mock return value for the billing service
const oneTimePremium = new Subject<boolean>();
mockBillingService.hasPremiumPersonally$.mockReturnValueOnce(oneTimePremium.asObservable());
activeAccountSubject.next(account);
featureFlagSubject.next(true);
organizationsSubject.next([]);
const p1 = firstValueFrom(service.available$);
const p2 = firstValueFrom(service.available$);
// Trigger the pipeline
oneTimePremium.next(true);
const [first, second] = await Promise.all([p1, p2]);
expect(first).toBe(true);
expect(second).toBe(true);
// The billing function should have been called at most once due to caching
expect(mockBillingService.hasPremiumPersonally$).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,116 @@
import { combineLatest, Observable, of, switchMap } from "rxjs";
import { catchError, distinctUntilChanged, map, shareReplay } from "rxjs/operators";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { UserId } from "@bitwarden/user-core";
import { PHISHING_DETECTION_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state";
import { PhishingDetectionSettingsServiceAbstraction } from "../abstractions/phishing-detection-settings.service.abstraction";
const ENABLE_PHISHING_DETECTION = new UserKeyDefinition(
PHISHING_DETECTION_DISK,
"enablePhishingDetection",
{
deserializer: (value: boolean) => value ?? true, // Default: enabled
clearOn: [],
},
);
export class PhishingDetectionSettingsService implements PhishingDetectionSettingsServiceAbstraction {
readonly available$: Observable<boolean>;
readonly enabled$: Observable<boolean>;
readonly on$: Observable<boolean>;
constructor(
private accountService: AccountService,
private billingService: BillingAccountProfileStateService,
private configService: ConfigService,
private organizationService: OrganizationService,
private stateProvider: StateProvider,
) {
this.available$ = this.buildAvailablePipeline$().pipe(
distinctUntilChanged(),
shareReplay({ bufferSize: 1, refCount: true }),
);
this.enabled$ = this.buildEnabledPipeline$().pipe(
distinctUntilChanged(),
shareReplay({ bufferSize: 1, refCount: true }),
);
this.on$ = combineLatest([this.available$, this.enabled$]).pipe(
map(([available, enabled]) => available && enabled),
distinctUntilChanged(),
shareReplay({ bufferSize: 1, refCount: true }),
);
}
async setEnabled(userId: UserId, enabled: boolean): Promise<void> {
await this.stateProvider.getUser(userId, ENABLE_PHISHING_DETECTION).update(() => enabled);
}
/**
* Builds the observable pipeline to determine if phishing detection is available to the user
*
* @returns An observable pipeline that determines if phishing detection is available
*/
private buildAvailablePipeline$(): Observable<boolean> {
return combineLatest([
this.accountService.activeAccount$,
this.configService.getFeatureFlag$(FeatureFlag.PhishingDetection),
]).pipe(
switchMap(([account, featureEnabled]) => {
if (!account || !featureEnabled) {
return of(false);
}
return combineLatest([
this.billingService.hasPremiumPersonally$(account.id).pipe(catchError(() => of(false))),
this.organizationService.organizations$(account.id).pipe(catchError(() => of([]))),
]).pipe(
map(([hasPremium, organizations]) => hasPremium || this.orgGrantsAccess(organizations)),
catchError(() => of(false)),
);
}),
);
}
/**
* Builds the observable pipeline to determine if phishing detection is enabled by the user
*
* @returns True if phishing detection is enabled for the active user
*/
private buildEnabledPipeline$(): Observable<boolean> {
return this.accountService.activeAccount$.pipe(
switchMap((account) => {
if (!account) {
return of(false);
}
return this.stateProvider.getUserState$(ENABLE_PHISHING_DETECTION, account.id);
}),
map((enabled) => enabled ?? true),
);
}
/**
* Determines if any of the user's organizations grant access to phishing detection
*
* @param organizations The organizations the user is a member of
* @returns True if any organization grants access to phishing detection
*/
private orgGrantsAccess(organizations: Organization[]): boolean {
return organizations.some((org) => {
if (!org.canAccess || !org.isMember || !org.usersGetPremium) {
return false;
}
return (
org.productTierType === ProductTierType.Families ||
(org.productTierType === ProductTierType.Enterprise && org.usePhishingBlocker)
);
});
}
}

View File

@@ -10,3 +10,5 @@ export {
VaultTimeoutNumberType,
VaultTimeoutStringType,
} from "./types/vault-timeout.type";
// Only used by desktop's electron-key.service.spec.ts test
export { VAULT_TIMEOUT } from "./services/vault-timeout-settings.state";

View File

@@ -13,6 +13,7 @@ import {
shareReplay,
switchMap,
tap,
concatMap,
} from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
@@ -150,7 +151,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
return from(
this.determineVaultTimeout(currentVaultTimeout, maxSessionTimeoutPolicyData),
).pipe(
tap((vaultTimeout: VaultTimeout) => {
concatMap(async (vaultTimeout: VaultTimeout) => {
this.logService.debug(
"[VaultTimeoutSettingsService] Determined vault timeout is %o for user id %s",
vaultTimeout,
@@ -159,8 +160,9 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA
// As a side effect, set the new value determined by determineVaultTimeout into state if it's different from the current
if (vaultTimeout !== currentVaultTimeout) {
return this.stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, userId);
await this.stateProvider.setUserState(VAULT_TIMEOUT, vaultTimeout, userId);
}
return vaultTimeout;
}),
catchError((error: unknown) => {
// Protect outer observable from canceling on error by catching and returning EMPTY

View File

@@ -7,7 +7,7 @@ import { BehaviorSubject, from, of } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { LockService, LogoutService } from "@bitwarden/auth/common";
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
import { FakeAccountService, mockAccountServiceWith, mockAccountInfoWith } from "../../../../spec";
import { AccountInfo } from "../../../auth/abstractions/account.service";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
@@ -109,19 +109,19 @@ describe("VaultTimeoutService", () => {
if (globalSetups?.userId) {
accountService.activeAccountSubject.next({
id: globalSetups.userId as UserId,
email: null,
emailVerified: false,
name: null,
...mockAccountInfoWith({
email: null,
name: null,
}),
});
}
accountService.accounts$ = of(
Object.entries(accounts).reduce(
(agg, [id]) => {
agg[id] = {
agg[id] = mockAccountInfoWith({
email: "",
emailVerified: true,
name: "",
};
});
return agg;
},
{} as Record<string, AccountInfo>,

View File

@@ -7,6 +7,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { mockAccountInfoWith } from "../../../../spec";
import { AccountService } from "../../../auth/abstractions/account.service";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
@@ -163,9 +164,10 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
} else {
activeUserAccount$.next({
id: userId,
email: "email",
name: "Test Name",
emailVerified: true,
...mockAccountInfoWith({
email: "email",
name: "Test Name",
}),
});
}
}
@@ -174,7 +176,10 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
const currentAccounts = (userAccounts$.getValue() as Record<string, any>) ?? {};
userAccounts$.next({
...currentAccounts,
[userId]: { email: "email", name: "Test Name", emailVerified: true },
[userId]: mockAccountInfoWith({
email: "email",
name: "Test Name",
}),
} as any);
}

View File

@@ -8,7 +8,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
import { awaitAsync } from "../../../../spec";
import { awaitAsync, mockAccountInfoWith } from "../../../../spec";
import { Matrix } from "../../../../spec/matrix";
import { AccountService } from "../../../auth/abstractions/account.service";
import { AuthService } from "../../../auth/abstractions/auth.service";
@@ -139,11 +139,18 @@ describe("NotificationsService", () => {
activeAccount.next(null);
accounts.next({} as any);
} else {
activeAccount.next({ id: userId, email: "email", name: "Test Name", emailVerified: true });
const accountInfo = mockAccountInfoWith({
email: "email",
name: "Test Name",
});
activeAccount.next({
id: userId,
...accountInfo,
});
const current = (accounts.getValue() as Record<string, any>) ?? {};
accounts.next({
...current,
[userId]: { email: "email", name: "Test Name", emailVerified: true },
[userId]: accountInfo,
} as any);
}
}
@@ -349,7 +356,13 @@ describe("NotificationsService", () => {
describe("processNotification", () => {
beforeEach(async () => {
appIdService.getAppId.mockResolvedValue("test-app-id");
activeAccount.next({ id: mockUser1, email: "email", name: "Test Name", emailVerified: true });
activeAccount.next({
id: mockUser1,
...mockAccountInfoWith({
email: "email",
name: "Test Name",
}),
});
});
describe("NotificationType.LogOut", () => {

View File

@@ -1,6 +1,6 @@
import { firstValueFrom } from "rxjs";
import { FakeStateProvider, awaitAsync } from "../../../spec";
import { FakeStateProvider, awaitAsync, mockAccountInfoWith } from "../../../spec";
import { FakeAccountService } from "../../../spec/fake-account-service";
import { UserId } from "../../types/guid";
import { CloudRegion, Region } from "../abstractions/environment.service";
@@ -28,16 +28,14 @@ describe("EnvironmentService", () => {
beforeEach(async () => {
accountService = new FakeAccountService({
[testUser]: {
[testUser]: mockAccountInfoWith({
name: "name",
email: "email",
emailVerified: false,
},
[alternateTestUser]: {
}),
[alternateTestUser]: mockAccountInfoWith({
name: "name",
email: "email",
emailVerified: false,
},
}),
});
stateProvider = new FakeStateProvider(accountService);
@@ -47,9 +45,10 @@ describe("EnvironmentService", () => {
const switchUser = async (userId: UserId) => {
accountService.activeAccountSubject.next({
id: userId,
email: "test@example.com",
name: `Test Name ${userId}`,
emailVerified: false,
...mockAccountInfoWith({
email: "test@example.com",
name: `Test Name ${userId}`,
}),
});
await awaitAsync();
};

View File

@@ -3,7 +3,7 @@ import { TextEncoder } from "util";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { mockAccountServiceWith } from "../../../../spec";
import { mockAccountServiceWith, mockAccountInfoWith } from "../../../../spec";
import { Account } from "../../../auth/abstractions/account.service";
import { CipherId, UserId } from "../../../types/guid";
import { CipherService, EncryptionContext } from "../../../vault/abstractions/cipher.service";
@@ -40,9 +40,10 @@ describe("FidoAuthenticatorService", () => {
const userId = "testId" as UserId;
const activeAccountSubject = new BehaviorSubject<Account | null>({
id: userId,
email: "test@example.com",
emailVerified: true,
name: "Test User",
...mockAccountInfoWith({
email: "test@example.com",
name: "Test User",
}),
});
let cipherService!: MockProxy<CipherService>;

View File

@@ -12,9 +12,9 @@ import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
mockAccountInfoWith,
} from "../../../../spec";
import { ApiService } from "../../../abstractions/api.service";
import { AccountInfo } from "../../../auth/abstractions/account.service";
import { EncryptedString } from "../../../key-management/crypto/models/enc-string";
import { UserId } from "../../../types/guid";
import { UserKey } from "../../../types/key";
@@ -92,7 +92,10 @@ describe("DefaultSdkService", () => {
.calledWith(userId)
.mockReturnValue(new BehaviorSubject(mock<Environment>()));
accountService.accounts$ = of({
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo,
[userId]: mockAccountInfoWith({
email: "email",
name: "name",
}),
});
kdfConfigService.getKdfConfig$
.calledWith(userId)

View File

@@ -8,9 +8,9 @@ import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
mockAccountInfoWith,
} from "../../../../spec";
import { ApiService } from "../../../abstractions/api.service";
import { AccountInfo } from "../../../auth/abstractions/account.service";
import { UserId } from "../../../types/guid";
import { ConfigService } from "../../abstractions/config/config.service";
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
@@ -76,7 +76,10 @@ describe("DefaultRegisterSdkService", () => {
.calledWith(userId)
.mockReturnValue(new BehaviorSubject(mock<Environment>()));
accountService.accounts$ = of({
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo,
[userId]: mockAccountInfoWith({
email: "email",
name: "name",
}),
});
});
@@ -125,7 +128,10 @@ describe("DefaultRegisterSdkService", () => {
it("destroys the internal SDK client when the account is removed (logout)", async () => {
const accounts$ = new BehaviorSubject({
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo,
[userId]: mockAccountInfoWith({
email: "email",
name: "name",
}),
});
accountService.accounts$ = accounts$;

View File

@@ -272,6 +272,7 @@ export class DefaultSyncService extends CoreSyncService {
await this.tokenService.setSecurityStamp(response.securityStamp, response.id);
await this.accountService.setAccountEmailVerified(response.id, response.emailVerified);
await this.accountService.setAccountVerifyNewDeviceLogin(response.id, response.verifyDevices);
await this.accountService.setAccountCreationDate(response.id, response.creationDate);
await this.billingAccountProfileStateService.setHasPremium(
response.premiumPersonally,

View File

@@ -6,6 +6,7 @@ import { ObservedValueOf, of } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { UserId } from "@bitwarden/user-core";
import { mockAccountInfoWith } from "../../spec";
import { AccountService } from "../auth/abstractions/account.service";
import { TokenService } from "../auth/abstractions/token.service";
import { DeviceType } from "../enums";
@@ -55,9 +56,10 @@ describe("ApiService", () => {
accountService.activeAccount$ = of({
id: testActiveUser,
email: "user1@example.com",
emailVerified: true,
name: "Test Name",
...mockAccountInfoWith({
email: "user1@example.com",
name: "Test Name",
}),
} satisfies ObservedValueOf<AccountService["activeAccount$"]>);
httpOperations = mock();

View File

@@ -1,7 +1,12 @@
import { mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { FakeAccountService, FakeStateProvider, awaitAsync } from "../../../spec";
import {
FakeAccountService,
FakeStateProvider,
awaitAsync,
mockAccountInfoWith,
} from "../../../spec";
import { Account } from "../../auth/abstractions/account.service";
import { EXTENSION_DISK, UserKeyDefinition } from "../../platform/state";
import { UserId } from "../../types/guid";
@@ -21,9 +26,10 @@ import { SimpleLogin } from "./vendor/simplelogin";
const SomeUser = "some user" as UserId;
const SomeAccount = {
id: SomeUser,
email: "someone@example.com",
emailVerified: true,
name: "Someone",
...mockAccountInfoWith({
email: "someone@example.com",
name: "Someone",
}),
};
const SomeAccount$ = new BehaviorSubject<Account>(SomeAccount);

View File

@@ -1,3 +1,5 @@
# Tools Vendor Integration
This module defines interfaces and helpers for creating vendor integration sites.
## RPC

View File

@@ -11,6 +11,7 @@ import {
FakeStateProvider,
awaitAsync,
mockAccountServiceWith,
mockAccountInfoWith,
} from "../../../../spec";
import { KeyGenerationService } from "../../../key-management/crypto";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
@@ -71,9 +72,10 @@ describe("SendService", () => {
accountService.activeAccountSubject.next({
id: mockUserId,
email: "email",
emailVerified: false,
name: "name",
...mockAccountInfoWith({
email: "email",
name: "name",
}),
});
// Initial encrypted state

View File

@@ -6,6 +6,7 @@ import {
awaitAsync,
FakeAccountService,
FakeStateProvider,
mockAccountInfoWith,
ObservableTracker,
} from "../../../spec";
import { Account } from "../../auth/abstractions/account.service";
@@ -23,17 +24,19 @@ import { UserStateSubject } from "./user-state-subject";
const SomeUser = "some user" as UserId;
const SomeAccount = {
id: SomeUser,
email: "someone@example.com",
emailVerified: true,
name: "Someone",
...mockAccountInfoWith({
email: "someone@example.com",
name: "Someone",
}),
};
const SomeAccount$ = new BehaviorSubject<Account>(SomeAccount);
const SomeOtherAccount = {
id: "some other user" as UserId,
email: "someone@example.com",
emailVerified: true,
name: "Someone",
...mockAccountInfoWith({
email: "someone@example.com",
name: "Someone",
}),
};
type TestType = { foo: string };