diff --git a/apps/desktop/src/autofill/main/main-desktop-autotype.service.spec.ts b/apps/desktop/src/autofill/main/main-desktop-autotype.service.spec.ts new file mode 100644 index 00000000000..92802d2e2cf --- /dev/null +++ b/apps/desktop/src/autofill/main/main-desktop-autotype.service.spec.ts @@ -0,0 +1,400 @@ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ + +import { TestBed } from "@angular/core/testing"; +import { ipcMain, globalShortcut } from "electron"; + +import { autotype } from "@bitwarden/desktop-napi"; +import { LogService } from "@bitwarden/logging"; + +import { WindowMain } from "../../main/window.main"; +import { AutotypeConfig } from "../models/autotype-config"; +import { AutotypeMatchError } from "../models/autotype-errors"; +import { AutotypeVaultData } from "../models/autotype-vault-data"; +import { AUTOTYPE_IPC_CHANNELS } from "../models/ipc-channels"; +import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut"; + +import { MainDesktopAutotypeService } from "./main-desktop-autotype.service"; + +// Mock electron modules +jest.mock("electron", () => ({ + ipcMain: { + on: jest.fn(), + removeAllListeners: jest.fn(), + }, + globalShortcut: { + register: jest.fn(), + unregister: jest.fn(), + isRegistered: jest.fn(), + }, +})); + +// Mock desktop-napi +jest.mock("@bitwarden/desktop-napi", () => ({ + autotype: { + getForegroundWindowTitle: jest.fn(), + typeInput: jest.fn(), + }, +})); + +// Mock AutotypeKeyboardShortcut +jest.mock("../models/main-autotype-keyboard-shortcut", () => ({ + AutotypeKeyboardShortcut: jest.fn().mockImplementation(() => ({ + set: jest.fn().mockReturnValue(true), + getElectronFormat: jest.fn().mockReturnValue("Control+Alt+B"), + getArrayFormat: jest.fn().mockReturnValue(["Control", "Alt", "B"]), + })), +})); + +describe("MainDesktopAutotypeService", () => { + let service: MainDesktopAutotypeService; + let mockLogService: jest.Mocked; + let mockWindowMain: jest.Mocked; + let ipcHandlers: Map; + + beforeEach(() => { + // Track IPC handlers + ipcHandlers = new Map(); + (ipcMain.on as jest.Mock).mockImplementation((channel: string, handler: Function) => { + ipcHandlers.set(channel, handler); + }); + + // Mock LogService + mockLogService = { + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + warning: jest.fn(), + } as any; + + // Mock WindowMain with webContents + mockWindowMain = { + win: { + webContents: { + send: jest.fn(), + }, + }, + } as any; + + // Reset all mocks + jest.clearAllMocks(); + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(false); + (globalShortcut.register as jest.Mock).mockReturnValue(true); + + TestBed.configureTestingModule({ + providers: [ + { provide: LogService, useValue: mockLogService }, + { provide: WindowMain, useValue: mockWindowMain }, + ], + }); + + // Create service manually since it doesn't use Angular DI + service = new MainDesktopAutotypeService(mockLogService, mockWindowMain); + }); + + afterEach(() => { + ipcHandlers.clear(); // Clear handler map + service.dispose(); + }); + + describe("constructor", () => { + it("should create service", () => { + expect(service).toBeTruthy(); + }); + + it("should initialize keyboard shortcut", () => { + expect(service.autotypeKeyboardShortcut).toBeDefined(); + }); + + it("should register IPC handlers", () => { + expect(ipcMain.on).toHaveBeenCalledWith(AUTOTYPE_IPC_CHANNELS.TOGGLE, expect.any(Function)); + expect(ipcMain.on).toHaveBeenCalledWith( + AUTOTYPE_IPC_CHANNELS.CONFIGURE, + expect.any(Function), + ); + expect(ipcMain.on).toHaveBeenCalledWith(AUTOTYPE_IPC_CHANNELS.EXECUTE, expect.any(Function)); + expect(ipcMain.on).toHaveBeenCalledWith( + "autofill.completeAutotypeError", + expect.any(Function), + ); + }); + }); + + describe("TOGGLE handler", () => { + it("should enable autotype when toggle is true", () => { + const toggleHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.TOGGLE); + + toggleHandler({}, true); + + expect(globalShortcut.register).toHaveBeenCalled(); + expect(mockLogService.debug).toHaveBeenCalledWith("Autotype enabled."); + }); + + it("should disable autotype when toggle is false", () => { + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(true); + const toggleHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.TOGGLE); + + toggleHandler({}, false); + + expect(globalShortcut.unregister).toHaveBeenCalled(); + expect(mockLogService.debug).toHaveBeenCalledWith("Autotype disabled."); + }); + }); + + describe("CONFIGURE handler", () => { + it("should update keyboard shortcut with valid configuration", () => { + const config: AutotypeConfig = { + keyboardShortcut: ["Control", "Alt", "A"], + }; + + const mockNewShortcut = { + set: jest.fn().mockReturnValue(true), + getElectronFormat: jest.fn().mockReturnValue("Control+Alt+A"), + getArrayFormat: jest.fn().mockReturnValue(["Control", "Alt", "A"]), + }; + (AutotypeKeyboardShortcut as jest.Mock).mockReturnValue(mockNewShortcut); + + const configureHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.CONFIGURE); + configureHandler({}, config); + + expect(mockNewShortcut.set).toHaveBeenCalledWith(config.keyboardShortcut); + }); + + it("should log error with invalid keyboard shortcut", () => { + const config: AutotypeConfig = { + keyboardShortcut: ["Invalid", "Keys"], + }; + + const mockNewShortcut = { + set: jest.fn().mockReturnValue(false), + getElectronFormat: jest.fn(), + getArrayFormat: jest.fn(), + }; + (AutotypeKeyboardShortcut as jest.Mock).mockReturnValue(mockNewShortcut); + + const configureHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.CONFIGURE); + configureHandler({}, config); + + expect(mockLogService.error).toHaveBeenCalledWith( + "Configure autotype failed: the keyboard shortcut is invalid.", + ); + }); + + it("should register new shortcut if one already registered", () => { + (globalShortcut.isRegistered as jest.Mock) + .mockReturnValueOnce(true) + .mockReturnValueOnce(true) + .mockReturnValueOnce(false); + + const config: AutotypeConfig = { + keyboardShortcut: ["Control", "Alt", "B"], + }; + + const mockNewShortcut = { + set: jest.fn().mockReturnValue(true), + getElectronFormat: jest.fn().mockReturnValue("Control+Alt+B"), + getArrayFormat: jest.fn().mockReturnValue(["Control", "Alt", "B"]), + }; + (AutotypeKeyboardShortcut as jest.Mock).mockReturnValue(mockNewShortcut); + + const configureHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.CONFIGURE); + configureHandler({}, config); + + expect(globalShortcut.unregister).toHaveBeenCalled(); + expect(globalShortcut.register).toHaveBeenCalled(); + }); + + it("should not change shortcut if it is the same", () => { + const config: AutotypeConfig = { + keyboardShortcut: ["Control", "Alt", "B"], + }; + + jest + .spyOn(service.autotypeKeyboardShortcut, "getElectronFormat") + .mockReturnValue("Control+Alt+B"); + + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(true); + + const configureHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.CONFIGURE); + configureHandler({}, config); + + expect(mockLogService.debug).toHaveBeenCalledWith( + "setKeyboardShortcut() called but shortcut is not different from current.", + ); + }); + }); + + describe("EXECUTE handler", () => { + it("should execute autotype with valid vault data", async () => { + const vaultData: AutotypeVaultData = { + username: "testuser", + password: "testpass", + }; + + jest + .spyOn(service.autotypeKeyboardShortcut, "getArrayFormat") + .mockReturnValue(["Control", "Alt", "B"]); + + const executeHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.EXECUTE); + await executeHandler({}, vaultData); + + expect(autotype.typeInput).toHaveBeenCalledWith(expect.any(Array), ["Control", "Alt", "B"]); + }); + + it("should not execute autotype with empty username", () => { + const vaultData: AutotypeVaultData = { + username: "", + password: "testpass", + }; + + const executeHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.EXECUTE); + executeHandler({}, vaultData); + + expect(autotype.typeInput).not.toHaveBeenCalled(); + }); + + it("should not execute autotype with empty password", () => { + const vaultData: AutotypeVaultData = { + username: "testuser", + password: "", + }; + + const executeHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.EXECUTE); + executeHandler({}, vaultData); + + expect(autotype.typeInput).not.toHaveBeenCalled(); + }); + + it("should format input with tab separator", () => { + const mockNewShortcut = { + set: jest.fn().mockReturnValue(true), + getElectronFormat: jest.fn().mockReturnValue("Control+Alt+B"), + getArrayFormat: jest.fn().mockReturnValue(["Control", "Alt", "B"]), + }; + + (AutotypeKeyboardShortcut as jest.Mock).mockReturnValue(mockNewShortcut); + + const vaultData: AutotypeVaultData = { + username: "user", + password: "pass", + }; + + const executeHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.EXECUTE); + executeHandler({}, vaultData); + + // Verify the input array contains char codes for "user\tpass" + const expectedPattern = "user\tpass"; + const expectedArray = Array.from(expectedPattern).map((c) => c.charCodeAt(0)); + + expect(autotype.typeInput).toHaveBeenCalledWith(expectedArray, ["Control", "Alt", "B"]); + }); + }); + + describe("completeAutotypeError handler", () => { + it("should log autotype match errors", () => { + const matchError: AutotypeMatchError = { + windowTitle: "Test Window", + errorMessage: "No matching vault item", + }; + + const errorHandler = ipcHandlers.get("autofill.completeAutotypeError"); + errorHandler({}, matchError); + + expect(mockLogService.debug).toHaveBeenCalledWith( + "autofill.completeAutotypeError", + "No match for window: Test Window", + ); + expect(mockLogService.error).toHaveBeenCalledWith( + "autofill.completeAutotypeError", + "No matching vault item", + ); + }); + }); + + describe("disableAutotype", () => { + it("should unregister shortcut if registered", () => { + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(true); + + service.disableAutotype(); + + expect(globalShortcut.unregister).toHaveBeenCalled(); + expect(mockLogService.debug).toHaveBeenCalledWith("Autotype disabled."); + }); + + it("should log debug message if shortcut not registered", () => { + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(false); + + service.disableAutotype(); + + expect(globalShortcut.unregister).not.toHaveBeenCalled(); + expect(mockLogService.debug).toHaveBeenCalledWith( + "Autotype is not registered, implicitly disabled.", + ); + }); + }); + + describe("dispose", () => { + it("should remove all IPC listeners", () => { + service.dispose(); + + expect(ipcMain.removeAllListeners).toHaveBeenCalledWith(AUTOTYPE_IPC_CHANNELS.TOGGLE); + expect(ipcMain.removeAllListeners).toHaveBeenCalledWith(AUTOTYPE_IPC_CHANNELS.CONFIGURE); + expect(ipcMain.removeAllListeners).toHaveBeenCalledWith(AUTOTYPE_IPC_CHANNELS.EXECUTE); + }); + + it("should disable autotype", () => { + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(true); + + service.dispose(); + + expect(globalShortcut.unregister).toHaveBeenCalled(); + }); + }); + + describe("enableAutotype (via TOGGLE handler)", () => { + it("should register global shortcut", () => { + const toggleHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.TOGGLE); + + toggleHandler({}, true); + + expect(globalShortcut.register).toHaveBeenCalledWith("Control+Alt+B", expect.any(Function)); + }); + + it("should not register if already registered", () => { + (globalShortcut.isRegistered as jest.Mock).mockReturnValue(true); + const toggleHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.TOGGLE); + + toggleHandler({}, true); + + expect(globalShortcut.register).not.toHaveBeenCalled(); + expect(mockLogService.debug).toHaveBeenCalledWith( + "Autotype is already enabled with this keyboard shortcut: Control+Alt+B", + ); + }); + + it("should log error if registration fails", () => { + (globalShortcut.register as jest.Mock).mockReturnValue(false); + const toggleHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.TOGGLE); + + toggleHandler({}, true); + + expect(mockLogService.error).toHaveBeenCalledWith("Failed to enable Autotype."); + }); + + it("should send window title to renderer on shortcut activation", () => { + (autotype.getForegroundWindowTitle as jest.Mock).mockReturnValue("Notepad"); + + const toggleHandler = ipcHandlers.get(AUTOTYPE_IPC_CHANNELS.TOGGLE); + toggleHandler({}, true); + + // Get the registered callback + const registeredCallback = (globalShortcut.register as jest.Mock).mock.calls[0][1]; + registeredCallback(); + + expect(autotype.getForegroundWindowTitle).toHaveBeenCalled(); + expect(mockWindowMain.win.webContents.send).toHaveBeenCalledWith( + AUTOTYPE_IPC_CHANNELS.LISTEN, + { windowTitle: "Notepad" }, + ); + }); + }); +}); diff --git a/apps/desktop/src/autofill/services/desktop-autotype.service.spec.ts b/apps/desktop/src/autofill/services/desktop-autotype.service.spec.ts index 30cc800dd28..000242476ed 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype.service.spec.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype.service.spec.ts @@ -1,6 +1,363 @@ -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; -import { getAutotypeVaultData } from "./desktop-autotype.service"; +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"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { DeviceType } from "@bitwarden/common/enums"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LogService } from "@bitwarden/logging"; + +import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service"; +import { DesktopAutotypeService, getAutotypeVaultData } from "./desktop-autotype.service"; + +describe("DesktopAutotypeService", () => { + let service: DesktopAutotypeService; + + // Mock dependencies + let mockAccountService: jest.Mocked; + let mockAuthService: jest.Mocked; + let mockCipherService: jest.Mocked; + let mockConfigService: jest.Mocked; + let mockGlobalStateProvider: jest.Mocked; + let mockPlatformUtilsService: jest.Mocked; + let mockBillingAccountProfileStateService: jest.Mocked; + let mockDesktopAutotypePolicy: jest.Mocked; + let mockLogService: jest.Mocked; + + // Mock GlobalState objects + let mockAutotypeEnabledState: any; + let mockAutotypeKeyboardShortcutState: any; + + // BehaviorSubjects for reactive state + let autotypeEnabledSubject: BehaviorSubject; + let autotypeKeyboardShortcutSubject: BehaviorSubject; + let activeAccountSubject: BehaviorSubject; + let activeAccountStatusSubject: BehaviorSubject; + let hasPremiumSubject: BehaviorSubject; + let featureFlagSubject: BehaviorSubject; + let autotypeDefaultPolicySubject: BehaviorSubject; + let cipherViewsSubject: BehaviorSubject; + + beforeEach(() => { + // Initialize BehaviorSubjects + autotypeEnabledSubject = new BehaviorSubject(null); + autotypeKeyboardShortcutSubject = new BehaviorSubject(["Control", "Shift", "B"]); + activeAccountSubject = new BehaviorSubject({ id: "user-123" }); + activeAccountStatusSubject = new BehaviorSubject( + AuthenticationStatus.Unlocked, + ); + hasPremiumSubject = new BehaviorSubject(true); + featureFlagSubject = new BehaviorSubject(true); + autotypeDefaultPolicySubject = new BehaviorSubject(false); + cipherViewsSubject = new BehaviorSubject([]); + + // Mock GlobalState objects + mockAutotypeEnabledState = { + state$: autotypeEnabledSubject.asObservable(), + update: jest.fn().mockImplementation(async (configureState, options) => { + const newState = configureState(autotypeEnabledSubject.value, null); + + // Handle shouldUpdate option + if (options?.shouldUpdate && !options.shouldUpdate(autotypeEnabledSubject.value)) { + return autotypeEnabledSubject.value; + } + + autotypeEnabledSubject.next(newState); + return newState; + }), + }; + + mockAutotypeKeyboardShortcutState = { + state$: autotypeKeyboardShortcutSubject.asObservable(), + update: jest.fn().mockImplementation(async (configureState) => { + const newState = configureState(autotypeKeyboardShortcutSubject.value, null); + autotypeKeyboardShortcutSubject.next(newState); + return newState; + }), + }; + + // Mock GlobalStateProvider + mockGlobalStateProvider = { + get: jest.fn().mockImplementation((keyDef) => { + if (keyDef.key === "autotypeEnabled") { + return mockAutotypeEnabledState; + } + if (keyDef.key === "autotypeKeyboardShortcut") { + return mockAutotypeKeyboardShortcutState; + } + }), + } as any; + + // Mock AccountService + mockAccountService = { + activeAccount$: activeAccountSubject.asObservable(), + } as any; + + // Mock AuthService + mockAuthService = { + activeAccountStatus$: activeAccountStatusSubject.asObservable(), + } as any; + + // Mock CipherService + mockCipherService = { + cipherViews$: jest.fn().mockReturnValue(cipherViewsSubject.asObservable()), + } as any; + + // Mock ConfigService + mockConfigService = { + getFeatureFlag$: jest.fn().mockReturnValue(featureFlagSubject.asObservable()), + } as any; + + // Mock PlatformUtilsService + mockPlatformUtilsService = { + getDevice: jest.fn().mockReturnValue(DeviceType.WindowsDesktop), + } as any; + + // Mock BillingAccountProfileStateService + mockBillingAccountProfileStateService = { + hasPremiumFromAnySource$: jest.fn().mockReturnValue(hasPremiumSubject.asObservable()), + } as any; + + // Mock DesktopAutotypeDefaultSettingPolicy + mockDesktopAutotypePolicy = { + autotypeDefaultSetting$: autotypeDefaultPolicySubject.asObservable(), + } as any; + + // Mock LogService + mockLogService = { + error: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + } as any; + + // Mock ipc (global) + global.ipc = { + autofill: { + listenAutotypeRequest: jest.fn(), + configureAutotype: jest.fn(), + toggleAutotype: jest.fn(), + }, + } as any; + + TestBed.configureTestingModule({ + providers: [ + DesktopAutotypeService, + { provide: AccountService, useValue: mockAccountService }, + { provide: AuthService, useValue: mockAuthService }, + { provide: CipherService, useValue: mockCipherService }, + { provide: ConfigService, useValue: mockConfigService }, + { provide: GlobalStateProvider, useValue: mockGlobalStateProvider }, + { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, + { + provide: BillingAccountProfileStateService, + useValue: mockBillingAccountProfileStateService, + }, + { provide: DesktopAutotypeDefaultSettingPolicy, useValue: mockDesktopAutotypePolicy }, + { provide: LogService, useValue: mockLogService }, + ], + }); + + service = TestBed.inject(DesktopAutotypeService); + }); + + afterEach(() => { + jest.clearAllMocks(); + service.ngOnDestroy(); + }); + + describe("constructor", () => { + it("should create service", () => { + expect(service).toBeTruthy(); + }); + + it("should initialize observables", () => { + expect(service.autotypeEnabledUserSetting$).toBeDefined(); + expect(service.autotypeKeyboardShortcut$).toBeDefined(); + }); + }); + + describe("init", () => { + it("should register autotype request listener on Windows", async () => { + await service.init(); + + expect(global.ipc.autofill.listenAutotypeRequest).toHaveBeenCalled(); + }); + + it("should not initialize on non-Windows platforms", async () => { + mockPlatformUtilsService.getDevice.mockReturnValue(DeviceType.MacOsDesktop); + + await service.init(); + + expect(global.ipc.autofill.listenAutotypeRequest).not.toHaveBeenCalled(); + }); + + it("should configure autotype when keyboard shortcut changes", async () => { + await service.init(); + + // Allow observables to emit + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(global.ipc.autofill.configureAutotype).toHaveBeenCalled(); + }); + + it("should toggle autotype when feature enabled state changes", async () => { + autotypeEnabledSubject.next(true); + + await service.init(); + + // Allow observables to emit + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(global.ipc.autofill.toggleAutotype).toHaveBeenCalled(); + }); + + it("should enable autotype when policy is true and user setting is null", async () => { + autotypeEnabledSubject.next(null); + autotypeDefaultPolicySubject.next(true); + + await service.init(); + + // Allow observables to emit + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockAutotypeEnabledState.update).toHaveBeenCalled(); + expect(autotypeEnabledSubject.value).toBe(true); + }); + }); + + describe("setAutotypeEnabledState", () => { + it("should update autotype enabled state", async () => { + await service.setAutotypeEnabledState(true); + + expect(mockAutotypeEnabledState.update).toHaveBeenCalled(); + expect(autotypeEnabledSubject.value).toBe(true); + }); + + it("should not update if value has not changed", async () => { + autotypeEnabledSubject.next(true); + + await service.setAutotypeEnabledState(true); + + // Update was called but shouldUpdate prevented the change + expect(mockAutotypeEnabledState.update).toHaveBeenCalled(); + expect(autotypeEnabledSubject.value).toBe(true); + }); + }); + + describe("setAutotypeKeyboardShortcutState", () => { + it("should update keyboard shortcut state", async () => { + const newShortcut = ["Control", "Alt", "A"]; + + await service.setAutotypeKeyboardShortcutState(newShortcut); + + expect(mockAutotypeKeyboardShortcutState.update).toHaveBeenCalled(); + expect(autotypeKeyboardShortcutSubject.value).toEqual(newShortcut); + }); + }); + + describe("matchCiphersToWindowTitle", () => { + it("should match ciphers with matching apptitle URIs", async () => { + const mockCiphers = [ + { + login: { + username: "user1", + password: "pass1", + uris: [{ uri: "apptitle://notepad" }], + }, + deletedDate: null, + }, + { + login: { + username: "user2", + password: "pass2", + uris: [{ uri: "apptitle://chrome" }], + }, + deletedDate: null, + }, + ]; + + cipherViewsSubject.next(mockCiphers); + + const result = await service.matchCiphersToWindowTitle("Notepad - Document.txt"); + + expect(result).toHaveLength(1); + expect(result[0].login.username).toBe("user1"); + }); + + it("should filter out deleted ciphers", async () => { + const mockCiphers = [ + { + login: { + username: "user1", + password: "pass1", + uris: [{ uri: "apptitle://notepad" }], + }, + deletedDate: new Date(), + }, + ]; + + cipherViewsSubject.next(mockCiphers); + + const result = await service.matchCiphersToWindowTitle("Notepad"); + + expect(result).toHaveLength(0); + }); + + it("should filter out ciphers without username or password", async () => { + const mockCiphers = [ + { + login: { + username: null, + password: "pass1", + uris: [{ uri: "apptitle://notepad" }], + }, + deletedDate: null, + }, + ]; + + cipherViewsSubject.next(mockCiphers); + + const result = await service.matchCiphersToWindowTitle("Notepad"); + + expect(result).toHaveLength(0); + }); + + it("should perform case-insensitive matching", async () => { + const mockCiphers = [ + { + login: { + username: "user1", + password: "pass1", + uris: [{ uri: "apptitle://NOTEPAD" }], + }, + deletedDate: null, + }, + ]; + + cipherViewsSubject.next(mockCiphers); + + const result = await service.matchCiphersToWindowTitle("notepad - document.txt"); + + expect(result).toHaveLength(1); + }); + }); + + describe("ngOnDestroy", () => { + it("should complete destroy subject", () => { + const destroySpy = jest.spyOn(service["destroy$"], "complete"); + + service.ngOnDestroy(); + + expect(destroySpy).toHaveBeenCalled(); + }); + }); +}); describe("getAutotypeVaultData", () => { it("should return vault data when cipher has username and password", () => {