1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-20 19:34:03 +00:00

moved service to libs

This commit is contained in:
jng
2025-08-15 16:11:39 -04:00
parent 4d2163e950
commit c70dfa989a
4 changed files with 9 additions and 6 deletions

View File

@@ -1,3 +1,7 @@
export {
AtRiskPasswordCalloutService,
AtRiskPasswordCalloutData,
} from "./services/at-risk-password-callout.service";
export { PasswordRepromptService } from "./services/password-reprompt.service";
export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service";
export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive";

View File

@@ -0,0 +1,112 @@
import { TestBed } from "@angular/core/testing";
import { firstValueFrom, of } from "rxjs";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
import { StateProvider } from "@bitwarden/state";
import { UserId } from "@bitwarden/user-core";
import { FakeSingleUserState } from "../../../common/spec/fake-state";
import {
AT_RISK_PASSWORD_CALLOUT_KEY,
AtRiskPasswordCalloutData,
AtRiskPasswordCalloutService,
} from "./at-risk-password-callout.service";
const fakeUserState = () =>
({
update: jest.fn().mockResolvedValue(undefined),
}) as unknown as FakeSingleUserState<AtRiskPasswordCalloutData>;
class MockCipherView {
constructor(
public id: string,
private deleted: boolean,
) {}
get isDeleted() {
return this.deleted;
}
}
describe("AtRiskPasswordCalloutService", () => {
let service: AtRiskPasswordCalloutService;
const mockTaskService = { pendingTasks$: jest.fn() };
const mockCipherService = { cipherViews$: jest.fn() };
const mockStateProvider = { getUser: jest.fn().mockReturnValue(fakeUserState()) };
const userId: UserId = "user1" as UserId;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AtRiskPasswordCalloutService,
{
provide: TaskService,
useValue: mockTaskService,
},
{
provide: CipherService,
useValue: mockCipherService,
},
{
provide: StateProvider,
useValue: mockStateProvider,
},
],
});
service = TestBed.inject(AtRiskPasswordCalloutService);
});
it("should be created", () => {
expect(service).toBeTruthy();
});
describe("pendingTasks$", () => {
it("filters tasks to only items with UpdateAtRiskCredential types and not deleted cipher", async () => {
const tasks: SecurityTask[] = [
{ id: "t1", cipherId: "c1", type: SecurityTaskType.UpdateAtRiskCredential } as any,
{ id: "t2", cipherId: "c2", type: null } as any,
{ id: "t3", cipherId: "nope", type: SecurityTaskType.UpdateAtRiskCredential } as any,
{ id: "t4", cipherId: "c3", type: SecurityTaskType.UpdateAtRiskCredential } as any,
];
const ciphers = [
new MockCipherView("c1", false),
new MockCipherView("c2", false),
new MockCipherView("c3", true),
];
jest.spyOn(mockTaskService, "pendingTasks$").mockReturnValue(of(tasks));
jest.spyOn(mockCipherService, "cipherViews$").mockReturnValue(of(ciphers));
const result = await firstValueFrom(service.pendingTasks$(userId));
expect(result.map((t) => t.id)).toEqual(["t1"]);
});
});
describe("atRiskPasswordState", () => {
it("calls stateProvider.getUser with proper values", () => {
service.atRiskPasswordState(userId);
expect(mockStateProvider.getUser).toHaveBeenCalledWith(userId, AT_RISK_PASSWORD_CALLOUT_KEY);
});
});
describe("updateAtRiskPasswordState", () => {
it("calls update on the returned SingleUserState", () => {
const returnedState = fakeUserState();
mockStateProvider.getUser.mockReturnValue(returnedState);
const updateObj: AtRiskPasswordCalloutData = {
hadPendingTasks: true,
showTasksCompleteBanner: false,
tasksBannerDismissed: false,
};
service.updateAtRiskPasswordState(userId, updateObj);
expect(returnedState.update).toHaveBeenCalledWith(expect.any(Function));
const updater = (returnedState.update as jest.Mock).mock.calls[0][0];
expect(updater({})).toEqual(updateObj);
});
});
});

View File

@@ -0,0 +1,63 @@
import { Injectable } from "@angular/core";
import { combineLatest, map, Observable } from "rxjs";
import {
SingleUserState,
StateProvider,
UserKeyDefinition,
VAULT_AT_RISK_PASSWORDS_DISK,
} from "@bitwarden/common/platform/state";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
import { UserId } from "@bitwarden/user-core";
export type AtRiskPasswordCalloutData = {
hadPendingTasks: boolean;
showTasksCompleteBanner: boolean;
tasksBannerDismissed: boolean;
};
export const AT_RISK_PASSWORD_CALLOUT_KEY = new UserKeyDefinition<AtRiskPasswordCalloutData>(
VAULT_AT_RISK_PASSWORDS_DISK,
"atRiskPasswords",
{
deserializer: (jsonData) => jsonData,
clearOn: ["logout", "lock"],
},
);
@Injectable()
export class AtRiskPasswordCalloutService {
constructor(
private taskService: TaskService,
private cipherService: CipherService,
private stateProvider: StateProvider,
) {}
pendingTasks$(userId: UserId): Observable<SecurityTask[]> {
return combineLatest([
this.taskService.pendingTasks$(userId),
this.cipherService.cipherViews$(userId),
]).pipe(
map(([tasks, ciphers]) => {
return tasks.filter((t: SecurityTask) => {
const associatedCipher = ciphers.find((c) => c.id === t.cipherId);
return (
t.type === SecurityTaskType.UpdateAtRiskCredential &&
associatedCipher &&
!associatedCipher.isDeleted
);
});
}),
);
}
atRiskPasswordState(userId: UserId): SingleUserState<AtRiskPasswordCalloutData> {
return this.stateProvider.getUser(userId, AT_RISK_PASSWORD_CALLOUT_KEY);
}
updateAtRiskPasswordState(userId: UserId, updatedState: AtRiskPasswordCalloutData): void {
void this.atRiskPasswordState(userId).update(() => updatedState);
}
}