mirror of
https://github.com/bitwarden/browser
synced 2026-01-23 12:53:44 +00:00
Desktop Autotype add service unit tests (#17678)
This commit is contained in:
@@ -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<LogService>;
|
||||
let mockWindowMain: jest.Mocked<WindowMain>;
|
||||
let ipcHandlers: Map<string, Function>;
|
||||
|
||||
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" },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<AccountService>;
|
||||
let mockAuthService: jest.Mocked<AuthService>;
|
||||
let mockCipherService: jest.Mocked<CipherService>;
|
||||
let mockConfigService: jest.Mocked<ConfigService>;
|
||||
let mockGlobalStateProvider: jest.Mocked<GlobalStateProvider>;
|
||||
let mockPlatformUtilsService: jest.Mocked<PlatformUtilsService>;
|
||||
let mockBillingAccountProfileStateService: jest.Mocked<BillingAccountProfileStateService>;
|
||||
let mockDesktopAutotypePolicy: jest.Mocked<DesktopAutotypeDefaultSettingPolicy>;
|
||||
let mockLogService: jest.Mocked<LogService>;
|
||||
|
||||
// Mock GlobalState objects
|
||||
let mockAutotypeEnabledState: any;
|
||||
let mockAutotypeKeyboardShortcutState: any;
|
||||
|
||||
// BehaviorSubjects for reactive state
|
||||
let autotypeEnabledSubject: BehaviorSubject<boolean | null>;
|
||||
let autotypeKeyboardShortcutSubject: BehaviorSubject<string[]>;
|
||||
let activeAccountSubject: BehaviorSubject<any>;
|
||||
let activeAccountStatusSubject: BehaviorSubject<AuthenticationStatus>;
|
||||
let hasPremiumSubject: BehaviorSubject<boolean>;
|
||||
let featureFlagSubject: BehaviorSubject<boolean>;
|
||||
let autotypeDefaultPolicySubject: BehaviorSubject<boolean>;
|
||||
let cipherViewsSubject: BehaviorSubject<any[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Initialize BehaviorSubjects
|
||||
autotypeEnabledSubject = new BehaviorSubject<boolean | null>(null);
|
||||
autotypeKeyboardShortcutSubject = new BehaviorSubject<string[]>(["Control", "Shift", "B"]);
|
||||
activeAccountSubject = new BehaviorSubject<any>({ id: "user-123" });
|
||||
activeAccountStatusSubject = new BehaviorSubject<AuthenticationStatus>(
|
||||
AuthenticationStatus.Unlocked,
|
||||
);
|
||||
hasPremiumSubject = new BehaviorSubject<boolean>(true);
|
||||
featureFlagSubject = new BehaviorSubject<boolean>(true);
|
||||
autotypeDefaultPolicySubject = new BehaviorSubject<boolean>(false);
|
||||
cipherViewsSubject = new BehaviorSubject<any[]>([]);
|
||||
|
||||
// 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", () => {
|
||||
|
||||
Reference in New Issue
Block a user