1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-19 02:44:01 +00:00
Files
browser/libs/angular/src/key-management/encrypted-migration/encrypted-migrations-scheduler.service.spec.ts
Todd Martin b3994e2fc5 feat(accounts): Add creationDate of account to AccountInfo
* Add creationDate of account to AccountInfo

* Added initialization of creationDate.

* Removed extra changes.

* Fixed tests to initialize creation date

* Added helper method to abstract account initialization in tests.

* More test updates.

* Linting

* Additional test fixes.

* Fixed spec reference

* Fixed imports

* Linting.

* Fixed browser test.

* Modified tsconfig to reference spec file.

* Fixed import.

* Removed dependency on os.  This is necessary so that the @bitwarden/common/spec lib package can be referenced in tests without node.

* Revert "Removed dependency on os.  This is necessary so that the @bitwarden/common/spec lib package can be referenced in tests without node."

This reverts commit 669f6557b6.

* Updated stories to hard-code new field.

* Removed changes to tsconfig

* Revert "Removed changes to tsconfig"

This reverts commit b7d916e8dc.
2026-02-17 09:25:12 -08:00

268 lines
11 KiB
TypeScript

import { Router } from "@angular/router";
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EncryptedMigrator } from "@bitwarden/common/key-management/encrypted-migrator/encrypted-migrator.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
import { SyncService } from "@bitwarden/common/platform/sync";
import { mockAccountInfoWith, FakeAccountService } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService, ToastService } from "@bitwarden/components";
import { LogService } from "@bitwarden/logging";
import {
DefaultEncryptedMigrationsSchedulerService,
ENCRYPTED_MIGRATION_DISMISSED,
} from "./encrypted-migrations-scheduler.service";
import { PromptMigrationPasswordComponent } from "./prompt-migration-password.component";
const SomeUser = "SomeUser" as UserId;
const AnotherUser = "SomeOtherUser" as UserId;
const accounts = {
[SomeUser]: mockAccountInfoWith({
name: "some user",
email: "some.user@example.com",
}),
[AnotherUser]: mockAccountInfoWith({
name: "some other user",
email: "some.other.user@example.com",
}),
};
describe("DefaultEncryptedMigrationsSchedulerService", () => {
let service: DefaultEncryptedMigrationsSchedulerService;
const mockAccountService = new FakeAccountService(accounts);
const mockAuthService = mock<AuthService>();
const mockEncryptedMigrator = mock<EncryptedMigrator>();
const mockStateProvider = mock<StateProvider>();
const mockSyncService = mock<SyncService>();
const mockDialogService = mock<DialogService>();
const mockToastService = mock<ToastService>();
const mockI18nService = mock<I18nService>();
const mockLogService = mock<LogService>();
const mockRouter = mock<Router>();
const mockUserId = "test-user-id" as UserId;
const mockMasterPassword = "test-master-password";
const createMockUserState = <T>(value: T): jest.Mocked<SingleUserState<T>> =>
({
state$: of(value),
userId: mockUserId,
update: jest.fn(),
combinedState$: of([mockUserId, value]),
}) as any;
beforeEach(() => {
const mockDialogRef = {
closed: of(mockMasterPassword),
};
jest.spyOn(PromptMigrationPasswordComponent, "open").mockReturnValue(mockDialogRef as any);
mockI18nService.t.mockReturnValue("translated_migrationsFailed");
(mockRouter as any)["events"] = of({ url: "/vault" }) as any;
service = new DefaultEncryptedMigrationsSchedulerService(
mockSyncService,
mockAccountService,
mockStateProvider,
mockEncryptedMigrator,
mockAuthService,
mockLogService,
mockDialogService,
mockToastService,
mockI18nService,
mockRouter,
);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("runMigrationsIfNeeded", () => {
it("should return early if user is not unlocked", async () => {
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Locked));
await service.runMigrationsIfNeeded(mockUserId);
expect(mockEncryptedMigrator.needsMigrations).not.toHaveBeenCalled();
expect(mockLogService.info).not.toHaveBeenCalled();
});
it("should log and return when no migration is needed", async () => {
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
mockEncryptedMigrator.needsMigrations.mockResolvedValue("noMigrationNeeded");
await service.runMigrationsIfNeeded(mockUserId);
expect(mockEncryptedMigrator.needsMigrations).toHaveBeenCalledWith(mockUserId);
expect(mockLogService.info).toHaveBeenCalledWith(
`[EncryptedMigrationsScheduler] No migrations needed for user ${mockUserId}`,
);
expect(mockEncryptedMigrator.runMigrations).not.toHaveBeenCalled();
});
it("should run migrations without interaction when master password is not required", async () => {
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
mockEncryptedMigrator.needsMigrations.mockResolvedValue("needsMigration");
await service.runMigrationsIfNeeded(mockUserId);
expect(mockEncryptedMigrator.needsMigrations).toHaveBeenCalledWith(mockUserId);
expect(mockLogService.info).toHaveBeenCalledWith(
`[EncryptedMigrationsScheduler] User ${mockUserId} needs migrations with master password`,
);
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(mockUserId, null);
});
it("should run migrations with interaction when migration is needed", async () => {
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
mockEncryptedMigrator.needsMigrations.mockResolvedValue("needsMigrationWithMasterPassword");
const mockUserState = createMockUserState(null);
mockStateProvider.getUser.mockReturnValue(mockUserState);
await service.runMigrationsIfNeeded(mockUserId);
expect(mockEncryptedMigrator.needsMigrations).toHaveBeenCalledWith(mockUserId);
expect(mockLogService.info).toHaveBeenCalledWith(
`[EncryptedMigrationsScheduler] User ${mockUserId} needs migrations with master password`,
);
expect(PromptMigrationPasswordComponent.open).toHaveBeenCalledWith(mockDialogService);
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(
mockUserId,
mockMasterPassword,
);
});
});
describe("runMigrationsWithoutInteraction", () => {
it("should run migrations without master password", async () => {
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
mockEncryptedMigrator.needsMigrations.mockResolvedValue("needsMigration");
await service.runMigrationsIfNeeded(mockUserId);
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(mockUserId, null);
expect(mockLogService.error).not.toHaveBeenCalled();
});
it("should handle errors during migration without interaction", async () => {
const mockError = new Error("Migration failed");
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
mockEncryptedMigrator.needsMigrations.mockResolvedValue("needsMigration");
mockEncryptedMigrator.runMigrations.mockRejectedValue(mockError);
await service.runMigrationsIfNeeded(mockUserId);
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(mockUserId, null);
expect(mockLogService.error).toHaveBeenCalledWith(
"[EncryptedMigrationsScheduler] Error during migration without interaction",
mockError,
);
});
});
describe("runMigrationsWithInteraction", () => {
beforeEach(() => {
mockAuthService.authStatusFor$.mockReturnValue(of(AuthenticationStatus.Unlocked));
mockEncryptedMigrator.needsMigrations.mockResolvedValue("needsMigrationWithMasterPassword");
});
it("should skip if migration was dismissed recently", async () => {
const recentDismissDate = new Date(Date.now() - 12 * 60 * 60 * 1000); // 12 hours ago
const mockUserState = createMockUserState(recentDismissDate);
mockStateProvider.getUser.mockReturnValue(mockUserState);
await service.runMigrationsIfNeeded(mockUserId);
expect(mockStateProvider.getUser).toHaveBeenCalledWith(
mockUserId,
ENCRYPTED_MIGRATION_DISMISSED,
);
expect(mockLogService.info).toHaveBeenCalledWith(
"[EncryptedMigrationsScheduler] Migration prompt dismissed recently, skipping for now.",
);
expect(PromptMigrationPasswordComponent.open).not.toHaveBeenCalled();
});
it("should prompt for migration if dismissed date is older than 24 hours", async () => {
const oldDismissDate = new Date(Date.now() - 25 * 60 * 60 * 1000); // 25 hours ago
const mockUserState = createMockUserState(oldDismissDate);
mockStateProvider.getUser.mockReturnValue(mockUserState);
await service.runMigrationsIfNeeded(mockUserId);
expect(mockStateProvider.getUser).toHaveBeenCalledWith(
mockUserId,
ENCRYPTED_MIGRATION_DISMISSED,
);
expect(PromptMigrationPasswordComponent.open).toHaveBeenCalledWith(mockDialogService);
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(
mockUserId,
mockMasterPassword,
);
});
it("should prompt for migration if no dismiss date exists", async () => {
const mockUserState = createMockUserState(null);
mockStateProvider.getUser.mockReturnValue(mockUserState);
await service.runMigrationsIfNeeded(mockUserId);
expect(PromptMigrationPasswordComponent.open).toHaveBeenCalledWith(mockDialogService);
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(
mockUserId,
mockMasterPassword,
);
});
it("should set dismiss date when empty password is provided", async () => {
const mockUserState = createMockUserState(null);
mockStateProvider.getUser.mockReturnValue(mockUserState);
const mockDialogRef = {
closed: of(""), // Empty password
};
jest.spyOn(PromptMigrationPasswordComponent, "open").mockReturnValue(mockDialogRef as any);
await service.runMigrationsIfNeeded(mockUserId);
expect(PromptMigrationPasswordComponent.open).toHaveBeenCalledWith(mockDialogService);
expect(mockEncryptedMigrator.runMigrations).not.toHaveBeenCalled();
expect(mockStateProvider.setUserState).toHaveBeenCalledWith(
ENCRYPTED_MIGRATION_DISMISSED,
expect.any(Date),
mockUserId,
);
});
it("should handle errors during migration prompt and show toast", async () => {
const mockUserState = createMockUserState(null);
mockStateProvider.getUser.mockReturnValue(mockUserState);
const mockError = new Error("Migration failed");
mockEncryptedMigrator.runMigrations.mockRejectedValue(mockError);
await service.runMigrationsIfNeeded(mockUserId);
expect(PromptMigrationPasswordComponent.open).toHaveBeenCalledWith(mockDialogService);
expect(mockEncryptedMigrator.runMigrations).toHaveBeenCalledWith(
mockUserId,
mockMasterPassword,
);
expect(mockLogService.error).toHaveBeenCalledWith(
"[EncryptedMigrationsScheduler] Error during migration prompt",
mockError,
);
expect(mockToastService.showToast).toHaveBeenCalledWith({
variant: "error",
message: "translated_migrationsFailed",
});
});
});
});