1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-26410] Update autotype policy to include all org members (#16689)

* PM-26410 use policies$ to apply default behavior to all org members

* linting error, remove unused imports
This commit is contained in:
Daniel Riera
2025-10-01 16:20:03 -04:00
committed by GitHub
parent cae58232e5
commit 5de8a145ec
2 changed files with 164 additions and 32 deletions

View File

@@ -1,8 +1,10 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, take, timeout, TimeoutError } from "rxjs";
import { BehaviorSubject, firstValueFrom, take } from "rxjs";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
@@ -18,10 +20,10 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
let policyService: MockProxy<InternalPolicyService>;
let configService: MockProxy<ConfigService>;
let mockAccountSubject: BehaviorSubject<{ id: UserId } | null>;
let mockAccountSubject: BehaviorSubject<Account | null>;
let mockFeatureFlagSubject: BehaviorSubject<boolean>;
let mockAuthStatusSubject: BehaviorSubject<AuthenticationStatus>;
let mockPolicyAppliesSubject: BehaviorSubject<boolean>;
let mockPoliciesSubject: BehaviorSubject<Policy[]>;
const mockUserId = "user-123" as UserId;
@@ -36,7 +38,7 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
mockAuthStatusSubject = new BehaviorSubject<AuthenticationStatus>(
AuthenticationStatus.Unlocked,
);
mockPolicyAppliesSubject = new BehaviorSubject<boolean>(false);
mockPoliciesSubject = new BehaviorSubject<Policy[]>([]);
accountService = mock<AccountService>();
authService = mock<AuthService>();
@@ -50,9 +52,7 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
authService.authStatusFor$ = jest
.fn()
.mockImplementation((_: UserId) => mockAuthStatusSubject.asObservable());
policyService.policyAppliesToUser$ = jest
.fn()
.mockReturnValue(mockPolicyAppliesSubject.asObservable());
policyService.policies$ = jest.fn().mockReturnValue(mockPoliciesSubject.asObservable());
TestBed.configureTestingModule({
providers: [
@@ -72,7 +72,7 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
mockAccountSubject.complete();
mockFeatureFlagSubject.complete();
mockAuthStatusSubject.complete();
mockPolicyAppliesSubject.complete();
mockPoliciesSubject.complete();
});
describe("autotypeDefaultSetting$", () => {
@@ -82,11 +82,20 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
expect(result).toBeNull();
});
it("should not emit when no active account", async () => {
it("does not emit until an account appears", async () => {
mockAccountSubject.next(null);
await expect(
firstValueFrom(service.autotypeDefaultSetting$.pipe(timeout({ first: 30 }))),
).rejects.toBeInstanceOf(TimeoutError);
mockAccountSubject.next({ id: mockUserId } as Account);
mockAuthStatusSubject.next(AuthenticationStatus.Unlocked);
mockPoliciesSubject.next([
{
type: PolicyType.AutotypeDefaultSetting,
enabled: true,
} as Policy,
]);
const result = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(result).toBe(true);
});
it("should emit null when user is not unlocked", async () => {
@@ -96,34 +105,56 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
});
it("should emit null when no autotype policy exists", async () => {
mockPolicyAppliesSubject.next(false);
mockPoliciesSubject.next([]);
const policy = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(policy).toBeNull();
});
it("should emit true when autotype policy is enabled", async () => {
mockPolicyAppliesSubject.next(true);
mockPoliciesSubject.next([
{
type: PolicyType.AutotypeDefaultSetting,
enabled: true,
} as Policy,
]);
const policyStatus = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(policyStatus).toBe(true);
});
it("should emit false when autotype policy is disabled", async () => {
mockPolicyAppliesSubject.next(false);
it("should emit null when autotype policy is disabled", async () => {
mockPoliciesSubject.next([
{
type: PolicyType.AutotypeDefaultSetting,
enabled: false,
} as Policy,
]);
const policyStatus = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(policyStatus).toBeNull();
});
it("should emit null when autotype policy does not apply", async () => {
mockPolicyAppliesSubject.next(false);
mockPoliciesSubject.next([
{
type: PolicyType.RequireSso,
enabled: true,
} as Policy,
]);
const policy = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(policy).toBeNull();
});
it("should react to authentication status changes", async () => {
mockPoliciesSubject.next([
{
type: PolicyType.AutotypeDefaultSetting,
enabled: true,
} as Policy,
]);
// Expect one emission when unlocked
mockAuthStatusSubject.next(AuthenticationStatus.Unlocked);
const first = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(first).toBeNull();
expect(first).toBe(true);
// Expect null emission when locked
mockAuthStatusSubject.next(AuthenticationStatus.Locked);
@@ -134,33 +165,131 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
it("should react to account changes", async () => {
const newUserId = "user-456" as UserId;
mockPoliciesSubject.next([
{
type: PolicyType.AutotypeDefaultSetting,
enabled: true,
} as Policy,
]);
// First value for original user
const firstValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(firstValue).toBeNull();
expect(firstValue).toBe(true);
// Change account and expect a new emission
mockAccountSubject.next({
id: newUserId,
});
} as Account);
const secondValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(secondValue).toBeNull();
expect(secondValue).toBe(true);
// Verify the auth lookup was switched to the new user
expect(authService.authStatusFor$).toHaveBeenCalledWith(newUserId);
expect(policyService.policies$).toHaveBeenCalledWith(newUserId);
});
it("should react to policy changes", async () => {
mockPolicyAppliesSubject.next(false);
mockPoliciesSubject.next([]);
const nullValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(nullValue).toBeNull();
mockPolicyAppliesSubject.next(true);
mockPoliciesSubject.next([
{
type: PolicyType.AutotypeDefaultSetting,
enabled: true,
} as Policy,
]);
const trueValue = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(trueValue).toBe(true);
mockPolicyAppliesSubject.next(false);
mockPoliciesSubject.next([]);
const nullValueAgain = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(nullValueAgain).toBeNull();
});
it("emits null again if the feature flag turns off after emitting", async () => {
mockPoliciesSubject.next([
{ type: PolicyType.AutotypeDefaultSetting, enabled: true } as Policy,
]);
expect(await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)))).toBe(true);
mockFeatureFlagSubject.next(false);
expect(await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)))).toBeNull();
});
it("replays the latest value to late subscribers", async () => {
mockPoliciesSubject.next([
{ type: PolicyType.AutotypeDefaultSetting, enabled: true } as Policy,
]);
await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
const late = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(late).toBe(true);
});
it("does not re-emit when effective value is unchanged", async () => {
mockAccountSubject.next({ id: mockUserId } as Account);
mockAuthStatusSubject.next(AuthenticationStatus.Unlocked);
const policies = [
{
type: PolicyType.AutotypeDefaultSetting,
enabled: true,
} as Policy,
];
mockPoliciesSubject.next(policies);
const first = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(first).toBe(true);
let emissionCount = 0;
const subscription = service.autotypeDefaultSetting$.subscribe(() => {
emissionCount++;
});
mockPoliciesSubject.next(policies);
await new Promise((resolve) => setTimeout(resolve, 50));
subscription.unsubscribe();
expect(emissionCount).toBe(1);
});
it("does not emit policy values while locked; emits after unlocking", async () => {
mockAuthStatusSubject.next(AuthenticationStatus.Locked);
mockPoliciesSubject.next([
{ type: PolicyType.AutotypeDefaultSetting, enabled: true } as Policy,
]);
expect(await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)))).toBeNull();
mockAuthStatusSubject.next(AuthenticationStatus.Unlocked);
expect(await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)))).toBe(true);
});
it("emits correctly if auth unlocks before policies arrive", async () => {
mockAccountSubject.next({ id: mockUserId } as Account);
mockAuthStatusSubject.next(AuthenticationStatus.Unlocked);
mockPoliciesSubject.next([
{
type: PolicyType.AutotypeDefaultSetting,
enabled: true,
} as Policy,
]);
const result = await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(result).toBe(true);
});
it("wires dependencies with initial user id", async () => {
mockPoliciesSubject.next([
{ type: PolicyType.AutotypeDefaultSetting, enabled: true } as Policy,
]);
await firstValueFrom(service.autotypeDefaultSetting$.pipe(take(1)));
expect(authService.authStatusFor$).toHaveBeenCalledWith(mockUserId);
expect(policyService.policies$).toHaveBeenCalledWith(mockUserId);
});
});
});

View File

@@ -34,7 +34,7 @@ export class DesktopAutotypeDefaultSettingPolicy {
}
return this.accountService.activeAccount$.pipe(
filter((account) => account != null),
filter((account) => account != null && account.id != null),
getUserId,
distinctUntilChanged(),
switchMap((userId) => {
@@ -43,13 +43,16 @@ export class DesktopAutotypeDefaultSettingPolicy {
distinctUntilChanged(),
);
const policy$ = this.policyService
.policyAppliesToUser$(PolicyType.AutotypeDefaultSetting, userId)
.pipe(
map((appliesToUser) => (appliesToUser ? true : null)),
distinctUntilChanged(),
shareReplay({ bufferSize: 1, refCount: true }),
);
const policy$ = this.policyService.policies$(userId).pipe(
map((policies) => {
const autotypePolicy = policies.find(
(policy) => policy.type === PolicyType.AutotypeDefaultSetting && policy.enabled,
);
return autotypePolicy ? true : null;
}),
distinctUntilChanged(),
shareReplay({ bufferSize: 1, refCount: true }),
);
return isUnlocked$.pipe(switchMap((unlocked) => (unlocked ? policy$ : of(null))));
}),