bitwarden-estensione-browser/apps/browser/src/autofill/services/autofill.service.spec.ts

4633 lines
160 KiB
TypeScript

import { mock, mockReset } from "jest-mock-extended";
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
import { EventType } from "@bitwarden/common/enums";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import {
FieldType,
LinkedIdType,
LoginLinkedId,
UriMatchType,
CipherType,
} from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { BrowserApi } from "../../platform/browser/browser-api";
import { BrowserStateService } from "../../platform/services/browser-state.service";
import { AutofillPort } from "../enums/autofill-port.enums";
import {
createAutofillFieldMock,
createAutofillPageDetailsMock,
createAutofillScriptMock,
createChromeTabMock,
createGenerateFillScriptOptionsMock,
} from "../jest/autofill-mocks";
import { triggerTestFailure } from "../jest/testing-utils";
import AutofillField from "../models/autofill-field";
import AutofillPageDetails from "../models/autofill-page-details";
import AutofillScript from "../models/autofill-script";
import { AutofillOverlayVisibility } from "../utils/autofill-overlay.enum";
import {
AutoFillOptions,
GenerateFillScriptOptions,
PageDetail,
} from "./abstractions/autofill.service";
import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants";
import AutofillService from "./autofill.service";
describe("AutofillService", () => {
let autofillService: AutofillService;
const cipherService = mock<CipherService>();
const stateService = mock<BrowserStateService>();
const totpService = mock<TotpService>();
const eventCollectionService = mock<EventCollectionService>();
const logService = mock<LogService>();
const settingsService = mock<SettingsService>();
const userVerificationService = mock<UserVerificationService>();
const configService = mock<ConfigService>();
beforeEach(() => {
autofillService = new AutofillService(
cipherService,
stateService,
totpService,
eventCollectionService,
logService,
settingsService,
userVerificationService,
configService,
);
});
afterEach(() => {
jest.clearAllMocks();
mockReset(cipherService);
});
describe("loadAutofillScriptsOnInstall", () => {
let tab1: chrome.tabs.Tab;
let tab2: chrome.tabs.Tab;
let tab3: chrome.tabs.Tab;
beforeEach(() => {
tab1 = createChromeTabMock({ id: 1, url: "https://some-url.com" });
tab2 = createChromeTabMock({ id: 2, url: "http://some-url.com" });
tab3 = createChromeTabMock({ id: 3, url: "chrome-extension://some-extension-route" });
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValueOnce([tab1, tab2]);
});
it("queries all browser tabs and injects the autofill scripts into them", async () => {
jest.spyOn(autofillService, "injectAutofillScripts");
await autofillService.loadAutofillScriptsOnInstall();
expect(BrowserApi.tabsQuery).toHaveBeenCalledWith({});
expect(autofillService.injectAutofillScripts).toHaveBeenCalledWith(tab1, 0, false);
expect(autofillService.injectAutofillScripts).toHaveBeenCalledWith(tab2, 0, false);
});
it("skips injecting scripts into tabs that do not have an http(s) protocol", async () => {
jest.spyOn(autofillService, "injectAutofillScripts");
await autofillService.loadAutofillScriptsOnInstall();
expect(BrowserApi.tabsQuery).toHaveBeenCalledWith({});
expect(autofillService.injectAutofillScripts).not.toHaveBeenCalledWith(tab3);
});
it("sets up an extension runtime onConnect listener", async () => {
await autofillService.loadAutofillScriptsOnInstall();
// eslint-disable-next-line no-restricted-syntax
expect(chrome.runtime.onConnect.addListener).toHaveBeenCalledWith(expect.any(Function));
});
});
describe("reloadAutofillScripts", () => {
it("disconnects and removes all autofill script ports", () => {
const port1 = mock<chrome.runtime.Port>({
disconnect: jest.fn(),
});
const port2 = mock<chrome.runtime.Port>({
disconnect: jest.fn(),
});
autofillService["autofillScriptPortsSet"] = new Set([port1, port2]);
autofillService.reloadAutofillScripts();
expect(port1.disconnect).toHaveBeenCalled();
expect(port2.disconnect).toHaveBeenCalled();
expect(autofillService["autofillScriptPortsSet"].size).toBe(0);
});
it("re-injects the autofill scripts in all tabs", () => {
autofillService["autofillScriptPortsSet"] = new Set([mock<chrome.runtime.Port>()]);
jest.spyOn(autofillService as any, "injectAutofillScriptsInAllTabs");
autofillService.reloadAutofillScripts();
expect(autofillService["injectAutofillScriptsInAllTabs"]).toHaveBeenCalled();
});
});
describe("injectAutofillScripts", () => {
const autofillBootstrapScript = "bootstrap-autofill.js";
const autofillOverlayBootstrapScript = "bootstrap-autofill-overlay.js";
const defaultAutofillScripts = ["autofiller.js", "notificationBar.js", "contextMenuHandler.js"];
const defaultExecuteScriptOptions = { runAt: "document_start" };
let tabMock: chrome.tabs.Tab;
let sender: chrome.runtime.MessageSender;
beforeEach(() => {
tabMock = createChromeTabMock();
sender = { tab: tabMock, frameId: 1 };
jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation();
});
it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => {
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true);
[autofillOverlayBootstrapScript, ...defaultAutofillScripts].forEach((scriptName) => {
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
file: `content/${scriptName}`,
frameId: sender.frameId,
...defaultExecuteScriptOptions,
});
});
});
it("will inject the bootstrap-autofill-overlay script if the user has the autofill overlay enabled", async () => {
jest
.spyOn(autofillService["settingsService"], "getAutoFillOverlayVisibility")
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
file: `content/${autofillOverlayBootstrapScript}`,
frameId: sender.frameId,
...defaultExecuteScriptOptions,
});
expect(BrowserApi.executeScriptInTab).not.toHaveBeenCalledWith(tabMock.id, {
file: `content/${autofillBootstrapScript}`,
frameId: sender.frameId,
...defaultExecuteScriptOptions,
});
});
it("will inject the bootstrap-autofill script if the user does not have the autofill overlay enabled", async () => {
jest
.spyOn(autofillService["settingsService"], "getAutoFillOverlayVisibility")
.mockResolvedValue(AutofillOverlayVisibility.Off);
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
file: `content/${autofillBootstrapScript}`,
frameId: sender.frameId,
...defaultExecuteScriptOptions,
});
expect(BrowserApi.executeScriptInTab).not.toHaveBeenCalledWith(tabMock.id, {
file: `content/${autofillOverlayBootstrapScript}`,
frameId: sender.frameId,
...defaultExecuteScriptOptions,
});
});
it("injects the bootstrap-content-message-handler script if not injecting on page load", async () => {
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, false);
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
file: "content/bootstrap-content-message-handler.js",
...defaultExecuteScriptOptions,
});
});
});
describe("getFormsWithPasswordFields", () => {
let pageDetailsMock: AutofillPageDetails;
beforeEach(() => {
pageDetailsMock = createAutofillPageDetailsMock();
});
it("returns an empty FormData array if no password fields are found", () => {
jest.spyOn(AutofillService, "loadPasswordFields");
const formData = autofillService.getFormsWithPasswordFields(pageDetailsMock);
expect(AutofillService.loadPasswordFields).toHaveBeenCalledWith(
pageDetailsMock,
true,
true,
false,
true,
);
expect(formData).toStrictEqual([]);
});
it("returns an FormData array containing a form with it's autofill data", () => {
const usernameInputField = createAutofillFieldMock({
opid: "username-field",
form: "validFormId",
elementNumber: 1,
});
const passwordInputField = createAutofillFieldMock({
opid: "password-field",
type: "password",
form: "validFormId",
elementNumber: 2,
});
pageDetailsMock.fields = [usernameInputField, passwordInputField];
const formData = autofillService.getFormsWithPasswordFields(pageDetailsMock);
expect(formData).toStrictEqual([
{
form: pageDetailsMock.forms.validFormId,
password: pageDetailsMock.fields[1],
passwords: [pageDetailsMock.fields[1]],
username: pageDetailsMock.fields[0],
},
]);
});
it("narrows down three passwords that are present on a page to a single password field to autofill when only one form element is present on the page", () => {
const usernameInputField = createAutofillFieldMock({
opid: "username-field",
form: "validFormId",
elementNumber: 1,
});
const passwordInputField = createAutofillFieldMock({
opid: "password-field",
type: "password",
form: "validFormId",
elementNumber: 2,
});
const secondPasswordInputField = createAutofillFieldMock({
opid: "another-password-field",
type: "password",
form: undefined,
elementNumber: 3,
});
const thirdPasswordInputField = createAutofillFieldMock({
opid: "a-third-password-field",
type: "password",
form: undefined,
elementNumber: 4,
});
pageDetailsMock.fields = [
usernameInputField,
passwordInputField,
secondPasswordInputField,
thirdPasswordInputField,
];
const formData = autofillService.getFormsWithPasswordFields(pageDetailsMock);
expect(formData).toStrictEqual([
{
form: pageDetailsMock.forms.validFormId,
password: pageDetailsMock.fields[1],
passwords: [
pageDetailsMock.fields[1],
{ ...pageDetailsMock.fields[2], form: pageDetailsMock.fields[1].form },
{ ...pageDetailsMock.fields[3], form: pageDetailsMock.fields[1].form },
],
username: pageDetailsMock.fields[0],
},
]);
});
it("will check for a hidden username field", () => {
const usernameInputField = createAutofillFieldMock({
opid: "username-field",
form: "validFormId",
elementNumber: 1,
isViewable: false,
readonly: true,
});
const passwordInputField = createAutofillFieldMock({
opid: "password-field",
type: "password",
form: "validFormId",
elementNumber: 2,
});
pageDetailsMock.fields = [usernameInputField, passwordInputField];
jest.spyOn(autofillService as any, "findUsernameField");
const formData = autofillService.getFormsWithPasswordFields(pageDetailsMock);
expect(autofillService["findUsernameField"]).toHaveBeenCalledWith(
pageDetailsMock,
passwordInputField,
true,
true,
false,
);
expect(formData).toStrictEqual([
{
form: pageDetailsMock.forms.validFormId,
password: pageDetailsMock.fields[1],
passwords: [pageDetailsMock.fields[1]],
username: pageDetailsMock.fields[0],
},
]);
});
});
describe("doAutoFill", () => {
let autofillOptions: AutoFillOptions;
const nothingToAutofillError = "Nothing to auto-fill.";
const didNotAutofillError = "Did not auto-fill.";
beforeEach(() => {
autofillOptions = {
cipher: mock<CipherView>({
id: "cipherId",
type: CipherType.Login,
}),
pageDetails: [
{
frameId: 1,
tab: createChromeTabMock(),
details: createAutofillPageDetailsMock({
fields: [
createAutofillFieldMock({
opid: "username-field",
form: "validFormId",
elementNumber: 1,
}),
createAutofillFieldMock({
opid: "password-field",
type: "password",
form: "validFormId",
elementNumber: 2,
}),
],
}),
},
],
tab: createChromeTabMock(),
};
autofillOptions.cipher.fields = [mock<FieldView>({ name: "username" })];
autofillOptions.cipher.login.matchesUri = jest.fn().mockReturnValue(true);
autofillOptions.cipher.login.username = "username";
autofillOptions.cipher.login.password = "password";
});
describe("given a set of autofill options that are incomplete", () => {
it("throws an error if the tab is not provided", async () => {
autofillOptions.tab = undefined;
try {
await autofillService.doAutoFill(autofillOptions);
triggerTestFailure();
} catch (error) {
expect(error.message).toBe(nothingToAutofillError);
}
});
it("throws an error if the cipher is not provided", async () => {
autofillOptions.cipher = undefined;
try {
await autofillService.doAutoFill(autofillOptions);
triggerTestFailure();
} catch (error) {
expect(error.message).toBe(nothingToAutofillError);
}
});
it("throws an error if the page details are not provided", async () => {
autofillOptions.pageDetails = undefined;
try {
await autofillService.doAutoFill(autofillOptions);
triggerTestFailure();
} catch (error) {
expect(error.message).toBe(nothingToAutofillError);
}
});
it("throws an error if the page details are empty", async () => {
autofillOptions.pageDetails = [];
try {
await autofillService.doAutoFill(autofillOptions);
triggerTestFailure();
} catch (error) {
expect(error.message).toBe(nothingToAutofillError);
}
});
it("throws an error if an autofill did not occur for any of the passed pages", async () => {
autofillOptions.tab.url = "https://a-different-url.com";
try {
await autofillService.doAutoFill(autofillOptions);
triggerTestFailure();
} catch (error) {
expect(error.message).toBe(didNotAutofillError);
}
});
});
it("will autofill login data for a page", async () => {
jest.spyOn(stateService, "getCanAccessPremium");
jest.spyOn(stateService, "getDefaultUriMatch");
jest.spyOn(autofillService as any, "generateFillScript");
jest.spyOn(autofillService as any, "generateLoginFillScript");
jest.spyOn(logService, "info");
jest.spyOn(cipherService, "updateLastUsedDate");
jest.spyOn(eventCollectionService, "collect");
const autofillResult = await autofillService.doAutoFill(autofillOptions);
const currentAutofillPageDetails = autofillOptions.pageDetails[0];
expect(stateService.getCanAccessPremium).toHaveBeenCalled();
expect(stateService.getDefaultUriMatch).toHaveBeenCalled();
expect(autofillService["generateFillScript"]).toHaveBeenCalledWith(
currentAutofillPageDetails.details,
{
skipUsernameOnlyFill: autofillOptions.skipUsernameOnlyFill || false,
onlyEmptyFields: autofillOptions.onlyEmptyFields || false,
onlyVisibleFields: autofillOptions.onlyVisibleFields || false,
fillNewPassword: autofillOptions.fillNewPassword || false,
allowTotpAutofill: autofillOptions.allowTotpAutofill || false,
cipher: autofillOptions.cipher,
tabUrl: autofillOptions.tab.url,
defaultUriMatch: 0,
},
);
expect(autofillService["generateLoginFillScript"]).toHaveBeenCalled();
expect(logService.info).not.toHaveBeenCalled();
expect(cipherService.updateLastUsedDate).toHaveBeenCalledWith(autofillOptions.cipher.id);
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
autofillOptions.pageDetails[0].tab.id,
{
command: "fillForm",
fillScript: {
autosubmit: null,
metadata: {},
properties: {
delay_between_operations: 20,
},
savedUrls: [],
script: [
["click_on_opid", "username-field"],
["focus_by_opid", "username-field"],
["fill_by_opid", "username-field", "username"],
["click_on_opid", "password-field"],
["focus_by_opid", "password-field"],
["fill_by_opid", "password-field", "password"],
["focus_by_opid", "password-field"],
],
untrustedIframe: false,
},
url: currentAutofillPageDetails.tab.url,
pageDetailsUrl: "url",
},
{
frameId: currentAutofillPageDetails.frameId,
},
expect.any(Function),
);
expect(eventCollectionService.collect).toHaveBeenCalledWith(
EventType.Cipher_ClientAutofilled,
autofillOptions.cipher.id,
);
expect(autofillResult).toBeNull();
});
it("will autofill card data for a page", async () => {
autofillOptions.cipher.type = CipherType.Card;
autofillOptions.cipher.card = mock<CardView>({
cardholderName: "cardholderName",
});
autofillOptions.pageDetails[0].details.fields = [
createAutofillFieldMock({
opid: "cardholderName",
form: "validFormId",
elementNumber: 2,
autoCompleteType: "cc-name",
}),
];
jest.spyOn(autofillService as any, "generateCardFillScript");
jest.spyOn(eventCollectionService, "collect");
await autofillService.doAutoFill(autofillOptions);
expect(autofillService["generateCardFillScript"]).toHaveBeenCalled();
expect(chrome.tabs.sendMessage).toHaveBeenCalled();
expect(eventCollectionService.collect).toHaveBeenCalledWith(
EventType.Cipher_ClientAutofilled,
autofillOptions.cipher.id,
);
});
it("will autofill identity data for a page", async () => {
autofillOptions.cipher.type = CipherType.Identity;
autofillOptions.cipher.identity = mock<IdentityView>({
firstName: "firstName",
middleName: "middleName",
lastName: "lastName",
});
autofillOptions.pageDetails[0].details.fields = [
createAutofillFieldMock({
opid: "full-name",
form: "validFormId",
elementNumber: 2,
autoCompleteType: "full-name",
}),
];
jest.spyOn(autofillService as any, "generateIdentityFillScript");
jest.spyOn(eventCollectionService, "collect");
await autofillService.doAutoFill(autofillOptions);
expect(autofillService["generateIdentityFillScript"]).toHaveBeenCalled();
expect(chrome.tabs.sendMessage).toHaveBeenCalled();
expect(eventCollectionService.collect).toHaveBeenCalledWith(
EventType.Cipher_ClientAutofilled,
autofillOptions.cipher.id,
);
});
it("blocks autofill on an untrusted iframe", async () => {
autofillOptions.allowUntrustedIframe = false;
autofillOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false);
jest.spyOn(logService, "info");
try {
await autofillService.doAutoFill(autofillOptions);
triggerTestFailure();
} catch (error) {
expect(logService.info).toHaveBeenCalledWith(
"Auto-fill on page load was blocked due to an untrusted iframe.",
);
expect(error.message).toBe(didNotAutofillError);
}
});
it("allows autofill on an untrusted iframe if the passed option allowing untrusted iframes is set to true", async () => {
autofillOptions.allowUntrustedIframe = true;
autofillOptions.cipher.login.matchesUri = jest.fn().mockReturnValue(false);
jest.spyOn(logService, "info");
await autofillService.doAutoFill(autofillOptions);
expect(logService.info).not.toHaveBeenCalledWith(
"Auto-fill on page load was blocked due to an untrusted iframe.",
);
});
it("skips updating the cipher's last used date if the passed options indicate that we should skip the last used cipher", async () => {
autofillOptions.skipLastUsed = true;
jest.spyOn(cipherService, "updateLastUsedDate");
await autofillService.doAutoFill(autofillOptions);
expect(cipherService.updateLastUsedDate).not.toHaveBeenCalled();
});
it("returns early if the fillScript cannot be generated", async () => {
jest.spyOn(autofillService as any, "generateFillScript").mockReturnValueOnce(undefined);
jest.spyOn(BrowserApi, "tabSendMessage");
try {
await autofillService.doAutoFill(autofillOptions);
triggerTestFailure();
} catch (error) {
expect(autofillService["generateFillScript"]).toHaveBeenCalled();
expect(BrowserApi.tabSendMessage).not.toHaveBeenCalled();
expect(error.message).toBe(didNotAutofillError);
}
});
it("returns a TOTP value", async () => {
const totpCode = "123456";
autofillOptions.cipher.login.totp = "totp";
jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(true);
jest.spyOn(stateService, "getDisableAutoTotpCopy").mockResolvedValue(false);
jest.spyOn(totpService, "getCode").mockResolvedValue(totpCode);
const autofillResult = await autofillService.doAutoFill(autofillOptions);
expect(stateService.getDisableAutoTotpCopy).toHaveBeenCalled();
expect(totpService.getCode).toHaveBeenCalledWith(autofillOptions.cipher.login.totp);
expect(autofillResult).toBe(totpCode);
});
it("does not return a TOTP value if the user does not have premium features", async () => {
autofillOptions.cipher.login.totp = "totp";
jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(false);
jest.spyOn(stateService, "getDisableAutoTotpCopy").mockResolvedValue(false);
const autofillResult = await autofillService.doAutoFill(autofillOptions);
expect(stateService.getDisableAutoTotpCopy).not.toHaveBeenCalled();
expect(totpService.getCode).not.toHaveBeenCalled();
expect(autofillResult).toBeNull();
});
it("returns a null value if the cipher type is not for a Login", async () => {
autofillOptions.cipher.type = CipherType.Identity;
autofillOptions.cipher.identity = mock<IdentityView>();
const autofillResult = await autofillService.doAutoFill(autofillOptions);
expect(autofillResult).toBeNull();
});
it("returns a null value if the login does not contain a TOTP value", async () => {
autofillOptions.cipher.login.totp = undefined;
jest.spyOn(stateService, "getDisableAutoTotpCopy");
jest.spyOn(totpService, "getCode");
const autofillResult = await autofillService.doAutoFill(autofillOptions);
expect(stateService.getDisableAutoTotpCopy).not.toHaveBeenCalled();
expect(totpService.getCode).not.toHaveBeenCalled();
expect(autofillResult).toBeNull();
});
it("returns a null value if the user cannot access premium and the organization does not use TOTP", async () => {
autofillOptions.cipher.login.totp = "totp";
autofillOptions.cipher.organizationUseTotp = false;
jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValueOnce(false);
const autofillResult = await autofillService.doAutoFill(autofillOptions);
expect(autofillResult).toBeNull();
});
it("returns a null value if the user has disabled `auto TOTP copy`", async () => {
autofillOptions.cipher.login.totp = "totp";
autofillOptions.cipher.organizationUseTotp = true;
jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(true);
jest.spyOn(stateService, "getDisableAutoTotpCopy").mockResolvedValue(true);
jest.spyOn(totpService, "getCode");
const autofillResult = await autofillService.doAutoFill(autofillOptions);
expect(stateService.getCanAccessPremium).toHaveBeenCalled();
expect(stateService.getDisableAutoTotpCopy).toHaveBeenCalled();
expect(totpService.getCode).not.toHaveBeenCalled();
expect(autofillResult).toBeNull();
});
});
describe("doAutoFillOnTab", () => {
let pageDetails: PageDetail[];
let tab: chrome.tabs.Tab;
beforeEach(() => {
tab = createChromeTabMock();
pageDetails = [
{
frameId: 1,
tab: createChromeTabMock(),
details: createAutofillPageDetailsMock({
fields: [
createAutofillFieldMock({
opid: "username-field",
form: "validFormId",
elementNumber: 1,
}),
createAutofillFieldMock({
opid: "password-field",
type: "password",
form: "validFormId",
elementNumber: 2,
}),
],
}),
},
];
});
describe("given a tab url which does not match a cipher", () => {
it("will skip autofill and return a null value when triggering on page load", async () => {
jest.spyOn(autofillService, "doAutoFill");
jest.spyOn(cipherService, "getNextCipherForUrl");
jest.spyOn(cipherService, "getLastLaunchedForUrl").mockResolvedValueOnce(null);
jest.spyOn(cipherService, "getLastUsedForUrl").mockResolvedValueOnce(null);
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, false);
expect(cipherService.getNextCipherForUrl).not.toHaveBeenCalled();
expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, true);
expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, true);
expect(autofillService.doAutoFill).not.toHaveBeenCalled();
expect(result).toBeNull();
});
it("will skip autofill and return a null value when triggering from a keyboard shortcut", async () => {
jest.spyOn(autofillService, "doAutoFill");
jest.spyOn(cipherService, "getNextCipherForUrl").mockResolvedValueOnce(null);
jest.spyOn(cipherService, "getLastLaunchedForUrl").mockResolvedValueOnce(null);
jest.spyOn(cipherService, "getLastUsedForUrl").mockResolvedValueOnce(null);
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true);
expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url);
expect(cipherService.getLastLaunchedForUrl).not.toHaveBeenCalled();
expect(cipherService.getLastUsedForUrl).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).not.toHaveBeenCalled();
expect(result).toBeNull();
});
});
describe("given a tab url which matches a cipher", () => {
let cipher: CipherView;
beforeEach(() => {
cipher = mock<CipherView>({
reprompt: CipherRepromptType.None,
localData: {
lastLaunched: Date.now().valueOf(),
},
});
});
it("will autofill the last launched cipher and return a TOTP value when triggering on page load", async () => {
const totpCode = "123456";
const fromCommand = false;
jest.spyOn(autofillService, "doAutoFill").mockResolvedValueOnce(totpCode);
jest.spyOn(cipherService, "getLastLaunchedForUrl").mockResolvedValueOnce(cipher);
jest.spyOn(cipherService, "getLastUsedForUrl");
jest.spyOn(cipherService, "updateLastUsedIndexForUrl");
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, fromCommand);
expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, true);
expect(cipherService.getLastUsedForUrl).not.toHaveBeenCalled();
expect(cipherService.updateLastUsedIndexForUrl).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab,
cipher: cipher,
pageDetails: pageDetails,
skipLastUsed: !fromCommand,
skipUsernameOnlyFill: !fromCommand,
onlyEmptyFields: !fromCommand,
onlyVisibleFields: !fromCommand,
fillNewPassword: fromCommand,
allowUntrustedIframe: fromCommand,
allowTotpAutofill: fromCommand,
});
expect(result).toBe(totpCode);
});
it("will autofill the last used cipher and return a TOTP value when triggering on page load ", async () => {
cipher.localData.lastLaunched = Date.now().valueOf() - 30001;
const totpCode = "123456";
const fromCommand = false;
jest.spyOn(autofillService, "doAutoFill").mockResolvedValueOnce(totpCode);
jest.spyOn(cipherService, "getLastLaunchedForUrl").mockResolvedValueOnce(cipher);
jest.spyOn(cipherService, "getLastUsedForUrl").mockResolvedValueOnce(cipher);
jest.spyOn(cipherService, "updateLastUsedIndexForUrl");
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, fromCommand);
expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, true);
expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, true);
expect(cipherService.updateLastUsedIndexForUrl).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab,
cipher: cipher,
pageDetails: pageDetails,
skipLastUsed: !fromCommand,
skipUsernameOnlyFill: !fromCommand,
onlyEmptyFields: !fromCommand,
onlyVisibleFields: !fromCommand,
fillNewPassword: fromCommand,
allowUntrustedIframe: fromCommand,
allowTotpAutofill: fromCommand,
});
expect(result).toBe(totpCode);
});
it("will autofill the next cipher, update the last used cipher index, and return a TOTP value when triggering from a keyboard shortcut", async () => {
const totpCode = "123456";
const fromCommand = true;
jest.spyOn(autofillService, "doAutoFill").mockResolvedValueOnce(totpCode);
jest.spyOn(cipherService, "getNextCipherForUrl").mockResolvedValueOnce(cipher);
jest.spyOn(cipherService, "updateLastUsedIndexForUrl");
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, fromCommand);
expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url);
expect(cipherService.updateLastUsedIndexForUrl).toHaveBeenCalledWith(tab.url);
expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab,
cipher: cipher,
pageDetails: pageDetails,
skipLastUsed: !fromCommand,
skipUsernameOnlyFill: !fromCommand,
onlyEmptyFields: !fromCommand,
onlyVisibleFields: !fromCommand,
fillNewPassword: fromCommand,
allowUntrustedIframe: fromCommand,
allowTotpAutofill: fromCommand,
});
expect(result).toBe(totpCode);
});
it("will skip autofill, launch the password reprompt window, and return a null value if the cipher re-prompt type is not `None`", async () => {
cipher.reprompt = CipherRepromptType.Password;
jest.spyOn(autofillService, "doAutoFill");
jest.spyOn(cipherService, "getNextCipherForUrl").mockResolvedValueOnce(cipher);
jest
.spyOn(userVerificationService, "hasMasterPasswordAndMasterKeyHash")
.mockResolvedValueOnce(true);
jest
.spyOn(autofillService as any, "openVaultItemPasswordRepromptPopout")
.mockImplementation();
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true);
expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url);
expect(userVerificationService.hasMasterPasswordAndMasterKeyHash).toHaveBeenCalled();
expect(autofillService["openVaultItemPasswordRepromptPopout"]).toHaveBeenCalledWith(tab, {
cipherId: cipher.id,
action: "autofill",
});
expect(autofillService.doAutoFill).not.toHaveBeenCalled();
expect(result).toBeNull();
});
it("skips autofill and does not launch the password reprompt window if the password reprompt is currently debouncing", async () => {
cipher.reprompt = CipherRepromptType.Password;
jest.spyOn(autofillService, "doAutoFill");
jest.spyOn(cipherService, "getNextCipherForUrl").mockResolvedValueOnce(cipher);
jest
.spyOn(userVerificationService, "hasMasterPasswordAndMasterKeyHash")
.mockResolvedValueOnce(true);
jest
.spyOn(autofillService as any, "openVaultItemPasswordRepromptPopout")
.mockImplementation();
jest
.spyOn(autofillService as any, "isDebouncingPasswordRepromptPopout")
.mockReturnValueOnce(true);
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true);
expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url);
expect(autofillService["openVaultItemPasswordRepromptPopout"]).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).not.toHaveBeenCalled();
expect(result).toBeNull();
});
});
});
describe("doAutoFillActiveTab", () => {
let pageDetails: PageDetail[];
let tab: chrome.tabs.Tab;
beforeEach(() => {
tab = createChromeTabMock();
pageDetails = [
{
frameId: 1,
tab: createChromeTabMock(),
details: createAutofillPageDetailsMock({
fields: [
createAutofillFieldMock({
opid: "username-field",
form: "validFormId",
elementNumber: 1,
}),
createAutofillFieldMock({
opid: "password-field",
type: "password",
form: "validFormId",
elementNumber: 2,
}),
],
}),
},
];
});
it("returns a null vault without doing autofill if the page details does not contain fields ", async () => {
pageDetails[0].details.fields = [];
jest.spyOn(autofillService as any, "getActiveTab");
jest.spyOn(autofillService, "doAutoFill");
const result = await autofillService.doAutoFillActiveTab(pageDetails, false);
expect(autofillService["getActiveTab"]).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).not.toHaveBeenCalled();
expect(result).toBeNull();
});
it("returns a null value without doing autofill if the active tab cannot be found", async () => {
jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(undefined);
jest.spyOn(autofillService, "doAutoFill");
const result = await autofillService.doAutoFillActiveTab(pageDetails, false);
expect(autofillService["getActiveTab"]).toHaveBeenCalled();
expect(autofillService.doAutoFill).not.toHaveBeenCalled();
expect(result).toBeNull();
});
it("returns a null value without doing autofill if the active tab url cannot be found", async () => {
jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce({
id: 1,
url: undefined,
});
jest.spyOn(autofillService, "doAutoFill");
const result = await autofillService.doAutoFillActiveTab(pageDetails, false);
expect(autofillService["getActiveTab"]).toHaveBeenCalled();
expect(autofillService.doAutoFill).not.toHaveBeenCalled();
expect(result).toBeNull();
});
it("queries the active tab and enacts an autofill on that tab", async () => {
const totp = "123456";
const fromCommand = false;
jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab);
jest.spyOn(autofillService, "doAutoFillOnTab").mockResolvedValueOnce(totp);
const result = await autofillService.doAutoFillActiveTab(
pageDetails,
fromCommand,
CipherType.Login,
);
expect(autofillService["getActiveTab"]).toHaveBeenCalled();
expect(autofillService.doAutoFillOnTab).toHaveBeenCalledWith(pageDetails, tab, fromCommand);
expect(result).toBe(totp);
});
it("auto-fills card cipher types", async () => {
const cardFormPageDetails = [
{
frameId: 1,
tab: createChromeTabMock(),
details: createAutofillPageDetailsMock({
fields: [
createAutofillFieldMock({
opid: "number-field",
form: "validFormId",
elementNumber: 1,
}),
createAutofillFieldMock({
opid: "ccv-field",
form: "validFormId",
elementNumber: 2,
}),
],
}),
},
];
const cardCipher = mock<CipherView>({
type: CipherType.Card,
reprompt: CipherRepromptType.None,
});
jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab);
jest.spyOn(autofillService, "doAutoFill").mockImplementation();
jest
.spyOn(autofillService["cipherService"], "getAllDecryptedForUrl")
.mockResolvedValueOnce([cardCipher]);
await autofillService.doAutoFillActiveTab(cardFormPageDetails, false, CipherType.Card);
expect(autofillService["cipherService"].getAllDecryptedForUrl).toHaveBeenCalled();
expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab,
cipher: cardCipher,
pageDetails: cardFormPageDetails,
skipLastUsed: true,
skipUsernameOnlyFill: true,
onlyEmptyFields: true,
onlyVisibleFields: true,
fillNewPassword: false,
allowUntrustedIframe: false,
allowTotpAutofill: false,
});
});
it("auto-fills identity cipher types", async () => {
const identityFormPageDetails = [
{
frameId: 1,
tab: createChromeTabMock(),
details: createAutofillPageDetailsMock({
fields: [
createAutofillFieldMock({
opid: "name-field",
form: "validFormId",
elementNumber: 1,
}),
createAutofillFieldMock({
opid: "address-field",
form: "validFormId",
elementNumber: 2,
}),
],
}),
},
];
const identityCipher = mock<CipherView>({
type: CipherType.Identity,
reprompt: CipherRepromptType.None,
});
jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab);
jest.spyOn(autofillService, "doAutoFill").mockImplementation();
jest
.spyOn(autofillService["cipherService"], "getAllDecryptedForUrl")
.mockResolvedValueOnce([identityCipher]);
await autofillService.doAutoFillActiveTab(
identityFormPageDetails,
false,
CipherType.Identity,
);
expect(autofillService["cipherService"].getAllDecryptedForUrl).toHaveBeenCalled();
expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab,
cipher: identityCipher,
pageDetails: identityFormPageDetails,
skipLastUsed: true,
skipUsernameOnlyFill: true,
onlyEmptyFields: true,
onlyVisibleFields: true,
fillNewPassword: false,
allowUntrustedIframe: false,
allowTotpAutofill: false,
});
});
});
describe("getActiveTab", () => {
it("throws are error if a tab cannot be found", async () => {
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValueOnce(undefined);
try {
await autofillService["getActiveTab"]();
triggerTestFailure();
} catch (error) {
expect(BrowserApi.getTabFromCurrentWindow).toHaveBeenCalled();
expect(error.message).toBe("No tab found.");
}
});
it("returns the active tab from the current window", async () => {
const tab = createChromeTabMock();
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValueOnce(tab);
const result = await autofillService["getActiveTab"]();
expect(BrowserApi.getTabFromCurrentWindow).toHaveBeenCalled();
expect(result).toBe(tab);
});
});
describe("generateFillScript", () => {
let defaultUsernameField: AutofillField;
let defaultUsernameFieldView: FieldView;
let defaultPasswordField: AutofillField;
let defaultPasswordFieldView: FieldView;
let pageDetail: AutofillPageDetails;
let generateFillScriptOptions: GenerateFillScriptOptions;
beforeEach(() => {
defaultUsernameField = createAutofillFieldMock({
opid: "username-field",
form: "validFormId",
htmlID: "username",
elementNumber: 1,
});
defaultUsernameFieldView = mock<FieldView>({
name: "username",
value: defaultUsernameField.value,
});
defaultPasswordField = createAutofillFieldMock({
opid: "password-field",
type: "password",
form: "validFormId",
htmlID: "password",
elementNumber: 2,
});
defaultPasswordFieldView = mock<FieldView>({
name: "password",
value: defaultPasswordField.value,
});
pageDetail = createAutofillPageDetailsMock({
fields: [defaultUsernameField, defaultPasswordField],
});
generateFillScriptOptions = createGenerateFillScriptOptionsMock();
generateFillScriptOptions.cipher.fields = [
defaultUsernameFieldView,
defaultPasswordFieldView,
];
});
it("returns null if the page details are not provided", async () => {
const value = await autofillService["generateFillScript"](
undefined,
generateFillScriptOptions,
);
expect(value).toBeNull();
});
it("returns null if the passed options do not contain a valid cipher", async () => {
generateFillScriptOptions.cipher = undefined;
const value = await autofillService["generateFillScript"](
pageDetail,
generateFillScriptOptions,
);
expect(value).toBeNull();
});
describe("given a valid set of cipher fields and page detail fields", () => {
it("will not attempt to fill by opid duplicate fields found within the page details", async () => {
const duplicateUsernameField: AutofillField = createAutofillFieldMock({
opid: "username-field",
form: "validFormId",
htmlID: "username",
elementNumber: 3,
});
pageDetail.fields.push(duplicateUsernameField);
jest.spyOn(generateFillScriptOptions.cipher, "linkedFieldValue");
jest.spyOn(autofillService as any, "findMatchingFieldIndex");
jest.spyOn(AutofillService, "fillByOpid");
await autofillService["generateFillScript"](pageDetail, generateFillScriptOptions);
expect(AutofillService.fillByOpid).not.toHaveBeenCalledWith(
expect.anything(),
duplicateUsernameField,
duplicateUsernameField.value,
);
});
it("will not attempt to fill by opid fields that are not viewable and are not a `span` element", async () => {
defaultUsernameField.viewable = false;
jest.spyOn(AutofillService, "fillByOpid");
await autofillService["generateFillScript"](pageDetail, generateFillScriptOptions);
expect(AutofillService.fillByOpid).not.toHaveBeenCalledWith(
expect.anything(),
defaultUsernameField,
defaultUsernameField.value,
);
});
it("will fill by opid fields that are not viewable but are a `span` element", async () => {
defaultUsernameField.viewable = false;
defaultUsernameField.tagName = "span";
jest.spyOn(AutofillService, "fillByOpid");
await autofillService["generateFillScript"](pageDetail, generateFillScriptOptions);
expect(AutofillService.fillByOpid).toHaveBeenNthCalledWith(
1,
expect.anything(),
defaultUsernameField,
defaultUsernameField.value,
);
});
it("will not attempt to fill by opid fields that do not contain a property that matches the field name", async () => {
defaultUsernameField.htmlID = "does-not-match-username";
jest.spyOn(AutofillService, "fillByOpid");
await autofillService["generateFillScript"](pageDetail, generateFillScriptOptions);
expect(AutofillService.fillByOpid).not.toHaveBeenCalledWith(
expect.anything(),
defaultUsernameField,
defaultUsernameField.value,
);
});
it("will fill by opid fields that contain a property that matches the field name", async () => {
jest.spyOn(generateFillScriptOptions.cipher, "linkedFieldValue");
jest.spyOn(autofillService as any, "findMatchingFieldIndex");
jest.spyOn(AutofillService, "fillByOpid");
await autofillService["generateFillScript"](pageDetail, generateFillScriptOptions);
expect(autofillService["findMatchingFieldIndex"]).toHaveBeenCalledTimes(2);
expect(generateFillScriptOptions.cipher.linkedFieldValue).not.toHaveBeenCalled();
expect(AutofillService.fillByOpid).toHaveBeenNthCalledWith(
1,
expect.anything(),
defaultUsernameField,
defaultUsernameField.value,
);
expect(AutofillService.fillByOpid).toHaveBeenNthCalledWith(
2,
expect.anything(),
defaultPasswordField,
defaultPasswordField.value,
);
});
it("it will fill by opid fields of type Linked", async () => {
const fieldLinkedId: LinkedIdType = LoginLinkedId.Username;
const linkedFieldValue = "linkedFieldValue";
defaultUsernameFieldView.type = FieldType.Linked;
defaultUsernameFieldView.linkedId = fieldLinkedId;
jest
.spyOn(generateFillScriptOptions.cipher, "linkedFieldValue")
.mockReturnValueOnce(linkedFieldValue);
jest.spyOn(AutofillService, "fillByOpid");
await autofillService["generateFillScript"](pageDetail, generateFillScriptOptions);
expect(generateFillScriptOptions.cipher.linkedFieldValue).toHaveBeenCalledTimes(1);
expect(generateFillScriptOptions.cipher.linkedFieldValue).toHaveBeenCalledWith(
fieldLinkedId,
);
expect(AutofillService.fillByOpid).toHaveBeenNthCalledWith(
1,
expect.anything(),
defaultUsernameField,
linkedFieldValue,
);
expect(AutofillService.fillByOpid).toHaveBeenNthCalledWith(
2,
expect.anything(),
defaultPasswordField,
defaultPasswordField.value,
);
});
it("will fill by opid fields of type Boolean", async () => {
defaultUsernameFieldView.type = FieldType.Boolean;
defaultUsernameFieldView.value = "true";
jest.spyOn(generateFillScriptOptions.cipher, "linkedFieldValue");
jest.spyOn(AutofillService, "fillByOpid");
await autofillService["generateFillScript"](pageDetail, generateFillScriptOptions);
expect(generateFillScriptOptions.cipher.linkedFieldValue).not.toHaveBeenCalled();
expect(AutofillService.fillByOpid).toHaveBeenNthCalledWith(
1,
expect.anything(),
defaultUsernameField,
defaultUsernameFieldView.value,
);
});
it("will fill by opid fields of type Boolean with a value of false if no value is provided", async () => {
defaultUsernameFieldView.type = FieldType.Boolean;
defaultUsernameFieldView.value = undefined;
jest.spyOn(AutofillService, "fillByOpid");
await autofillService["generateFillScript"](pageDetail, generateFillScriptOptions);
expect(AutofillService.fillByOpid).toHaveBeenNthCalledWith(
1,
expect.anything(),
defaultUsernameField,
"false",
);
});
});
it("returns a fill script generated for a login autofill", async () => {
const fillScriptMock = createAutofillScriptMock(
{},
{ "username-field": "username-value", "password-value": "password-value" },
);
generateFillScriptOptions.cipher.type = CipherType.Login;
jest
.spyOn(autofillService as any, "generateLoginFillScript")
.mockReturnValueOnce(fillScriptMock);
const value = await autofillService["generateFillScript"](
pageDetail,
generateFillScriptOptions,
);
expect(autofillService["generateLoginFillScript"]).toHaveBeenCalledWith(
{
autosubmit: null,
metadata: {},
properties: {},
script: [
["click_on_opid", "username-field"],
["focus_by_opid", "username-field"],
["fill_by_opid", "username-field", "default-value"],
["click_on_opid", "password-field"],
["focus_by_opid", "password-field"],
["fill_by_opid", "password-field", "default-value"],
],
},
pageDetail,
{
"password-field": defaultPasswordField,
"username-field": defaultUsernameField,
},
generateFillScriptOptions,
);
expect(value).toBe(fillScriptMock);
});
it("returns a fill script generated for a card autofill", async () => {
const fillScriptMock = createAutofillScriptMock(
{},
{ "first-name-field": "first-name-value", "last-name-value": "last-name-value" },
);
generateFillScriptOptions.cipher.type = CipherType.Card;
jest
.spyOn(autofillService as any, "generateCardFillScript")
.mockReturnValueOnce(fillScriptMock);
const value = await autofillService["generateFillScript"](
pageDetail,
generateFillScriptOptions,
);
expect(autofillService["generateCardFillScript"]).toHaveBeenCalledWith(
{
autosubmit: null,
metadata: {},
properties: {},
script: [
["click_on_opid", "username-field"],
["focus_by_opid", "username-field"],
["fill_by_opid", "username-field", "default-value"],
["click_on_opid", "password-field"],
["focus_by_opid", "password-field"],
["fill_by_opid", "password-field", "default-value"],
],
},
pageDetail,
{
"password-field": defaultPasswordField,
"username-field": defaultUsernameField,
},
generateFillScriptOptions,
);
expect(value).toBe(fillScriptMock);
});
it("returns a fill script generated for an identity autofill", async () => {
const fillScriptMock = createAutofillScriptMock(
{},
{ "first-name-field": "first-name-value", "last-name-value": "last-name-value" },
);
generateFillScriptOptions.cipher.type = CipherType.Identity;
jest
.spyOn(autofillService as any, "generateIdentityFillScript")
.mockReturnValueOnce(fillScriptMock);
const value = await autofillService["generateFillScript"](
pageDetail,
generateFillScriptOptions,
);
expect(autofillService["generateIdentityFillScript"]).toHaveBeenCalledWith(
{
autosubmit: null,
metadata: {},
properties: {},
script: [
["click_on_opid", "username-field"],
["focus_by_opid", "username-field"],
["fill_by_opid", "username-field", "default-value"],
["click_on_opid", "password-field"],
["focus_by_opid", "password-field"],
["fill_by_opid", "password-field", "default-value"],
],
},
pageDetail,
{
"password-field": defaultPasswordField,
"username-field": defaultUsernameField,
},
generateFillScriptOptions,
);
expect(value).toBe(fillScriptMock);
});
it("returns null if the cipher type is not for a login, card, or identity", async () => {
generateFillScriptOptions.cipher.type = CipherType.SecureNote;
const value = await autofillService["generateFillScript"](
pageDetail,
generateFillScriptOptions,
);
expect(value).toBeNull();
});
});
describe("generateLoginFillScript", () => {
let fillScript: AutofillScript;
let pageDetails: AutofillPageDetails;
let filledFields: { [id: string]: AutofillField };
let options: GenerateFillScriptOptions;
let defaultLoginUriView: LoginUriView;
beforeEach(() => {
fillScript = createAutofillScriptMock();
pageDetails = createAutofillPageDetailsMock();
filledFields = {
"username-field": createAutofillFieldMock({
opid: "username-field",
form: "validFormId",
elementNumber: 1,
}),
"password-field": createAutofillFieldMock({
opid: "password-field",
form: "validFormId",
elementNumber: 2,
}),
"totp-field": createAutofillFieldMock({
opid: "totp-field",
form: "validFormId",
elementNumber: 3,
}),
};
defaultLoginUriView = mock<LoginUriView>({
uri: "https://www.example.com",
match: UriMatchType.Domain,
});
options = createGenerateFillScriptOptionsMock();
options.cipher.login = mock<LoginView>({
uris: [defaultLoginUriView],
});
options.cipher.login.matchesUri = jest.fn().mockReturnValue(true);
});
it("returns null if the cipher does not have login data", async () => {
options.cipher.login = undefined;
jest.spyOn(autofillService as any, "inUntrustedIframe");
jest.spyOn(AutofillService, "loadPasswordFields");
jest.spyOn(autofillService as any, "findUsernameField");
jest.spyOn(AutofillService, "fieldIsFuzzyMatch");
jest.spyOn(AutofillService, "fillByOpid");
jest.spyOn(AutofillService, "setFillScriptForFocus");
const value = await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["inUntrustedIframe"]).not.toHaveBeenCalled();
expect(AutofillService.loadPasswordFields).not.toHaveBeenCalled();
expect(autofillService["findUsernameField"]).not.toHaveBeenCalled();
expect(AutofillService.fieldIsFuzzyMatch).not.toHaveBeenCalled();
expect(AutofillService.fillByOpid).not.toHaveBeenCalled();
expect(AutofillService.setFillScriptForFocus).not.toHaveBeenCalled();
expect(value).toBeNull();
});
describe("given a list of login uri views", () => {
it("returns an empty array of saved login uri views if the login cipher has no login uri views", async () => {
options.cipher.login.uris = [];
const value = await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.savedUrls).toStrictEqual([]);
});
it("returns a list of saved login uri views within the fill script", async () => {
const secondUriView = mock<LoginUriView>({
uri: "https://www.second-example.com",
});
const thirdUriView = mock<LoginUriView>({
uri: "https://www.third-example.com",
});
options.cipher.login.uris = [defaultLoginUriView, secondUriView, thirdUriView];
const value = await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.savedUrls).toStrictEqual([
defaultLoginUriView.uri,
secondUriView.uri,
thirdUriView.uri,
]);
});
it("skips adding any login uri views that have a UriMatchType of Never to the list of saved urls", async () => {
const secondUriView = mock<LoginUriView>({
uri: "https://www.second-example.com",
});
const thirdUriView = mock<LoginUriView>({
uri: "https://www.third-example.com",
match: UriMatchType.Never,
});
options.cipher.login.uris = [defaultLoginUriView, secondUriView, thirdUriView];
const value = await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.savedUrls).toStrictEqual([defaultLoginUriView.uri, secondUriView.uri]);
expect(value.savedUrls).not.toContain(thirdUriView.uri);
});
});
describe("given a valid set of page details and autofill options", () => {
let usernameField: AutofillField;
let usernameFieldView: FieldView;
let passwordField: AutofillField;
let passwordFieldView: FieldView;
let totpField: AutofillField;
let totpFieldView: FieldView;
beforeEach(() => {
usernameField = createAutofillFieldMock({
opid: "username",
form: "validFormId",
elementNumber: 1,
});
usernameFieldView = mock<FieldView>({
name: "username",
});
passwordField = createAutofillFieldMock({
opid: "password",
type: "password",
form: "validFormId",
elementNumber: 2,
});
passwordFieldView = mock<FieldView>({
name: "password",
});
totpField = createAutofillFieldMock({
opid: "totp",
type: "text",
form: "validFormId",
htmlName: "totpcode",
elementNumber: 3,
});
totpFieldView = mock<FieldView>({
name: "totp",
});
pageDetails.fields = [usernameField, passwordField, totpField];
options.cipher.fields = [usernameFieldView, passwordFieldView, totpFieldView];
options.cipher.login.matchesUri = jest.fn().mockReturnValue(true);
options.cipher.login.username = "username";
options.cipher.login.password = "password";
options.cipher.login.totp = "totp";
});
it("attempts to load the password fields from hidden and read only elements if no visible password fields are found within the page details", async () => {
pageDetails.fields = [
createAutofillFieldMock({
opid: "password-field",
type: "password",
viewable: true,
readonly: true,
}),
];
jest.spyOn(AutofillService, "loadPasswordFields");
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.loadPasswordFields).toHaveBeenCalledTimes(2);
expect(AutofillService.loadPasswordFields).toHaveBeenNthCalledWith(
1,
pageDetails,
false,
false,
options.onlyEmptyFields,
options.fillNewPassword,
);
expect(AutofillService.loadPasswordFields).toHaveBeenNthCalledWith(
2,
pageDetails,
true,
true,
options.onlyEmptyFields,
options.fillNewPassword,
);
});
describe("given a valid list of forms within the passed page details", () => {
beforeEach(() => {
usernameField.viewable = false;
usernameField.readonly = true;
totpField.viewable = false;
totpField.readonly = true;
jest.spyOn(autofillService as any, "findUsernameField");
jest.spyOn(autofillService as any, "findTotpField");
});
it("will attempt to find a username field from hidden fields if no visible username fields are found", async () => {
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["findUsernameField"]).toHaveBeenCalledTimes(2);
expect(autofillService["findUsernameField"]).toHaveBeenNthCalledWith(
1,
pageDetails,
passwordField,
false,
false,
false,
);
expect(autofillService["findUsernameField"]).toHaveBeenNthCalledWith(
2,
pageDetails,
passwordField,
true,
true,
false,
);
});
it("will not attempt to find a username field from hidden fields if the passed options indicate only visible fields should be referenced", async () => {
options.onlyVisibleFields = true;
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["findUsernameField"]).toHaveBeenCalledTimes(1);
expect(autofillService["findUsernameField"]).toHaveBeenNthCalledWith(
1,
pageDetails,
passwordField,
false,
false,
false,
);
expect(autofillService["findUsernameField"]).not.toHaveBeenNthCalledWith(
2,
pageDetails,
passwordField,
true,
true,
false,
);
});
it("will attempt to find a totp field from hidden fields if no visible totp fields are found", async () => {
options.allowTotpAutofill = true;
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["findTotpField"]).toHaveBeenCalledTimes(2);
expect(autofillService["findTotpField"]).toHaveBeenNthCalledWith(
1,
pageDetails,
passwordField,
false,
false,
false,
);
expect(autofillService["findTotpField"]).toHaveBeenNthCalledWith(
2,
pageDetails,
passwordField,
true,
true,
false,
);
});
it("will not attempt to find a totp field from hidden fields if the passed options indicate only visible fields should be referenced", async () => {
options.allowTotpAutofill = true;
options.onlyVisibleFields = true;
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["findTotpField"]).toHaveBeenCalledTimes(1);
expect(autofillService["findTotpField"]).toHaveBeenNthCalledWith(
1,
pageDetails,
passwordField,
false,
false,
false,
);
expect(autofillService["findTotpField"]).not.toHaveBeenNthCalledWith(
2,
pageDetails,
passwordField,
true,
true,
false,
);
});
it("will not attempt to find a totp field from hidden fields if the passed options do not allow for TOTP values to be filled", async () => {
options.allowTotpAutofill = false;
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["findTotpField"]).not.toHaveBeenCalled();
});
});
describe("given a list of fields without forms within the passed page details", () => {
beforeEach(() => {
pageDetails.forms = undefined;
jest.spyOn(autofillService as any, "findUsernameField");
jest.spyOn(autofillService as any, "findTotpField");
});
it("will attempt to match a password field that does not contain a form to a username field", async () => {
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["findUsernameField"]).toHaveBeenCalledTimes(1);
expect(autofillService["findUsernameField"]).toHaveBeenCalledWith(
pageDetails,
passwordField,
false,
false,
true,
);
});
it("will attempt to match a password field that does not contain a form to a username field that is not visible", async () => {
usernameField.viewable = false;
usernameField.readonly = true;
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["findUsernameField"]).toHaveBeenCalledTimes(2);
expect(autofillService["findUsernameField"]).toHaveBeenNthCalledWith(
1,
pageDetails,
passwordField,
false,
false,
true,
);
expect(autofillService["findUsernameField"]).toHaveBeenNthCalledWith(
2,
pageDetails,
passwordField,
true,
true,
true,
);
});
it("will not attempt to match a password field that does not contain a form to a username field that is not visible if the passed options indicate only visible fields", async () => {
usernameField.viewable = false;
usernameField.readonly = true;
options.onlyVisibleFields = true;
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["findUsernameField"]).toHaveBeenCalledTimes(1);
expect(autofillService["findUsernameField"]).toHaveBeenNthCalledWith(
1,
pageDetails,
passwordField,
false,
false,
true,
);
expect(autofillService["findUsernameField"]).not.toHaveBeenNthCalledWith(
2,
pageDetails,
passwordField,
true,
true,
true,
);
});
it("will attempt to match a password field that does not contain a form to a TOTP field", async () => {
options.allowTotpAutofill = true;
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["findTotpField"]).toHaveBeenCalledTimes(1);
expect(autofillService["findTotpField"]).toHaveBeenCalledWith(
pageDetails,
passwordField,
false,
false,
true,
);
});
it("will attempt to match a password field that does not contain a form to a TOTP field that is not visible", async () => {
options.onlyVisibleFields = false;
options.allowTotpAutofill = true;
totpField.viewable = false;
totpField.readonly = true;
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["findTotpField"]).toHaveBeenCalledTimes(2);
expect(autofillService["findTotpField"]).toHaveBeenNthCalledWith(
1,
pageDetails,
passwordField,
false,
false,
true,
);
expect(autofillService["findTotpField"]).toHaveBeenNthCalledWith(
2,
pageDetails,
passwordField,
true,
true,
true,
);
});
});
describe("given a set of page details that does not contain a password field", () => {
let emailField: AutofillField;
let emailFieldView: FieldView;
let telephoneField: AutofillField;
let telephoneFieldView: FieldView;
let totpField: AutofillField;
let totpFieldView: FieldView;
let nonViewableField: AutofillField;
let nonViewableFieldView: FieldView;
beforeEach(() => {
usernameField.htmlName = "username";
emailField = createAutofillFieldMock({
opid: "email",
type: "email",
form: "validFormId",
elementNumber: 2,
});
emailFieldView = mock<FieldView>({
name: "email",
});
telephoneField = createAutofillFieldMock({
opid: "telephone",
type: "tel",
form: "validFormId",
elementNumber: 3,
});
telephoneFieldView = mock<FieldView>({
name: "telephone",
});
totpField = createAutofillFieldMock({
opid: "totp",
type: "text",
form: "validFormId",
htmlName: "totpcode",
elementNumber: 4,
});
totpFieldView = mock<FieldView>({
name: "totp",
});
nonViewableField = createAutofillFieldMock({
opid: "non-viewable",
form: "validFormId",
viewable: false,
elementNumber: 4,
});
nonViewableFieldView = mock<FieldView>({
name: "non-viewable",
});
pageDetails.fields = [
usernameField,
emailField,
telephoneField,
totpField,
nonViewableField,
];
options.cipher.fields = [
usernameFieldView,
emailFieldView,
telephoneFieldView,
totpFieldView,
nonViewableFieldView,
];
jest.spyOn(AutofillService, "fieldIsFuzzyMatch");
jest.spyOn(AutofillService, "fillByOpid");
});
it("will attempt to fuzzy match a username to a viewable text, email or tel field if no password fields are found and the username fill is not being skipped", async () => {
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.fieldIsFuzzyMatch).toHaveBeenCalledTimes(4);
expect(AutofillService.fieldIsFuzzyMatch).toHaveBeenNthCalledWith(
1,
usernameField,
AutoFillConstants.UsernameFieldNames,
);
expect(AutofillService.fieldIsFuzzyMatch).toHaveBeenNthCalledWith(
2,
emailField,
AutoFillConstants.UsernameFieldNames,
);
expect(AutofillService.fieldIsFuzzyMatch).toHaveBeenNthCalledWith(
3,
telephoneField,
AutoFillConstants.UsernameFieldNames,
);
expect(AutofillService.fieldIsFuzzyMatch).toHaveBeenNthCalledWith(
4,
totpField,
AutoFillConstants.UsernameFieldNames,
);
expect(AutofillService.fieldIsFuzzyMatch).not.toHaveBeenNthCalledWith(
5,
nonViewableField,
AutoFillConstants.UsernameFieldNames,
);
expect(AutofillService.fillByOpid).toHaveBeenCalledTimes(1);
expect(AutofillService.fillByOpid).toHaveBeenCalledWith(
fillScript,
usernameField,
options.cipher.login.username,
);
});
it("will not attempt to fuzzy match a username if the username fill is being skipped", async () => {
options.skipUsernameOnlyFill = true;
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.fieldIsFuzzyMatch).not.toHaveBeenCalledWith(
expect.anything(),
AutoFillConstants.UsernameFieldNames,
);
});
it("will attempt to fuzzy match a totp field if totp autofill is allowed", async () => {
options.allowTotpAutofill = true;
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.fieldIsFuzzyMatch).toHaveBeenCalledWith(
expect.anything(),
AutoFillConstants.TotpFieldNames,
);
});
it("will not attempt to fuzzy match a totp field if totp autofill is not allowed", async () => {
options.allowTotpAutofill = false;
await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.fieldIsFuzzyMatch).not.toHaveBeenCalledWith(
expect.anything(),
AutoFillConstants.TotpFieldNames,
);
});
});
it("returns a value indicating if the page url is in an untrusted iframe", async () => {
jest.spyOn(autofillService as any, "inUntrustedIframe").mockReturnValueOnce(true);
const value = await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.untrustedIframe).toBe(true);
});
it("returns a fill script used to autofill a login item", async () => {
jest.spyOn(autofillService as any, "inUntrustedIframe");
jest.spyOn(AutofillService, "loadPasswordFields");
jest.spyOn(autofillService as any, "findUsernameField");
jest.spyOn(AutofillService, "fieldIsFuzzyMatch");
jest.spyOn(AutofillService, "fillByOpid");
jest.spyOn(AutofillService, "setFillScriptForFocus");
const value = await autofillService["generateLoginFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["inUntrustedIframe"]).toHaveBeenCalledWith(pageDetails.url, options);
expect(AutofillService.loadPasswordFields).toHaveBeenCalledWith(
pageDetails,
false,
false,
options.onlyEmptyFields,
options.fillNewPassword,
);
expect(autofillService["findUsernameField"]).toHaveBeenCalledWith(
pageDetails,
passwordField,
false,
false,
false,
);
expect(AutofillService.fieldIsFuzzyMatch).not.toHaveBeenCalled();
expect(AutofillService.fillByOpid).toHaveBeenCalledTimes(2);
expect(AutofillService.fillByOpid).toHaveBeenNthCalledWith(
1,
fillScript,
usernameField,
options.cipher.login.username,
);
expect(AutofillService.fillByOpid).toHaveBeenNthCalledWith(
2,
fillScript,
passwordField,
options.cipher.login.password,
);
expect(AutofillService.setFillScriptForFocus).toHaveBeenCalledWith(
filledFields,
fillScript,
);
expect(value).toStrictEqual({
autosubmit: null,
metadata: {},
properties: { delay_between_operations: 20 },
savedUrls: ["https://www.example.com"],
script: [
["click_on_opid", "default-field"],
["focus_by_opid", "default-field"],
["fill_by_opid", "default-field", "default"],
["click_on_opid", "username"],
["focus_by_opid", "username"],
["fill_by_opid", "username", "username"],
["click_on_opid", "password"],
["focus_by_opid", "password"],
["fill_by_opid", "password", "password"],
["focus_by_opid", "password"],
],
itemType: "",
untrustedIframe: false,
});
});
});
});
describe("generateCardFillScript", () => {
let fillScript: AutofillScript;
let pageDetails: AutofillPageDetails;
let filledFields: { [id: string]: AutofillField };
let options: GenerateFillScriptOptions;
beforeEach(() => {
fillScript = createAutofillScriptMock({
script: [],
});
pageDetails = createAutofillPageDetailsMock();
filledFields = {
"cardholderName-field": createAutofillFieldMock({
opid: "cardholderName-field",
form: "validFormId",
elementNumber: 1,
htmlName: "cc-name",
}),
"cardNumber-field": createAutofillFieldMock({
opid: "cardNumber-field",
form: "validFormId",
elementNumber: 2,
htmlName: "cc-number",
}),
"expMonth-field": createAutofillFieldMock({
opid: "expMonth-field",
form: "validFormId",
elementNumber: 3,
htmlName: "exp-month",
}),
"expYear-field": createAutofillFieldMock({
opid: "expYear-field",
form: "validFormId",
elementNumber: 4,
htmlName: "exp-year",
}),
"code-field": createAutofillFieldMock({
opid: "code-field",
form: "validFormId",
elementNumber: 1,
htmlName: "cvc",
}),
};
options = createGenerateFillScriptOptionsMock();
options.cipher.card = mock<CardView>();
});
it("returns null if the passed options contains a cipher with no card view", () => {
options.cipher.card = undefined;
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value).toBeNull();
});
describe("given an invalid autofill field", () => {
const unmodifiedFillScriptValues: AutofillScript = {
autosubmit: null,
metadata: {},
properties: { delay_between_operations: 20 },
savedUrls: [],
script: [],
itemType: "",
untrustedIframe: false,
};
it("returns an unmodified fill script when the field is a `span` field", () => {
const spanField = createAutofillFieldMock({
opid: "span-field",
form: "validFormId",
elementNumber: 5,
htmlName: "spanField",
tagName: "span",
});
pageDetails.fields = [spanField];
jest.spyOn(AutofillService, "isExcludedFieldType");
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
expect(value).toStrictEqual(unmodifiedFillScriptValues);
});
AutoFillConstants.ExcludedAutofillTypes.forEach((excludedType) => {
it(`returns an unmodified fill script when the field has a '${excludedType}' type`, () => {
const invalidField = createAutofillFieldMock({
opid: `${excludedType}-field`,
form: "validFormId",
elementNumber: 5,
htmlName: "invalidField",
type: excludedType,
});
pageDetails.fields = [invalidField];
jest.spyOn(AutofillService, "isExcludedFieldType");
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith(
invalidField,
AutoFillConstants.ExcludedAutofillTypes,
);
expect(value).toStrictEqual(unmodifiedFillScriptValues);
});
});
it("returns an unmodified fill script when the field is not viewable", () => {
const notViewableField = createAutofillFieldMock({
opid: "invalid-field",
form: "validFormId",
elementNumber: 5,
htmlName: "invalidField",
type: "text",
viewable: false,
});
pageDetails.fields = [notViewableField];
jest.spyOn(AutofillService, "forCustomFieldsOnly");
jest.spyOn(AutofillService, "isExcludedFieldType");
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(notViewableField);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
expect(value).toStrictEqual(unmodifiedFillScriptValues);
});
});
describe("given a valid set of autofill fields", () => {
let cardholderNameField: AutofillField;
let cardholderNameFieldView: FieldView;
let cardNumberField: AutofillField;
let cardNumberFieldView: FieldView;
let expMonthField: AutofillField;
let expMonthFieldView: FieldView;
let expYearField: AutofillField;
let expYearFieldView: FieldView;
let codeField: AutofillField;
let codeFieldView: FieldView;
let brandField: AutofillField;
let brandFieldView: FieldView;
beforeEach(() => {
cardholderNameField = createAutofillFieldMock({
opid: "cardholderName",
form: "validFormId",
elementNumber: 1,
htmlName: "cc-name",
});
cardholderNameFieldView = mock<FieldView>({ name: "cardholderName" });
cardNumberField = createAutofillFieldMock({
opid: "cardNumber",
form: "validFormId",
elementNumber: 2,
htmlName: "cc-number",
});
cardNumberFieldView = mock<FieldView>({ name: "cardNumber" });
expMonthField = createAutofillFieldMock({
opid: "expMonth",
form: "validFormId",
elementNumber: 3,
htmlName: "exp-month",
});
expMonthFieldView = mock<FieldView>({ name: "expMonth" });
expYearField = createAutofillFieldMock({
opid: "expYear",
form: "validFormId",
elementNumber: 4,
htmlName: "exp-year",
});
expYearFieldView = mock<FieldView>({ name: "expYear" });
codeField = createAutofillFieldMock({
opid: "code",
form: "validFormId",
elementNumber: 1,
htmlName: "cvc",
});
brandField = createAutofillFieldMock({
opid: "brand",
form: "validFormId",
elementNumber: 1,
htmlName: "card-brand",
});
brandFieldView = mock<FieldView>({ name: "brand" });
codeFieldView = mock<FieldView>({ name: "code" });
pageDetails.fields = [
cardholderNameField,
cardNumberField,
expMonthField,
expYearField,
codeField,
brandField,
];
options.cipher.fields = [
cardholderNameFieldView,
cardNumberFieldView,
expMonthFieldView,
expYearFieldView,
codeFieldView,
brandFieldView,
];
options.cipher.card.cardholderName = "testCardholderName";
options.cipher.card.number = "testCardNumber";
options.cipher.card.expMonth = "testExpMonth";
options.cipher.card.expYear = "testExpYear";
options.cipher.card.code = "testCode";
options.cipher.card.brand = "testBrand";
jest.spyOn(AutofillService, "forCustomFieldsOnly");
jest.spyOn(AutofillService, "isExcludedFieldType");
jest.spyOn(AutofillService as any, "isFieldMatch");
jest.spyOn(autofillService as any, "makeScriptAction");
jest.spyOn(AutofillService, "hasValue");
jest.spyOn(autofillService as any, "fieldAttrsContain");
jest.spyOn(AutofillService, "fillByOpid");
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
});
it("returns a fill script containing all of the passed card fields", () => {
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledTimes(6);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledTimes(6);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalled();
expect(autofillService["makeScriptAction"]).toHaveBeenCalledTimes(4);
expect(AutofillService["hasValue"]).toHaveBeenCalledTimes(6);
expect(autofillService["fieldAttrsContain"]).toHaveBeenCalledTimes(3);
expect(AutofillService["fillByOpid"]).toHaveBeenCalledTimes(6);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledTimes(4);
expect(value).toStrictEqual({
autosubmit: null,
itemType: "",
metadata: {},
properties: {
delay_between_operations: 20,
},
savedUrls: [],
script: [
["click_on_opid", "cardholderName"],
["focus_by_opid", "cardholderName"],
["fill_by_opid", "cardholderName", "testCardholderName"],
["click_on_opid", "cardNumber"],
["focus_by_opid", "cardNumber"],
["fill_by_opid", "cardNumber", "testCardNumber"],
["click_on_opid", "code"],
["focus_by_opid", "code"],
["fill_by_opid", "code", "testCode"],
["click_on_opid", "brand"],
["focus_by_opid", "brand"],
["fill_by_opid", "brand", "testBrand"],
["click_on_opid", "expMonth"],
["focus_by_opid", "expMonth"],
["fill_by_opid", "expMonth", "testExpMonth"],
["click_on_opid", "expYear"],
["focus_by_opid", "expYear"],
["fill_by_opid", "expYear", "testExpYear"],
],
untrustedIframe: false,
});
});
});
describe("given an expiration month field", () => {
let expMonthField: AutofillField;
let expMonthFieldView: FieldView;
beforeEach(() => {
expMonthField = createAutofillFieldMock({
opid: "expMonth",
form: "validFormId",
elementNumber: 3,
htmlName: "exp-month",
selectInfo: {
options: [
["January", "01"],
["February", "02"],
["March", "03"],
["April", "04"],
["May", "05"],
["June", "06"],
["July", "07"],
["August", "08"],
["September", "09"],
["October", "10"],
["November", "11"],
["December", "12"],
],
},
});
expMonthFieldView = mock<FieldView>({ name: "expMonth" });
pageDetails.fields = [expMonthField];
options.cipher.fields = [expMonthFieldView];
options.cipher.card.expMonth = "05";
});
it("returns an expiration month parsed from found select options within the field", () => {
const testValue = "sometestvalue";
expMonthField.selectInfo.options[4] = ["May", testValue];
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, testValue]);
});
it("returns an expiration month parsed from found select options within the field when the select field has an empty option at the end of the list of options", () => {
const testValue = "sometestvalue";
expMonthField.selectInfo.options[4] = ["May", testValue];
expMonthField.selectInfo.options.push(["", ""]);
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, testValue]);
});
it("returns an expiration month parsed from found select options within the field when the select field has an empty option at the start of the list of options", () => {
const testValue = "sometestvalue";
expMonthField.selectInfo.options[4] = ["May", testValue];
expMonthField.selectInfo.options.unshift(["", ""]);
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, testValue]);
});
it("returns an expiration month with a zero attached if the field requires two characters, and the vault item has only one character", () => {
options.cipher.card.expMonth = "5";
expMonthField.selectInfo = null;
expMonthField.placeholder = "mm";
expMonthField.maxLength = 2;
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, "05"]);
});
});
describe("given an expiration year field", () => {
let expYearField: AutofillField;
let expYearFieldView: FieldView;
beforeEach(() => {
expYearField = createAutofillFieldMock({
opid: "expYear",
form: "validFormId",
elementNumber: 3,
htmlName: "exp-year",
selectInfo: {
options: [
["2023", "2023"],
["2024", "2024"],
["2025", "2025"],
],
},
});
expYearFieldView = mock<FieldView>({ name: "expYear" });
pageDetails.fields = [expYearField];
options.cipher.fields = [expYearFieldView];
options.cipher.card.expYear = "2024";
});
it("returns an expiration year parsed from the select options if an exact match is found for either the select option text or value", () => {
const someTestValue = "sometestvalue";
expYearField.selectInfo.options[1] = ["2024", someTestValue];
options.cipher.card.expYear = someTestValue;
let value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", expYearField.opid, someTestValue]);
expYearField.selectInfo.options[1] = [someTestValue, "2024"];
value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", expYearField.opid, someTestValue]);
});
it("returns an expiration year parsed from the select options if the value of an option contains only two characters and the vault item value contains four characters", () => {
const yearValue = "26";
expYearField.selectInfo.options.push(["The year 2026", yearValue]);
options.cipher.card.expYear = "2026";
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", expYearField.opid, yearValue]);
});
it("returns an expiration year parsed from the select options if the vault of an option is separated by a colon", () => {
const yearValue = "26";
const colonSeparatedYearValue = `2:0${yearValue}`;
expYearField.selectInfo.options.push(["The year 2026", colonSeparatedYearValue]);
options.cipher.card.expYear = yearValue;
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script[2]).toStrictEqual([
"fill_by_opid",
expYearField.opid,
colonSeparatedYearValue,
]);
});
it("returns an expiration year with `20` prepended to the vault item value if the field to be filled expects a `yyyy` format but the vault item only has two characters", () => {
const yearValue = "26";
expYearField.selectInfo = null;
expYearField.placeholder = "yyyy";
expYearField.maxLength = 4;
options.cipher.card.expYear = yearValue;
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script[2]).toStrictEqual([
"fill_by_opid",
expYearField.opid,
`20${yearValue}`,
]);
});
it("returns an expiration year with only the last two values if the field to be filled expects a `yy` format but the vault item contains four characters", () => {
const yearValue = "26";
expYearField.selectInfo = null;
expYearField.placeholder = "yy";
expYearField.maxLength = 2;
options.cipher.card.expYear = `20${yearValue}`;
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", expYearField.opid, yearValue]);
});
});
describe("given a generic expiration date field", () => {
let expirationDateField: AutofillField;
let expirationDateFieldView: FieldView;
beforeEach(() => {
expirationDateField = createAutofillFieldMock({
opid: "expirationDate",
form: "validFormId",
elementNumber: 3,
htmlName: "expiration-date",
});
filledFields["exp-field"] = expirationDateField;
expirationDateFieldView = mock<FieldView>({ name: "exp" });
pageDetails.fields = [expirationDateField];
options.cipher.fields = [expirationDateFieldView];
options.cipher.card.expMonth = "05";
options.cipher.card.expYear = "2024";
});
const expectedDateFormats = [
["mm/yyyy", "05/2024"],
["mm/yy", "05/24"],
["yyyy/mm", "2024/05"],
["yy/mm", "24/05"],
["mm-yyyy", "05-2024"],
["mm-yy", "05-24"],
["yyyy-mm", "2024-05"],
["yy-mm", "24-05"],
["yyyymm", "202405"],
["yymm", "2405"],
["mmyyyy", "052024"],
["mmyy", "0524"],
];
expectedDateFormats.forEach((dateFormat, index) => {
it(`returns an expiration date format matching '${dateFormat[0]}'`, () => {
expirationDateField.placeholder = dateFormat[0];
if (index === 0) {
options.cipher.card.expYear = "24";
}
if (index === 1) {
options.cipher.card.expMonth = "5";
}
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", dateFormat[1]]);
});
});
it("returns an expiration date format matching `yyyy-mm` if no valid format can be identified", () => {
const value = autofillService["generateCardFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", "2024-05"]);
});
});
});
describe("inUntrustedIframe", () => {
it("returns a false value if the passed pageUrl is equal to the options tabUrl", () => {
const pageUrl = "https://www.example.com";
const tabUrl = "https://www.example.com";
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true);
jest.spyOn(settingsService, "getEquivalentDomains");
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(settingsService.getEquivalentDomains).not.toHaveBeenCalled();
expect(generateFillScriptOptions.cipher.login.matchesUri).not.toHaveBeenCalled();
expect(result).toBe(false);
});
it("returns a false value if the passed pageUrl matches the domain of the tabUrl", () => {
const pageUrl = "https://subdomain.example.com";
const tabUrl = "https://www.example.com";
const equivalentDomains = new Set(["example.com"]);
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true);
jest.spyOn(settingsService as any, "getEquivalentDomains").mockReturnValue(equivalentDomains);
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(settingsService.getEquivalentDomains).toHaveBeenCalledWith(pageUrl);
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
pageUrl,
equivalentDomains,
generateFillScriptOptions.defaultUriMatch,
);
expect(result).toBe(false);
});
it("returns a true value if the passed pageUrl does not match the domain of the tabUrl", () => {
const pageUrl = "https://subdomain.example.com";
const tabUrl = "https://www.not-example.com";
const equivalentDomains = new Set(["not-example.com"]);
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false);
jest.spyOn(settingsService as any, "getEquivalentDomains").mockReturnValue(equivalentDomains);
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(settingsService.getEquivalentDomains).toHaveBeenCalledWith(pageUrl);
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
pageUrl,
equivalentDomains,
generateFillScriptOptions.defaultUriMatch,
);
expect(result).toBe(true);
});
});
describe("fieldAttrsContain", () => {
let cardNumberField: AutofillField;
beforeEach(() => {
cardNumberField = createAutofillFieldMock({
opid: "cardNumber",
form: "validFormId",
elementNumber: 1,
htmlName: "card-number",
});
});
it("returns false if a field is not passed", () => {
const value = autofillService["fieldAttrsContain"](null, "data-foo");
expect(value).toBe(false);
});
it("returns false if the field does not contain the passed attribute", () => {
const value = autofillService["fieldAttrsContain"](cardNumberField, "data-foo");
expect(value).toBe(false);
});
it("returns true if the field contains the passed attribute", () => {
const value = autofillService["fieldAttrsContain"](cardNumberField, "card-number");
expect(value).toBe(true);
});
});
describe("generateIdentityFillScript", () => {
let fillScript: AutofillScript;
let pageDetails: AutofillPageDetails;
let filledFields: { [id: string]: AutofillField };
let options: GenerateFillScriptOptions;
beforeEach(() => {
fillScript = createAutofillScriptMock({ script: [] });
pageDetails = createAutofillPageDetailsMock();
filledFields = {};
options = createGenerateFillScriptOptionsMock();
options.cipher.identity = mock<IdentityView>();
});
it("returns null if an identify is not found within the cipher", () => {
options.cipher.identity = null;
jest.spyOn(autofillService as any, "makeScriptAction");
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(value).toBeNull();
expect(autofillService["makeScriptAction"]).not.toHaveBeenCalled();
expect(autofillService["makeScriptActionWithValue"]).not.toHaveBeenCalled();
});
describe("given a set of page details that contains fields", () => {
const firstName = "John";
const middleName = "A";
const lastName = "Doe";
beforeEach(() => {
pageDetails.fields = [];
jest.spyOn(AutofillService, "forCustomFieldsOnly");
jest.spyOn(AutofillService, "isExcludedFieldType");
jest.spyOn(AutofillService as any, "isFieldMatch");
jest.spyOn(autofillService as any, "makeScriptAction");
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
});
it("will not attempt to match custom fields", () => {
const customField = createAutofillFieldMock({ tagName: "span" });
pageDetails.fields.push(customField);
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(customField);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
expect(value.script).toStrictEqual([]);
});
it("will not attempt to match a field that is of an excluded type", () => {
const excludedField = createAutofillFieldMock({ type: "hidden" });
pageDetails.fields.push(excludedField);
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith(
excludedField,
AutoFillConstants.ExcludedAutofillTypes,
);
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
expect(value.script).toStrictEqual([]);
});
it("will not attempt to match a field that is not viewable", () => {
const viewableField = createAutofillFieldMock({ viewable: false });
pageDetails.fields.push(viewableField);
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(viewableField);
expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled();
expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled();
expect(value.script).toStrictEqual([]);
});
it("will match a full name field to the vault item identity value", () => {
const fullNameField = createAutofillFieldMock({ opid: "fullName", htmlName: "full-name" });
pageDetails.fields = [fullNameField];
options.cipher.identity.firstName = firstName;
options.cipher.identity.middleName = middleName;
options.cipher.identity.lastName = lastName;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
fullNameField.htmlName,
IdentityAutoFillConstants.FullNameFieldNames,
IdentityAutoFillConstants.FullNameFieldNameValues,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
`${firstName} ${middleName} ${lastName}`,
fullNameField,
filledFields,
);
expect(value.script[2]).toStrictEqual([
"fill_by_opid",
fullNameField.opid,
`${firstName} ${middleName} ${lastName}`,
]);
});
it("will match a full name field to the a vault item that only has a last name", () => {
const fullNameField = createAutofillFieldMock({ opid: "fullName", htmlName: "full-name" });
pageDetails.fields = [fullNameField];
options.cipher.identity.firstName = "";
options.cipher.identity.middleName = "";
options.cipher.identity.lastName = lastName;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
fullNameField.htmlName,
IdentityAutoFillConstants.FullNameFieldNames,
IdentityAutoFillConstants.FullNameFieldNameValues,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
lastName,
fullNameField,
filledFields,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", fullNameField.opid, lastName]);
});
it("will match first name, middle name, and last name fields to the vault item identity value", () => {
const firstNameField = createAutofillFieldMock({
opid: "firstName",
htmlName: "first-name",
});
const middleNameField = createAutofillFieldMock({
opid: "middleName",
htmlName: "middle-name",
});
const lastNameField = createAutofillFieldMock({ opid: "lastName", htmlName: "last-name" });
pageDetails.fields = [firstNameField, middleNameField, lastNameField];
options.cipher.identity.firstName = firstName;
options.cipher.identity.middleName = middleName;
options.cipher.identity.lastName = lastName;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
firstNameField.htmlName,
IdentityAutoFillConstants.FirstnameFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
middleNameField.htmlName,
IdentityAutoFillConstants.MiddlenameFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
lastNameField.htmlName,
IdentityAutoFillConstants.LastnameFieldNames,
);
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity,
expect.anything(),
filledFields,
firstNameField.opid,
);
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity,
expect.anything(),
filledFields,
middleNameField.opid,
);
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity,
expect.anything(),
filledFields,
lastNameField.opid,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", firstNameField.opid, firstName]);
expect(value.script[5]).toStrictEqual(["fill_by_opid", middleNameField.opid, middleName]);
expect(value.script[8]).toStrictEqual(["fill_by_opid", lastNameField.opid, lastName]);
});
it("will match title and email fields to the vault item identity value", () => {
const titleField = createAutofillFieldMock({ opid: "title", htmlName: "title" });
const emailField = createAutofillFieldMock({ opid: "email", htmlName: "email" });
pageDetails.fields = [titleField, emailField];
const title = "Mr.";
const email = "email@example.com";
options.cipher.identity.title = title;
options.cipher.identity.email = email;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
titleField.htmlName,
IdentityAutoFillConstants.TitleFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
emailField.htmlName,
IdentityAutoFillConstants.EmailFieldNames,
);
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity,
expect.anything(),
filledFields,
titleField.opid,
);
expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith(
fillScript,
options.cipher.identity,
expect.anything(),
filledFields,
emailField.opid,
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", titleField.opid, title]);
expect(value.script[5]).toStrictEqual(["fill_by_opid", emailField.opid, email]);
});
it("will match a full address field to the vault item identity values", () => {
const fullAddressField = createAutofillFieldMock({
opid: "fullAddress",
htmlName: "address",
});
pageDetails.fields = [fullAddressField];
const address1 = "123 Main St.";
const address2 = "Apt. 1";
const address3 = "P.O. Box 123";
options.cipher.identity.address1 = address1;
options.cipher.identity.address2 = address2;
options.cipher.identity.address3 = address3;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
fullAddressField.htmlName,
IdentityAutoFillConstants.AddressFieldNames,
IdentityAutoFillConstants.AddressFieldNameValues,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
`${address1}, ${address2}, ${address3}`,
fullAddressField,
filledFields,
);
expect(value.script[2]).toStrictEqual([
"fill_by_opid",
fullAddressField.opid,
`${address1}, ${address2}, ${address3}`,
]);
});
it("will match address1, address2, address3, postalCode, city, state, country, phone, username, and company fields to their corresponding vault item identity values", () => {
const address1Field = createAutofillFieldMock({ opid: "address1", htmlName: "address-1" });
const address2Field = createAutofillFieldMock({ opid: "address2", htmlName: "address-2" });
const address3Field = createAutofillFieldMock({ opid: "address3", htmlName: "address-3" });
const postalCodeField = createAutofillFieldMock({
opid: "postalCode",
htmlName: "postal-code",
});
const cityField = createAutofillFieldMock({ opid: "city", htmlName: "city" });
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" });
const phoneField = createAutofillFieldMock({ opid: "phone", htmlName: "phone" });
const usernameField = createAutofillFieldMock({ opid: "username", htmlName: "username" });
const companyField = createAutofillFieldMock({ opid: "company", htmlName: "company" });
pageDetails.fields = [
address1Field,
address2Field,
address3Field,
postalCodeField,
cityField,
stateField,
countryField,
phoneField,
usernameField,
companyField,
];
const address1 = "123 Main St.";
const address2 = "Apt. 1";
const address3 = "P.O. Box 123";
const postalCode = "12345";
const city = "City";
const state = "State";
const country = "Country";
const phone = "123-456-7890";
const username = "username";
const company = "Company";
options.cipher.identity.address1 = address1;
options.cipher.identity.address2 = address2;
options.cipher.identity.address3 = address3;
options.cipher.identity.postalCode = postalCode;
options.cipher.identity.city = city;
options.cipher.identity.state = state;
options.cipher.identity.country = country;
options.cipher.identity.phone = phone;
options.cipher.identity.username = username;
options.cipher.identity.company = company;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
address1Field.htmlName,
IdentityAutoFillConstants.Address1FieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
address2Field.htmlName,
IdentityAutoFillConstants.Address2FieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
address3Field.htmlName,
IdentityAutoFillConstants.Address3FieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
postalCodeField.htmlName,
IdentityAutoFillConstants.PostalCodeFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
cityField.htmlName,
IdentityAutoFillConstants.CityFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
stateField.htmlName,
IdentityAutoFillConstants.StateFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
countryField.htmlName,
IdentityAutoFillConstants.CountryFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
phoneField.htmlName,
IdentityAutoFillConstants.PhoneFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
usernameField.htmlName,
IdentityAutoFillConstants.UserNameFieldNames,
);
expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith(
companyField.htmlName,
IdentityAutoFillConstants.CompanyFieldNames,
);
expect(autofillService["makeScriptAction"]).toHaveBeenCalled();
expect(value.script[2]).toStrictEqual(["fill_by_opid", address1Field.opid, address1]);
expect(value.script[5]).toStrictEqual(["fill_by_opid", address2Field.opid, address2]);
expect(value.script[8]).toStrictEqual(["fill_by_opid", address3Field.opid, address3]);
expect(value.script[11]).toStrictEqual(["fill_by_opid", cityField.opid, city]);
expect(value.script[14]).toStrictEqual(["fill_by_opid", postalCodeField.opid, postalCode]);
expect(value.script[17]).toStrictEqual(["fill_by_opid", companyField.opid, company]);
expect(value.script[20]).toStrictEqual(["fill_by_opid", phoneField.opid, phone]);
expect(value.script[23]).toStrictEqual(["fill_by_opid", usernameField.opid, username]);
expect(value.script[26]).toStrictEqual(["fill_by_opid", stateField.opid, state]);
expect(value.script[29]).toStrictEqual(["fill_by_opid", countryField.opid, country]);
});
it("will find the two character IsoState value for an identity cipher that contains the full name of a state", () => {
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
pageDetails.fields = [stateField];
const state = "California";
options.cipher.identity.state = state;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
"CA",
expect.anything(),
expect.anything(),
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "CA"]);
});
it("will find the two character IsoProvince value for an identity cipher that contains the full name of a province", () => {
const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" });
pageDetails.fields = [stateField];
const state = "Ontario";
options.cipher.identity.state = state;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
"ON",
expect.anything(),
expect.anything(),
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "ON"]);
});
it("will find the two character IsoCountry value for an identity cipher that contains the full name of a country", () => {
const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" });
pageDetails.fields = [countryField];
const country = "Somalia";
options.cipher.identity.country = country;
const value = autofillService["generateIdentityFillScript"](
fillScript,
pageDetails,
filledFields,
options,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
"SO",
expect.anything(),
expect.anything(),
);
expect(value.script[2]).toStrictEqual(["fill_by_opid", countryField.opid, "SO"]);
});
});
});
describe("isExcludedType", () => {
it("returns true if the passed type is within the excluded type list", () => {
const value = AutofillService["isExcludedType"](
"hidden",
AutoFillConstants.ExcludedAutofillTypes,
);
expect(value).toBe(true);
});
it("returns true if the passed type is within the excluded type list", () => {
const value = AutofillService["isExcludedType"](
"text",
AutoFillConstants.ExcludedAutofillTypes,
);
expect(value).toBe(false);
});
});
describe("isSearchField", () => {
it("returns true if the passed field type is 'search'", () => {
const typedSearchField = createAutofillFieldMock({ type: "search" });
const value = AutofillService["isSearchField"](typedSearchField);
expect(value).toBe(true);
});
it("returns true if the passed field type is missing and another checked attribute value contains a reference to search", () => {
const untypedSearchField = createAutofillFieldMock({
htmlID: "aSearchInput",
placeholder: null,
type: null,
value: null,
});
const value = AutofillService["isSearchField"](untypedSearchField);
expect(value).toBe(true);
});
it("returns false if the passed field is not a search field", () => {
const typedSearchField = createAutofillFieldMock();
const value = AutofillService["isSearchField"](typedSearchField);
expect(value).toBe(false);
});
});
describe("isFieldMatch", () => {
it("returns true if the passed value is equal to one of the values in the passed options list", () => {
const passedAttribute = "cc-name";
const passedOptions = ["cc-name", "cc_full_name"];
const value = AutofillService["isFieldMatch"](passedAttribute, passedOptions);
expect(value).toBe(true);
});
it("should returns true if the passed options contain a value within the containsOptions list and the passed value partial matches the option", () => {
const passedAttribute = "cc-name-full";
const passedOptions = ["cc-name", "cc_full_name"];
const containsOptions = ["cc-name"];
const value = AutofillService["isFieldMatch"](
passedAttribute,
passedOptions,
containsOptions,
);
expect(value).toBe(true);
});
it("returns false if the value is not a partial match to an option found within the containsOption list", () => {
const passedAttribute = "cc-full-name";
const passedOptions = ["cc-name", "cc_full_name"];
const containsOptions = ["cc-name"];
const value = AutofillService["isFieldMatch"](
passedAttribute,
passedOptions,
containsOptions,
);
expect(value).toBe(false);
});
});
describe("makeScriptAction", () => {
let fillScript: AutofillScript;
let options: GenerateFillScriptOptions;
let mockLoginView: any;
let fillFields: { [key: string]: AutofillField };
const filledFields = {};
beforeEach(() => {
fillScript = createAutofillScriptMock({});
options = createGenerateFillScriptOptionsMock({});
mockLoginView = mock<LoginView>() as any;
options.cipher.login = mockLoginView;
fillFields = {
"username-field": createAutofillFieldMock({ opid: "username-field" }),
};
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
});
it("makes a call to makeScriptActionWithValue using the passed dataProp value", () => {
const dataProp = "username-field";
autofillService["makeScriptAction"](
fillScript,
options.cipher.login,
fillFields,
filledFields,
dataProp,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
mockLoginView[dataProp],
fillFields[dataProp],
filledFields,
);
});
it("makes a call to makeScriptActionWithValue using the passed fieldProp value used for fillFields", () => {
const dataProp = "value";
const fieldProp = "username-field";
autofillService["makeScriptAction"](
fillScript,
options.cipher.login,
fillFields,
filledFields,
dataProp,
fieldProp,
);
expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith(
fillScript,
mockLoginView[dataProp],
fillFields[fieldProp],
filledFields,
);
});
});
describe("makeScriptActionWithValue", () => {
let fillScript: AutofillScript;
let options: GenerateFillScriptOptions;
let mockLoginView: any;
let fillFields: { [key: string]: AutofillField };
const filledFields = {};
beforeEach(() => {
fillScript = createAutofillScriptMock({});
options = createGenerateFillScriptOptionsMock({});
mockLoginView = mock<LoginView>() as any;
options.cipher.login = mockLoginView;
fillFields = {
"username-field": createAutofillFieldMock({ opid: "username-field" }),
};
jest.spyOn(autofillService as any, "makeScriptActionWithValue");
jest.spyOn(AutofillService, "hasValue");
jest.spyOn(AutofillService, "fillByOpid");
});
it("will not add an autofill action to the fill script if the value does not exist", () => {
const dataValue = "";
autofillService["makeScriptActionWithValue"](
fillScript,
dataValue,
fillFields["username-field"],
filledFields,
);
expect(AutofillService.hasValue).toHaveBeenCalledWith(dataValue);
expect(AutofillService.fillByOpid).not.toHaveBeenCalled();
});
it("will not add an autofill action to the fill script if a field is not passed", () => {
const dataValue = "username";
autofillService["makeScriptActionWithValue"](fillScript, dataValue, null, filledFields);
expect(AutofillService.hasValue).toHaveBeenCalledWith(dataValue);
expect(AutofillService.fillByOpid).not.toHaveBeenCalled();
});
it("will add an autofill action to the fill script", () => {
const dataValue = "username";
autofillService["makeScriptActionWithValue"](
fillScript,
dataValue,
fillFields["username-field"],
filledFields,
);
expect(AutofillService.hasValue).toHaveBeenCalledWith(dataValue);
expect(AutofillService.fillByOpid).toHaveBeenCalledWith(
fillScript,
fillFields["username-field"],
dataValue,
);
});
describe("given a autofill field value that indicates the field is a `select` input", () => {
it("will not add an autofil action to the fill script if the dataValue cannot be found in the select options", () => {
const dataValue = "username";
const selectField = createAutofillFieldMock({
opid: "username-field",
tagName: "select",
type: "select-one",
selectInfo: {
options: [["User Name", "Some Other Username Value"]],
},
});
autofillService["makeScriptActionWithValue"](
fillScript,
dataValue,
selectField,
filledFields,
);
expect(AutofillService.hasValue).toHaveBeenCalledWith(dataValue);
expect(AutofillService.fillByOpid).not.toHaveBeenCalled();
});
it("will update the data value to the value found in the select options, and add an autofill action to the fill script", () => {
const dataValue = "username";
const selectField = createAutofillFieldMock({
opid: "username-field",
tagName: "select",
type: "select-one",
selectInfo: {
options: [["username", "Some Other Username Value"]],
},
});
autofillService["makeScriptActionWithValue"](
fillScript,
dataValue,
selectField,
filledFields,
);
expect(AutofillService.hasValue).toHaveBeenCalledWith(dataValue);
expect(AutofillService.fillByOpid).toHaveBeenCalledWith(
fillScript,
selectField,
"Some Other Username Value",
);
});
});
});
describe("loadPasswordFields", () => {
let pageDetails: AutofillPageDetails;
let passwordField: AutofillField;
beforeEach(() => {
pageDetails = createAutofillPageDetailsMock({});
passwordField = createAutofillFieldMock({
opid: "password-field",
type: "password",
form: "validFormId",
});
jest.spyOn(AutofillService, "forCustomFieldsOnly");
});
it("returns an empty array if passed a field that is a `span` element", () => {
const customField = createAutofillFieldMock({ tagName: "span" });
pageDetails.fields = [customField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(customField);
expect(result).toStrictEqual([]);
});
it("returns an empty array if passed a disabled field", () => {
passwordField.disabled = true;
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([]);
});
describe("given a field that is readonly", () => {
it("returns an empty array if the field cannot be readonly", () => {
passwordField.readonly = true;
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([]);
});
it("returns the field within an array if the field can be readonly", () => {
passwordField.readonly = true;
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, true, false, true);
expect(result).toStrictEqual([passwordField]);
});
});
describe("give a field that is not of type `password`", () => {
beforeEach(() => {
passwordField.type = "text";
});
it("returns an empty array if the field type is not `text`", () => {
passwordField.type = "email";
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([]);
});
it("returns an empty array if the `htmlID`, `htmlName`, or `placeholder` of the field's values do not include the word `password`", () => {
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([]);
});
it("returns an empty array if the `htmlID` of the field is `null", () => {
passwordField.htmlID = null;
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([]);
});
it("returns an empty array if the `htmlID` of the field is equal to `onetimepassword`", () => {
passwordField.htmlID = "onetimepassword";
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([]);
});
it("returns the field in an array if the field's htmlID contains the word `password`", () => {
passwordField.htmlID = "password";
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([passwordField]);
});
it("returns the an empty array if the field's htmlID contains the words `password` and `captcha`", () => {
passwordField.htmlID = "inputPasswordCaptcha";
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([]);
});
it("returns the field in an array if the field's htmlName contains the word `password`", () => {
passwordField.htmlName = "password";
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([passwordField]);
});
it("returns the an empty array if the field's htmlName contains the words `password` and `captcha`", () => {
passwordField.htmlName = "inputPasswordCaptcha";
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([]);
});
it("returns the field in an array if the field's placeholder contains the word `password`", () => {
passwordField.placeholder = "password";
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([passwordField]);
});
it("returns the an empty array if the field's placeholder contains the words `password` and `captcha`", () => {
passwordField.placeholder = "inputPasswordCaptcha";
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([]);
});
it("returns the an empty array if any of the field's checked attributed contain the words `captcha` while any other attribute contains the word `password` and no excluded terms", () => {
passwordField.htmlID = "inputPasswordCaptcha";
passwordField.htmlName = "captcha";
passwordField.placeholder = "Enter password";
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([]);
});
});
describe("given a field that is not viewable", () => {
it("returns an empty array if the field cannot be hidden", () => {
passwordField.viewable = false;
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([]);
});
it("returns the field within an array if the field can be hidden", () => {
passwordField.viewable = false;
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, true, false, false, true);
expect(result).toStrictEqual([passwordField]);
});
});
describe("given a need for the passed to be empty", () => {
it("returns an empty array if the passed field contains a value that is not null or empty", () => {
passwordField.value = "Some Password Value";
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, true, false);
expect(result).toStrictEqual([]);
});
it("returns the field within an array if the field contains a null value", () => {
passwordField.value = null;
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, true, false);
expect(result).toStrictEqual([passwordField]);
});
it("returns the field within an array if the field contains an empty value", () => {
passwordField.value = "";
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, true, false);
expect(result).toStrictEqual([passwordField]);
});
});
describe("given a field with a new password", () => {
beforeEach(() => {
passwordField.autoCompleteType = "new-password";
});
it("returns an empty array if not filling a new password and the autoCompleteType is `new-password`", () => {
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, false);
expect(result).toStrictEqual([]);
});
it("returns the field within an array if filling a new password and the autoCompleteType is `new-password`", () => {
pageDetails.fields = [passwordField];
const result = AutofillService.loadPasswordFields(pageDetails, false, false, false, true);
expect(result).toStrictEqual([passwordField]);
});
});
});
describe("findUsernameField", () => {
let pageDetails: AutofillPageDetails;
let usernameField: AutofillField;
let passwordField: AutofillField;
beforeEach(() => {
pageDetails = createAutofillPageDetailsMock({});
usernameField = createAutofillFieldMock({
opid: "username-field",
type: "text",
form: "validFormId",
elementNumber: 0,
});
passwordField = createAutofillFieldMock({
opid: "password-field",
type: "password",
form: "validFormId",
elementNumber: 1,
});
pageDetails.fields = [usernameField, passwordField];
jest.spyOn(AutofillService, "forCustomFieldsOnly");
jest.spyOn(autofillService as any, "findMatchingFieldIndex");
});
it("returns null when passed a field that is a `span` element", () => {
const field = createAutofillFieldMock({ tagName: "span" });
pageDetails.fields = [field];
const result = autofillService["findUsernameField"](pageDetails, field, false, false, false);
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(field);
expect(result).toBe(null);
});
it("returns null when the passed username field has a larger elementNumber than the passed password field", () => {
usernameField.elementNumber = 2;
const result = autofillService["findUsernameField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(null);
});
it("returns null if the passed username field is disabled", () => {
usernameField.disabled = true;
const result = autofillService["findUsernameField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(null);
});
describe("given a field that is readonly", () => {
beforeEach(() => {
usernameField.readonly = true;
});
it("returns null if the field cannot be readonly", () => {
const result = autofillService["findUsernameField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(null);
});
it("returns the field if the field can be readonly", () => {
const result = autofillService["findUsernameField"](
pageDetails,
passwordField,
false,
true,
false,
);
expect(result).toBe(usernameField);
});
});
describe("given a username field that does not contain a form that matches the password field", () => {
beforeEach(() => {
usernameField.form = "invalidFormId";
usernameField.type = "tel";
});
it("returns null if the field cannot be without a form", () => {
const result = autofillService["findUsernameField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(null);
});
it("returns the field if the username field can be without a form", () => {
const result = autofillService["findUsernameField"](
pageDetails,
passwordField,
false,
false,
true,
);
expect(result).toBe(usernameField);
});
});
describe("given a field that is not viewable", () => {
beforeEach(() => {
usernameField.viewable = false;
usernameField.type = "email";
});
it("returns null if the field cannot be hidden", () => {
const result = autofillService["findUsernameField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(null);
});
it("returns the field if the field can be hidden", () => {
const result = autofillService["findUsernameField"](
pageDetails,
passwordField,
true,
false,
false,
);
expect(result).toBe(usernameField);
});
});
it("returns null if the username field does not have a type of `text`, `email`, or `tel`", () => {
usernameField.type = "checkbox";
const result = autofillService["findUsernameField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(null);
});
it("returns the username field whose attributes most closely describe the username of the password field", () => {
const usernameField2 = createAutofillFieldMock({
opid: "username-field-2",
type: "text",
form: "validFormId",
htmlName: "username",
elementNumber: 1,
});
const usernameField3 = createAutofillFieldMock({
opid: "username-field-3",
type: "text",
form: "validFormId",
elementNumber: 1,
});
passwordField.elementNumber = 3;
pageDetails.fields = [usernameField, usernameField2, usernameField3, passwordField];
const result = autofillService["findUsernameField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(usernameField2);
expect(autofillService["findMatchingFieldIndex"]).toHaveBeenCalledTimes(2);
expect(autofillService["findMatchingFieldIndex"]).not.toHaveBeenCalledWith(
usernameField3,
AutoFillConstants.UsernameFieldNames,
);
});
});
describe("findTotpField", () => {
let pageDetails: AutofillPageDetails;
let passwordField: AutofillField;
let totpField: AutofillField;
beforeEach(() => {
pageDetails = createAutofillPageDetailsMock({});
passwordField = createAutofillFieldMock({
opid: "password-field",
type: "password",
form: "validFormId",
elementNumber: 0,
});
totpField = createAutofillFieldMock({
opid: "totp-field",
type: "text",
form: "validFormId",
htmlName: "totp",
elementNumber: 1,
});
pageDetails.fields = [passwordField, totpField];
jest.spyOn(AutofillService, "forCustomFieldsOnly");
jest.spyOn(autofillService as any, "findMatchingFieldIndex");
jest.spyOn(AutofillService, "fieldIsFuzzyMatch");
});
it("returns null when passed a field that is a `span` element", () => {
const field = createAutofillFieldMock({ tagName: "span" });
pageDetails.fields = [field];
const result = autofillService["findTotpField"](pageDetails, field, false, false, false);
expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(field);
expect(result).toBe(null);
});
it("returns null if the passed totp field is disabled", () => {
totpField.disabled = true;
const result = autofillService["findTotpField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(null);
});
describe("given a field that is readonly", () => {
beforeEach(() => {
totpField.readonly = true;
});
it("returns null if the field cannot be readonly", () => {
const result = autofillService["findTotpField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(null);
});
it("returns the field if the field can be readonly", () => {
const result = autofillService["findTotpField"](
pageDetails,
passwordField,
false,
true,
false,
);
expect(result).toBe(totpField);
});
});
describe("given a totp field that does not contain a form that matches the password field", () => {
beforeEach(() => {
totpField.form = "invalidFormId";
});
it("returns null if the field cannot be without a form", () => {
const result = autofillService["findTotpField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(null);
});
it("returns the field if the username field can be without a form", () => {
const result = autofillService["findTotpField"](
pageDetails,
passwordField,
false,
false,
true,
);
expect(result).toBe(totpField);
});
});
describe("given a field that is not viewable", () => {
beforeEach(() => {
totpField.viewable = false;
totpField.type = "number";
});
it("returns null if the field cannot be hidden", () => {
const result = autofillService["findTotpField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(null);
});
it("returns the field if the field can be hidden", () => {
const result = autofillService["findTotpField"](
pageDetails,
passwordField,
true,
false,
false,
);
expect(result).toBe(totpField);
});
});
it("returns null if the totp field does not have a type of `text`, or `number`", () => {
totpField.type = "checkbox";
const result = autofillService["findTotpField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(null);
});
it("returns the field if the autoCompleteType is `one-time-code`", () => {
totpField.autoCompleteType = "one-time-code";
jest.spyOn(autofillService as any, "findMatchingFieldIndex").mockReturnValueOnce(-1);
const result = autofillService["findTotpField"](
pageDetails,
passwordField,
false,
false,
false,
);
expect(result).toBe(totpField);
});
});
describe("findMatchingFieldIndex", () => {
beforeEach(() => {
jest.spyOn(autofillService as any, "fieldPropertyIsMatch");
});
it("returns the index of a value that matches a property prefix", () => {
const attributes = [
["htmlID", "id"],
["htmlName", "name"],
["label-aria", "label"],
["label-tag", "label"],
["label-right", "label"],
["label-left", "label"],
["placeholder", "placeholder"],
];
const value = "username";
attributes.forEach((attribute) => {
const field = createAutofillFieldMock({ [attribute[0]]: value });
const result = autofillService["findMatchingFieldIndex"](field, [
`${attribute[1]}=${value}`,
]);
expect(autofillService["fieldPropertyIsMatch"]).toHaveBeenCalledWith(
field,
attribute[0],
value,
);
expect(result).toBe(0);
});
});
it("returns the index of a value that matches a property", () => {
const attributes = [
"htmlID",
"htmlName",
"label-aria",
"label-tag",
"label-right",
"label-left",
"placeholder",
];
const value = "username";
attributes.forEach((attribute) => {
const field = createAutofillFieldMock({ [attribute]: value });
const result = autofillService["findMatchingFieldIndex"](field, [value]);
expect(result).toBe(0);
});
});
});
describe("fieldPropertyIsPrefixMatch", () => {
it("returns true if the field contains a property whose value is a match", () => {
const field = createAutofillFieldMock({ htmlID: "username" });
const result = autofillService["fieldPropertyIsPrefixMatch"](
field,
"htmlID",
"id=username",
"id",
);
expect(result).toBe(true);
});
it("returns false if the field contains a property whose value is not a match", () => {
const field = createAutofillFieldMock({ htmlID: "username" });
const result = autofillService["fieldPropertyIsPrefixMatch"](
field,
"htmlID",
"id=some-othername",
"id",
);
expect(result).toBe(false);
});
});
describe("fieldPropertyIsMatch", () => {
let field: AutofillField;
beforeEach(() => {
field = createAutofillFieldMock();
jest.spyOn(AutofillService, "hasValue");
});
it("returns false if the property within the field does not have a value", () => {
field.htmlID = "";
const result = autofillService["fieldPropertyIsMatch"](field, "htmlID", "some-value");
expect(AutofillService.hasValue).toHaveBeenCalledWith("");
expect(result).toBe(false);
});
it("returns true if the property within the field provides a value that is equal to the passed `name`", () => {
field.htmlID = "some-value";
const result = autofillService["fieldPropertyIsMatch"](field, "htmlID", "some-value");
expect(AutofillService.hasValue).toHaveBeenCalledWith("some-value");
expect(result).toBe(true);
});
describe("given a passed `name` value that is expecting a regex check", () => {
it("returns false if the property within the field fails the `name` regex check", () => {
field.htmlID = "some-false-value";
const result = autofillService["fieldPropertyIsMatch"](field, "htmlID", "regex=some-value");
expect(result).toBe(false);
});
it("returns true if the property within the field equals the `name` regex check", () => {
field.htmlID = "some-value";
const result = autofillService["fieldPropertyIsMatch"](field, "htmlID", "regex=some-value");
expect(result).toBe(true);
});
it("returns true if the property within the field has a partial match to the `name` regex check", () => {
field.htmlID = "some-value";
const result = autofillService["fieldPropertyIsMatch"](field, "htmlID", "regex=value");
expect(result).toBe(true);
});
it("will log an error when the regex triggers a catch block", () => {
field.htmlID = "some-value";
jest.spyOn(autofillService["logService"], "error");
const result = autofillService["fieldPropertyIsMatch"](field, "htmlID", "regex=+");
expect(autofillService["logService"].error).toHaveBeenCalled();
expect(result).toBe(false);
});
});
describe("given a passed `name` value that is checking comma separated values", () => {
it("returns false if the property within the field does not have a value that matches the values within the `name` CSV", () => {
field.htmlID = "some-false-value";
const result = autofillService["fieldPropertyIsMatch"](
field,
"htmlID",
"csv=some-value,some-other-value,some-third-value",
);
expect(result).toBe(false);
});
it("returns true if the property within the field matches a value within the `name` CSV", () => {
field.htmlID = "some-other-value";
const result = autofillService["fieldPropertyIsMatch"](
field,
"htmlID",
"csv=some-value,some-other-value,some-third-value",
);
expect(result).toBe(true);
});
});
});
describe("fieldIsFuzzyMatch", () => {
let field: AutofillField;
const fieldProperties = [
"htmlID",
"htmlName",
"label-aria",
"label-tag",
"label-top",
"label-left",
"placeholder",
];
beforeEach(() => {
field = createAutofillFieldMock();
jest.spyOn(AutofillService, "hasValue");
jest.spyOn(AutofillService as any, "fuzzyMatch");
});
it("returns false if the field properties do not have any values", () => {
fieldProperties.forEach((property) => {
field[property] = "";
});
const result = AutofillService["fieldIsFuzzyMatch"](field, ["some-value"]);
expect(AutofillService.hasValue).toHaveBeenCalledTimes(7);
expect(AutofillService["fuzzyMatch"]).not.toHaveBeenCalled();
expect(result).toBe(false);
});
it("returns false if the field properties do not have a value that is a fuzzy match", () => {
fieldProperties.forEach((property) => {
field[property] = "some-false-value";
const result = AutofillService["fieldIsFuzzyMatch"](field, ["some-value"]);
expect(AutofillService.hasValue).toHaveBeenCalled();
expect(AutofillService["fuzzyMatch"]).toHaveBeenCalledWith(
["some-value"],
"some-false-value",
);
expect(result).toBe(false);
field[property] = "";
});
});
it("returns true if the field property has a value that is a fuzzy match", () => {
fieldProperties.forEach((property) => {
field[property] = "some-value";
const result = AutofillService["fieldIsFuzzyMatch"](field, ["some-value"]);
expect(AutofillService.hasValue).toHaveBeenCalled();
expect(AutofillService["fuzzyMatch"]).toHaveBeenCalledWith(["some-value"], "some-value");
expect(result).toBe(true);
field[property] = "";
});
});
});
describe("fuzzyMatch", () => {
it("returns false if the passed options is null", () => {
const result = AutofillService["fuzzyMatch"](null, "some-value");
expect(result).toBe(false);
});
it("returns false if the passed options contains an empty array", () => {
const result = AutofillService["fuzzyMatch"]([], "some-value");
expect(result).toBe(false);
});
it("returns false if the passed value is null", () => {
const result = AutofillService["fuzzyMatch"](["some-value"], null);
expect(result).toBe(false);
});
it("returns false if the passed value is an empty string", () => {
const result = AutofillService["fuzzyMatch"](["some-value"], "");
expect(result).toBe(false);
});
it("returns false if the passed value is not present in the options array", () => {
const result = AutofillService["fuzzyMatch"](["some-value"], "some-other-value");
expect(result).toBe(false);
});
it("returns true if the passed value is within the options array", () => {
const result = AutofillService["fuzzyMatch"](
["some-other-value", "some-value"],
"some-value",
);
expect(result).toBe(true);
});
});
describe("hasValue", () => {
it("returns false if the passed string is null", () => {
const result = AutofillService.hasValue(null);
expect(result).toBe(false);
});
it("returns false if the passed string is an empty string", () => {
const result = AutofillService.hasValue("");
expect(result).toBe(false);
});
it("returns true if the passed string is not null or an empty string", () => {
const result = AutofillService.hasValue("some-value");
expect(result).toBe(true);
});
});
describe("setFillScriptForFocus", () => {
let usernameField: AutofillField;
let passwordField: AutofillField;
let filledFields: { [key: string]: AutofillField };
let fillScript: AutofillScript;
beforeEach(() => {
usernameField = createAutofillFieldMock({
opid: "username-field",
type: "text",
form: "validFormId",
elementNumber: 0,
});
passwordField = createAutofillFieldMock({
opid: "password-field",
type: "password",
form: "validFormId",
elementNumber: 1,
});
filledFields = {
"username-field": usernameField,
"password-field": passwordField,
};
fillScript = createAutofillScriptMock({ script: [] });
});
it("returns a fill script with an unmodified actions list if an empty filledFields value is passed", () => {
const result = AutofillService.setFillScriptForFocus({}, fillScript);
expect(result.script).toStrictEqual([]);
});
it("returns a fill script with the password field prioritized when adding a `focus_by_opid` action", () => {
const result = AutofillService.setFillScriptForFocus(filledFields, fillScript);
expect(result.script).toStrictEqual([["focus_by_opid", "password-field"]]);
});
it("returns a fill script with the username field if a password field is not present when adding a `focus_by_opid` action", () => {
delete filledFields["password-field"];
const result = AutofillService.setFillScriptForFocus(filledFields, fillScript);
expect(result.script).toStrictEqual([["focus_by_opid", "username-field"]]);
});
});
describe("fillByOpid", () => {
let usernameField: AutofillField;
let fillScript: AutofillScript;
beforeEach(() => {
usernameField = createAutofillFieldMock({
opid: "username-field",
type: "text",
form: "validFormId",
elementNumber: 0,
});
fillScript = createAutofillScriptMock({ script: [] });
});
it("returns a list of fill script actions for the passed field", () => {
usernameField.maxLength = 5;
AutofillService.fillByOpid(fillScript, usernameField, "some-long-value");
expect(fillScript.script).toStrictEqual([
["click_on_opid", "username-field"],
["focus_by_opid", "username-field"],
["fill_by_opid", "username-field", "some-long-value"],
]);
});
it("returns only the `fill_by_opid` action if the passed field is a `span` element", () => {
usernameField.tagName = "span";
AutofillService.fillByOpid(fillScript, usernameField, "some-long-value");
expect(fillScript.script).toStrictEqual([
["fill_by_opid", "username-field", "some-long-value"],
]);
});
});
describe("forCustomFieldsOnly", () => {
it("returns a true value if the passed field has a tag name of `span`", () => {
const field = createAutofillFieldMock({ tagName: "span" });
const result = AutofillService.forCustomFieldsOnly(field);
expect(result).toBe(true);
});
it("returns a false value if the passed field does not have a tag name of `span`", () => {
const field = createAutofillFieldMock({ tagName: "input" });
const result = AutofillService.forCustomFieldsOnly(field);
expect(result).toBe(false);
});
});
describe("isDebouncingPasswordRepromptPopout", () => {
it("returns false and sets up the debounce if a master password reprompt window is not currently opening", () => {
jest.spyOn(globalThis, "setTimeout");
const result = autofillService["isDebouncingPasswordRepromptPopout"]();
expect(result).toBe(false);
expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), 100);
expect(autofillService["currentlyOpeningPasswordRepromptPopout"]).toBe(true);
});
it("returns true if a master password reprompt window is currently opening", () => {
autofillService["currentlyOpeningPasswordRepromptPopout"] = true;
const result = autofillService["isDebouncingPasswordRepromptPopout"]();
expect(result).toBe(true);
});
it("resets the currentlyOpeningPasswordRepromptPopout value to false after the debounce has occurred", () => {
jest.useFakeTimers();
const result = autofillService["isDebouncingPasswordRepromptPopout"]();
jest.advanceTimersByTime(100);
expect(result).toBe(false);
expect(autofillService["currentlyOpeningPasswordRepromptPopout"]).toBe(false);
});
});
describe("handleInjectedScriptPortConnection", () => {
it("ignores port connections that do not have the correct port name", () => {
const port = mock<chrome.runtime.Port>({
name: "some-invalid-port-name",
onDisconnect: { addListener: jest.fn() },
}) as any;
autofillService["handleInjectedScriptPortConnection"](port);
expect(port.onDisconnect.addListener).not.toHaveBeenCalled();
expect(autofillService["autofillScriptPortsSet"].size).toBe(0);
});
it("adds the connect port to the set of injected script ports and sets up an onDisconnect listener", () => {
const port = mock<chrome.runtime.Port>({
name: AutofillPort.InjectedScript,
onDisconnect: { addListener: jest.fn() },
}) as any;
jest.spyOn(autofillService as any, "handleInjectScriptPortOnDisconnect");
autofillService["handleInjectedScriptPortConnection"](port);
expect(port.onDisconnect.addListener).toHaveBeenCalledWith(
autofillService["handleInjectScriptPortOnDisconnect"],
);
expect(autofillService["autofillScriptPortsSet"].size).toBe(1);
});
});
describe("handleInjectScriptPortOnDisconnect", () => {
it("ignores port disconnections that do not have the correct port name", () => {
autofillService["autofillScriptPortsSet"].add(mock<chrome.runtime.Port>());
autofillService["handleInjectScriptPortOnDisconnect"](
mock<chrome.runtime.Port>({
name: "some-invalid-port-name",
}),
);
expect(autofillService["autofillScriptPortsSet"].size).toBe(1);
});
it("removes the port from the set of injected script ports", () => {
const port = mock<chrome.runtime.Port>({
name: AutofillPort.InjectedScript,
}) as any;
autofillService["autofillScriptPortsSet"].add(port);
autofillService["handleInjectScriptPortOnDisconnect"](port);
expect(autofillService["autofillScriptPortsSet"].size).toBe(0);
});
});
});