bitwarden-browser/apps/browser/src/autofill/background/notification.background.spe...

1365 lines
55 KiB
TypeScript

import { mock } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
import { BrowserApi } from "../../platform/browser/browser-api";
import { BrowserStateService } from "../../platform/services/browser-state.service";
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
import { FormData } from "../services/abstractions/autofill.service";
import AutofillService from "../services/autofill.service";
import { createAutofillPageDetailsMock, createChromeTabMock } from "../spec/autofill-mocks";
import { flushPromises, sendExtensionRuntimeMessage } from "../spec/testing-utils";
import {
AddChangePasswordQueueMessage,
AddLoginQueueMessage,
AddUnlockVaultQueueMessage,
LockedVaultPendingNotificationsData,
NotificationBackgroundExtensionMessage,
} from "./abstractions/notification.background";
import NotificationBackground from "./notification.background";
jest.mock("rxjs", () => {
const rxjs = jest.requireActual("rxjs");
const { firstValueFrom } = rxjs;
return {
...rxjs,
firstValueFrom: jest.fn(firstValueFrom),
};
});
describe("NotificationBackground", () => {
let notificationBackground: NotificationBackground;
const autofillService = mock<AutofillService>();
const cipherService = mock<CipherService>();
const authService = mock<AuthService>();
const policyService = mock<PolicyService>();
const folderService = mock<FolderService>();
const stateService = mock<BrowserStateService>();
const userNotificationSettingsService = mock<UserNotificationSettingsService>();
const domainSettingsService = mock<DomainSettingsService>();
const environmentService = mock<EnvironmentService>();
const logService = mock<LogService>();
const themeStateService = mock<ThemeStateService>();
beforeEach(() => {
notificationBackground = new NotificationBackground(
autofillService,
cipherService,
authService,
policyService,
folderService,
stateService,
userNotificationSettingsService,
domainSettingsService,
environmentService,
logService,
themeStateService,
);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("convertAddLoginQueueMessageToCipherView", () => {
it("returns a cipher view when passed an `AddLoginQueueMessage`", () => {
const message: AddLoginQueueMessage = {
type: "add",
username: "test",
password: "password",
uri: "https://example.com",
domain: "",
tab: createChromeTabMock(),
expires: new Date(),
wasVaultLocked: false,
};
const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"](message);
expect(cipherView.name).toEqual("example.com");
expect(cipherView.login).toEqual({
autofillOnPageLoad: null,
fido2Credentials: null,
password: message.password,
passwordRevisionDate: null,
totp: null,
uris: [
{
_canLaunch: null,
_domain: null,
_host: null,
_hostname: null,
_uri: message.uri,
match: null,
},
],
username: message.username,
});
});
it("returns a cipher view assigned to an existing folder id", () => {
const folderId = "folder-id";
const message: AddLoginQueueMessage = {
type: "add",
username: "test",
password: "password",
uri: "https://example.com",
domain: "example.com",
tab: createChromeTabMock(),
expires: new Date(),
wasVaultLocked: false,
};
const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"](
message,
folderId,
);
expect(cipherView.folderId).toEqual(folderId);
});
});
describe("notification bar extension message handlers", () => {
beforeEach(async () => {
await notificationBackground.init();
});
it("ignores messages whose command does not match the expected handlers", () => {
const message: NotificationBackgroundExtensionMessage = { command: "unknown" };
jest.spyOn(notificationBackground as any, "handleSaveCipherMessage");
sendExtensionRuntimeMessage(message);
expect(notificationBackground["handleSaveCipherMessage"]).not.toHaveBeenCalled();
});
describe("unlockCompleted message handler", () => {
it("sends a `closeNotificationBar` message if the retryCommand is for `autofill_login", async () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const message: NotificationBackgroundExtensionMessage = {
command: "unlockCompleted",
data: {
commandToRetry: { message: { command: "autofill_login" } },
} as LockedVaultPendingNotificationsData,
};
jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
sender.tab,
"closeNotificationBar",
);
});
it("triggers a retryHandler if the message target is `notification.background` and a handler exists", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "unlockCompleted",
data: {
commandToRetry: { message: { command: "bgSaveCipher" } },
target: "notification.background",
} as LockedVaultPendingNotificationsData,
};
jest.spyOn(notificationBackground as any, "handleSaveCipherMessage").mockImplementation();
sendExtensionRuntimeMessage(message);
await flushPromises();
expect(notificationBackground["handleSaveCipherMessage"]).toHaveBeenCalledWith(
message.data.commandToRetry.message,
message.data.commandToRetry.sender,
);
});
});
describe("bgGetFolderData message handler", () => {
it("returns a list of folders", async () => {
const folderView = mock<FolderView>({ id: "folder-id" });
const folderViews = [folderView];
const message: NotificationBackgroundExtensionMessage = {
command: "bgGetFolderData",
};
jest.spyOn(notificationBackground as any, "getFolderData");
(firstValueFrom as jest.Mock).mockResolvedValueOnce(folderViews);
sendExtensionRuntimeMessage(message);
await flushPromises();
expect(notificationBackground["getFolderData"]).toHaveBeenCalled();
expect(firstValueFrom).toHaveBeenCalled();
});
});
describe("bgCloseNotificationBar message handler", () => {
it("sends a `closeNotificationBar` message to the sender tab", async () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const message: NotificationBackgroundExtensionMessage = {
command: "bgCloseNotificationBar",
};
jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
sender.tab,
"closeNotificationBar",
);
});
});
describe("bgAdjustNotificationBar message handler", () => {
it("sends a `adjustNotificationBar` message to the sender tab", async () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const message: NotificationBackgroundExtensionMessage = {
command: "bgAdjustNotificationBar",
data: { height: 100 },
};
jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith(
sender.tab,
"adjustNotificationBar",
message.data,
);
});
});
describe("bgAddLogin message handler", () => {
let tab: chrome.tabs.Tab;
let sender: chrome.runtime.MessageSender;
let getAuthStatusSpy: jest.SpyInstance;
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
let pushAddLoginToQueueSpy: jest.SpyInstance;
let pushChangePasswordToQueueSpy: jest.SpyInstance;
let getAllDecryptedForUrlSpy: jest.SpyInstance;
beforeEach(() => {
tab = createChromeTabMock();
sender = mock<chrome.runtime.MessageSender>({ tab });
getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus");
getEnableAddedLoginPromptSpy = jest.spyOn(
notificationBackground as any,
"getEnableAddedLoginPrompt",
);
getEnableChangedPasswordPromptSpy = jest.spyOn(
notificationBackground as any,
"getEnableChangedPasswordPrompt",
);
pushAddLoginToQueueSpy = jest.spyOn(notificationBackground as any, "pushAddLoginToQueue");
pushChangePasswordToQueueSpy = jest.spyOn(
notificationBackground as any,
"pushChangePasswordToQueue",
);
getAllDecryptedForUrlSpy = jest.spyOn(cipherService, "getAllDecryptedForUrl");
});
it("skips attempting to add the login if the user is logged out", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgAddLogin",
login: { username: "test", password: "password", url: "https://example.com" },
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.LoggedOut);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
});
it("skips attempting to add the login if the login data does not contain a valid url", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgAddLogin",
login: { username: "test", password: "password", url: "" },
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
});
it("skips attempting to add the login if the user with a locked vault has disabled the login notification", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgAddLogin",
login: { username: "test", password: "password", url: "https://example.com" },
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).not.toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("skips attempting to add the login if the user with an unlocked vault has disabled the login notification", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgAddLogin",
login: { username: "test", password: "password", url: "https://example.com" },
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("skips attempting to change the password for an existing login if the user has disabled changing the password notification", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgAddLogin",
login: { username: "test", password: "password", url: "https://example.com" },
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "oldPassword" } }),
]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(getEnableChangedPasswordPromptSpy).toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("skips attempting to change the password for an existing login if the password has not changed", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgAddLogin",
login: { username: "test", password: "password", url: "https://example.com" },
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "password" } }),
]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("adds the login to the queue if the user has a locked account", async () => {
const login = { username: "test", password: "password", url: "https://example.com" };
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab, true);
});
it("adds the login to the queue if the user has an unlocked account and the login is new", async () => {
const login = {
username: undefined,
password: "password",
url: "https://example.com",
} as any;
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "anotherTestUsername", password: "password" } }),
]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab);
});
it("adds a change password message to the queue if the user has changed an existing cipher's password", async () => {
const login = { username: "tEsT", password: "password", url: "https://example.com" };
const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login };
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true);
getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({
id: "cipher-id",
login: { username: "test", password: "oldPassword" },
}),
]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
"cipher-id",
"example.com",
login.password,
sender.tab,
);
});
});
describe("bgChangedPassword message handler", () => {
let tab: chrome.tabs.Tab;
let sender: chrome.runtime.MessageSender;
let getAuthStatusSpy: jest.SpyInstance;
let pushChangePasswordToQueueSpy: jest.SpyInstance;
let getAllDecryptedForUrlSpy: jest.SpyInstance;
beforeEach(() => {
tab = createChromeTabMock();
sender = mock<chrome.runtime.MessageSender>({ tab });
getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus");
pushChangePasswordToQueueSpy = jest.spyOn(
notificationBackground as any,
"pushChangePasswordToQueue",
);
getAllDecryptedForUrlSpy = jest.spyOn(cipherService, "getAllDecryptedForUrl");
});
it("skips attempting to add the change password message to the queue if the passed url is not valid", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgChangedPassword",
data: { newPassword: "newPassword", currentPassword: "currentPassword", url: "" },
};
sendExtensionRuntimeMessage(message);
await flushPromises();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("adds a change password message to the queue if the user does not have an unlocked account", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgChangedPassword",
data: {
newPassword: "newPassword",
currentPassword: "currentPassword",
url: "https://example.com",
},
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
null,
"example.com",
message.data.newPassword,
sender.tab,
true,
);
});
it("skips adding a change password message to the queue if the multiple ciphers exist for the passed URL and the current password is not found within the list of ciphers", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgChangedPassword",
data: {
newPassword: "newPassword",
currentPassword: "currentPassword",
url: "https://example.com",
},
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "password" } }),
]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("skips adding a change password message if more than one existing cipher is found with a matching password ", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgChangedPassword",
data: {
newPassword: "newPassword",
currentPassword: "currentPassword",
url: "https://example.com",
},
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "password" } }),
mock<CipherView>({ login: { username: "test2", password: "password" } }),
]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("adds a change password message to the queue if a single cipher matches the passed current password", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgChangedPassword",
data: {
newPassword: "newPassword",
currentPassword: "currentPassword",
url: "https://example.com",
},
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({
id: "cipher-id",
login: { username: "test", password: "currentPassword" },
}),
]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
"cipher-id",
"example.com",
message.data.newPassword,
sender.tab,
);
});
it("skips adding a change password message if no current password is passed in the message and more than one cipher is found for a url", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgChangedPassword",
data: {
newPassword: "newPassword",
url: "https://example.com",
},
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({ login: { username: "test", password: "password" } }),
mock<CipherView>({ login: { username: "test2", password: "password" } }),
]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
});
it("adds a change password message to the queue if no current password is passed with the message, but a single cipher is matched for the uri", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgChangedPassword",
data: {
newPassword: "newPassword",
url: "https://example.com",
},
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
mock<CipherView>({
id: "cipher-id",
login: { username: "test", password: "password" },
}),
]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
"cipher-id",
"example.com",
message.data.newPassword,
sender.tab,
);
});
});
describe("bgRemoveTabFromNotificationQueue message handler", () => {
it("splices a notification queue item based on the passed tab", async () => {
const tab = createChromeTabMock({ id: 2 });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgRemoveTabFromNotificationQueue",
};
const removeTabFromNotificationQueueSpy = jest.spyOn(
notificationBackground as any,
"removeTabFromNotificationQueue",
);
const firstQueueMessage = mock<AddLoginQueueMessage>({
tab: createChromeTabMock({ id: 1 }),
});
const secondQueueMessage = mock<AddLoginQueueMessage>({ tab });
const thirdQueueMessage = mock<AddLoginQueueMessage>({
tab: createChromeTabMock({ id: 3 }),
});
notificationBackground["notificationQueue"] = [
firstQueueMessage,
secondQueueMessage,
thirdQueueMessage,
];
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(removeTabFromNotificationQueueSpy).toHaveBeenCalledWith(tab);
expect(notificationBackground["notificationQueue"]).toEqual([
firstQueueMessage,
thirdQueueMessage,
]);
});
});
describe("bgSaveCipher message handler", () => {
let getAuthStatusSpy: jest.SpyInstance;
let tabSendMessageDataSpy: jest.SpyInstance;
let openUnlockPopoutSpy: jest.SpyInstance;
beforeEach(() => {
getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus");
tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
openUnlockPopoutSpy = jest
.spyOn(notificationBackground as any, "openUnlockPopout")
.mockImplementation();
});
it("skips saving the cipher and opens an unlock popout if the extension is not unlocked", async () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
sender.tab,
"addToLockedVaultPendingNotifications",
{
commandToRetry: { message, sender },
target: "notification.background",
},
);
expect(openUnlockPopoutSpy).toHaveBeenCalledWith(sender.tab);
});
describe("saveOrUpdateCredentials", () => {
let getDecryptedCipherByIdSpy: jest.SpyInstance;
let getAllDecryptedForUrlSpy: jest.SpyInstance;
let updatePasswordSpy: jest.SpyInstance;
let convertAddLoginQueueMessageToCipherViewSpy: jest.SpyInstance;
let tabSendMessageSpy: jest.SpyInstance;
let editItemSpy: jest.SpyInstance;
let setAddEditCipherInfoSpy: jest.SpyInstance;
let openAddEditVaultItemPopoutSpy: jest.SpyInstance;
let createWithServerSpy: jest.SpyInstance;
let updateWithServerSpy: jest.SpyInstance;
let folderExistsSpy: jest.SpyInstance;
let cipherEncryptSpy: jest.SpyInstance;
beforeEach(() => {
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked);
getDecryptedCipherByIdSpy = jest.spyOn(
notificationBackground as any,
"getDecryptedCipherById",
);
getAllDecryptedForUrlSpy = jest.spyOn(cipherService, "getAllDecryptedForUrl");
updatePasswordSpy = jest.spyOn(notificationBackground as any, "updatePassword");
convertAddLoginQueueMessageToCipherViewSpy = jest.spyOn(
notificationBackground as any,
"convertAddLoginQueueMessageToCipherView",
);
tabSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage").mockImplementation();
editItemSpy = jest.spyOn(notificationBackground as any, "editItem");
setAddEditCipherInfoSpy = jest.spyOn(stateService, "setAddEditCipherInfo");
openAddEditVaultItemPopoutSpy = jest.spyOn(
notificationBackground as any,
"openAddEditVaultItemPopout",
);
createWithServerSpy = jest.spyOn(cipherService, "createWithServer");
updateWithServerSpy = jest.spyOn(cipherService, "updateWithServer");
folderExistsSpy = jest.spyOn(notificationBackground as any, "folderExists");
cipherEncryptSpy = jest.spyOn(cipherService, "encrypt");
});
it("skips saving the cipher if the notification queue does not have a tab that is related to the sender", async () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 2 } });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
notificationBackground["notificationQueue"] = [
mock<AddLoginQueueMessage>({
tab: createChromeTabMock({ id: 1 }),
}),
];
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(updatePasswordSpy).not.toHaveBeenCalled();
expect(editItemSpy).not.toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled();
});
it("skips saving the cipher if the notification queue does not contain an AddLogin or ChangePassword type", async () => {
const tab = createChromeTabMock({ id: 1 });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
notificationBackground["notificationQueue"] = [
mock<AddUnlockVaultQueueMessage>({
tab,
type: NotificationQueueMessageType.UnlockVault,
}),
];
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(updatePasswordSpy).not.toHaveBeenCalled();
expect(editItemSpy).not.toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled();
});
it("skips saving the cipher if the notification queue message has a different domain than the passed tab", () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
notificationBackground["notificationQueue"] = [
mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab,
domain: "another.com",
}),
];
sendExtensionRuntimeMessage(message, sender);
expect(updatePasswordSpy).not.toHaveBeenCalled();
expect(editItemSpy).not.toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled();
});
it("updates the password if the notification message type is for ChangePassword", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
const queueMessage = mock<AddChangePasswordQueueMessage>({
type: NotificationQueueMessageType.ChangePassword,
tab,
domain: "example.com",
newPassword: "newPassword",
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>();
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(editItemSpy).not.toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled();
expect(updatePasswordSpy).toHaveBeenCalledWith(
cipherView,
queueMessage.newPassword,
message.edit,
sender.tab,
);
expect(updateWithServerSpy).toHaveBeenCalled();
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "saveCipherAttemptCompleted",
});
});
it("updates the cipher password if the queue message was locked and an existing cipher has the same username as the message", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
const queueMessage = mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab,
domain: "example.com",
username: "test",
password: "updated-password",
wasVaultLocked: true,
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
login: { username: "test", password: "old-password" },
});
getAllDecryptedForUrlSpy.mockResolvedValueOnce([cipherView]);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(updatePasswordSpy).toHaveBeenCalledWith(
cipherView,
queueMessage.password,
message.edit,
sender.tab,
);
expect(editItemSpy).not.toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled();
});
it("opens an editItem window and closes the notification bar if the edit value is within the passed message when attempting to update an existing cipher", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: true,
folder: "folder-id",
};
const queueMessage = mock<AddChangePasswordQueueMessage>({
type: NotificationQueueMessageType.ChangePassword,
tab,
domain: "example.com",
newPassword: "newPassword",
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>();
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
setAddEditCipherInfoSpy.mockResolvedValue(undefined);
openAddEditVaultItemPopoutSpy.mockResolvedValue(undefined);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(updatePasswordSpy).toHaveBeenCalledWith(
cipherView,
queueMessage.newPassword,
message.edit,
sender.tab,
);
expect(editItemSpy).toHaveBeenCalled();
expect(updateWithServerSpy).not.toHaveBeenCalled();
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "closeNotificationBar",
});
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "editedCipher",
});
expect(setAddEditCipherInfoSpy).toHaveBeenCalledWith({
cipher: cipherView,
collectionIds: cipherView.collectionIds,
});
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalledWith(sender.tab, {
cipherId: cipherView.id,
});
});
it("opens an editItem window and closes the notification bar if the edit value is within the passed message when attempting to save the cipher", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: true,
folder: "folder-id",
};
const queueMessage = mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab,
domain: "example.com",
username: "test",
password: "password",
wasVaultLocked: false,
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
login: { username: "test", password: "password" },
});
folderExistsSpy.mockResolvedValueOnce(true);
convertAddLoginQueueMessageToCipherViewSpy.mockReturnValueOnce(cipherView);
editItemSpy.mockResolvedValueOnce(undefined);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(updatePasswordSpy).not.toHaveBeenCalled();
expect(convertAddLoginQueueMessageToCipherViewSpy).toHaveBeenCalledWith(
queueMessage,
message.folder,
);
expect(editItemSpy).toHaveBeenCalledWith(cipherView, sender.tab);
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "closeNotificationBar",
});
expect(createWithServerSpy).not.toHaveBeenCalled();
});
it("creates the cipher within the server and sends an `saveCipherAttemptCompleted` and `addedCipher` message to the sender tab", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
const queueMessage = mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab,
domain: "example.com",
username: "test",
password: "password",
wasVaultLocked: false,
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
login: { username: "test", password: "password" },
});
folderExistsSpy.mockResolvedValueOnce(false);
convertAddLoginQueueMessageToCipherViewSpy.mockReturnValueOnce(cipherView);
editItemSpy.mockResolvedValueOnce(undefined);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(convertAddLoginQueueMessageToCipherViewSpy).toHaveBeenCalledWith(
queueMessage,
null,
);
expect(cipherEncryptSpy).toHaveBeenCalledWith(cipherView);
expect(createWithServerSpy).toHaveBeenCalled();
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "saveCipherAttemptCompleted",
});
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "addedCipher" });
});
it("sends an error message within the `saveCipherAttemptCompleted` message if the cipher cannot be saved to the server", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
const queueMessage = mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab,
domain: "example.com",
username: "test",
password: "password",
wasVaultLocked: false,
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>({
login: { username: "test", password: "password" },
});
folderExistsSpy.mockResolvedValueOnce(true);
convertAddLoginQueueMessageToCipherViewSpy.mockReturnValueOnce(cipherView);
editItemSpy.mockResolvedValueOnce(undefined);
const errorMessage = "fetch error";
createWithServerSpy.mockImplementation(() => {
throw new Error(errorMessage);
});
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(cipherEncryptSpy).toHaveBeenCalledWith(cipherView);
expect(createWithServerSpy).toThrow(errorMessage);
expect(tabSendMessageSpy).not.toHaveBeenCalledWith(sender.tab, {
command: "addedCipher",
});
expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
sender.tab,
"saveCipherAttemptCompleted",
{
error: errorMessage,
},
);
});
it("sends an error message within the `saveCipherAttemptCompleted` message if the cipher cannot be updated within the server", async () => {
const tab = createChromeTabMock({ id: 1, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "bgSaveCipher",
edit: false,
folder: "folder-id",
};
const queueMessage = mock<AddChangePasswordQueueMessage>({
type: NotificationQueueMessageType.ChangePassword,
tab,
domain: "example.com",
newPassword: "newPassword",
});
notificationBackground["notificationQueue"] = [queueMessage];
const cipherView = mock<CipherView>();
getDecryptedCipherByIdSpy.mockResolvedValueOnce(cipherView);
const errorMessage = "fetch error";
updateWithServerSpy.mockImplementation(() => {
throw new Error(errorMessage);
});
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(updateWithServerSpy).toThrow(errorMessage);
expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
sender.tab,
"saveCipherAttemptCompleted",
{
error: errorMessage,
},
);
});
});
});
describe("bgNeverSave message handler", () => {
let tabSendMessageDataSpy: jest.SpyInstance;
beforeEach(() => {
tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData");
});
it("skips saving the domain as a never value if the passed tab does not exist within the notification queue", async () => {
const tab = createChromeTabMock({ id: 2 });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" };
notificationBackground["notificationQueue"] = [
mock<AddLoginQueueMessage>({
tab: createChromeTabMock({ id: 1 }),
}),
mock<AddLoginQueueMessage>({
tab: createChromeTabMock({ id: 3 }),
}),
];
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(tabSendMessageDataSpy).not.toHaveBeenCalled();
});
it("skips saving the domain as a never value if the tab does not contain an addLogin message within the NotificationQueue", async () => {
const tab = createChromeTabMock({ id: 2 });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" };
notificationBackground["notificationQueue"] = [
mock<AddUnlockVaultQueueMessage>({ type: NotificationQueueMessageType.UnlockVault, tab }),
];
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(tabSendMessageDataSpy).not.toHaveBeenCalled();
});
it("skips saving the domain as a never value if the tab url does not match the queue message domain", async () => {
const tab = createChromeTabMock({ id: 2, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" };
notificationBackground["notificationQueue"] = [
mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab,
domain: "another.com",
}),
];
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(tabSendMessageDataSpy).not.toHaveBeenCalled();
});
it("saves the tabs domain as a never value and closes the notification bar", async () => {
const tab = createChromeTabMock({ id: 2, url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" };
const firstNotification = mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab,
domain: "example.com",
});
const secondNotification = mock<AddLoginQueueMessage>({
type: NotificationQueueMessageType.AddLogin,
tab: createChromeTabMock({ id: 3 }),
domain: "another.com",
});
notificationBackground["notificationQueue"] = [firstNotification, secondNotification];
jest.spyOn(cipherService, "saveNeverDomain").mockImplementation();
jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation();
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(tabSendMessageDataSpy).toHaveBeenCalledWith(tab, "closeNotificationBar");
expect(cipherService.saveNeverDomain).toHaveBeenCalledWith("example.com");
expect(notificationBackground["notificationQueue"]).toEqual([secondNotification]);
});
});
describe("collectPageDetailsResponse", () => {
let tabSendMessageDataSpy: jest.SpyInstance;
beforeEach(() => {
tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData");
});
it("skips sending the `notificationBarPageDetails` message if the message sender is not `notificationBar`", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "collectPageDetailsResponse",
sender: "not-notificationBar",
};
sendExtensionRuntimeMessage(message);
await flushPromises();
expect(tabSendMessageDataSpy).not.toHaveBeenCalled();
});
it("sends a `notificationBarPageDetails` message with the forms with password fields", async () => {
const tab = createChromeTabMock();
const message: NotificationBackgroundExtensionMessage = {
command: "collectPageDetailsResponse",
sender: "notificationBar",
details: createAutofillPageDetailsMock(),
tab,
};
const formData = [mock<FormData>()];
jest.spyOn(autofillService, "getFormsWithPasswordFields").mockReturnValueOnce(formData);
sendExtensionRuntimeMessage(message);
await flushPromises();
expect(tabSendMessageDataSpy).toHaveBeenCalledWith(
message.tab,
"notificationBarPageDetails",
{
details: message.details,
forms: formData,
},
);
});
});
describe("bgUnlockPopoutOpened message handler", () => {
let getAuthStatusSpy: jest.SpyInstance;
let pushUnlockVaultToQueueSpy: jest.SpyInstance;
beforeEach(() => {
getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus");
pushUnlockVaultToQueueSpy = jest.spyOn(
notificationBackground as any,
"pushUnlockVaultToQueue",
);
});
it("skips pushing the unlock vault message to the queue if the message indicates that the notification should be skipped", async () => {
const tabMock = createChromeTabMock();
const sender = mock<chrome.runtime.MessageSender>({ tab: tabMock });
const message: NotificationBackgroundExtensionMessage = {
command: "bgUnlockPopoutOpened",
data: { skipNotification: true },
};
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).not.toHaveBeenCalled();
expect(pushUnlockVaultToQueueSpy).not.toHaveBeenCalled();
});
it("skips pushing the unlock vault message to the queue if the auth status is not `Locked`", async () => {
const tabMock = createChromeTabMock();
const sender = mock<chrome.runtime.MessageSender>({ tab: tabMock });
const message: NotificationBackgroundExtensionMessage = {
command: "bgUnlockPopoutOpened",
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.LoggedOut);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(getAuthStatusSpy).toHaveBeenCalled();
expect(pushUnlockVaultToQueueSpy).not.toHaveBeenCalled();
});
it("skips pushing the unlock vault message to the queue if the notification queue already has an item", async () => {
const tabMock = createChromeTabMock();
const sender = mock<chrome.runtime.MessageSender>({ tab: tabMock });
const message: NotificationBackgroundExtensionMessage = {
command: "bgUnlockPopoutOpened",
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
notificationBackground["notificationQueue"] = [mock<AddLoginQueueMessage>()];
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(pushUnlockVaultToQueueSpy).not.toHaveBeenCalled();
});
it("sends an unlock vault message to the queue if the user has a locked vault", async () => {
const tabMock = createChromeTabMock({ url: "https://example.com" });
const sender = mock<chrome.runtime.MessageSender>({ tab: tabMock });
const message: NotificationBackgroundExtensionMessage = {
command: "bgUnlockPopoutOpened",
};
getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked);
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(pushUnlockVaultToQueueSpy).toHaveBeenCalledWith("example.com", sender.tab);
});
});
describe("checkNotificationQueue", () => {
let doNotificationQueueCheckSpy: jest.SpyInstance;
let getTabFromCurrentWindowSpy: jest.SpyInstance;
beforeEach(() => {
doNotificationQueueCheckSpy = jest.spyOn(
notificationBackground as any,
"doNotificationQueueCheck",
);
getTabFromCurrentWindowSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindow");
});
it("skips checking the notification queue if the queue does not contain any items", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "checkNotificationQueue",
};
notificationBackground["notificationQueue"] = [];
sendExtensionRuntimeMessage(message);
await flushPromises();
expect(doNotificationQueueCheckSpy).not.toHaveBeenCalled();
});
it("checks the notification queue for the sender tab", async () => {
const tab = createChromeTabMock();
const sender = mock<chrome.runtime.MessageSender>({ tab });
const message: NotificationBackgroundExtensionMessage = {
command: "checkNotificationQueue",
};
notificationBackground["notificationQueue"] = [
mock<AddLoginQueueMessage>({ tab }),
mock<AddLoginQueueMessage>({ tab: createChromeTabMock({ id: 2 }) }),
];
sendExtensionRuntimeMessage(message, sender);
await flushPromises();
expect(doNotificationQueueCheckSpy).toHaveBeenCalledWith(tab);
});
it("checks the notification queue for the current tab if the sender does not send a tab", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "checkNotificationQueue",
};
const currenTab = createChromeTabMock({ id: 2 });
notificationBackground["notificationQueue"] = [
mock<AddLoginQueueMessage>({ tab: currenTab }),
];
getTabFromCurrentWindowSpy.mockResolvedValueOnce(currenTab);
sendExtensionRuntimeMessage(message, mock<chrome.runtime.MessageSender>({ tab: null }));
await flushPromises();
expect(getTabFromCurrentWindowSpy).toHaveBeenCalledWith();
expect(doNotificationQueueCheckSpy).toHaveBeenCalledWith(currenTab);
});
});
describe("bgReopenUnlockPopout message handler", () => {
it("opens the unlock popout window", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "bgReopenUnlockPopout",
};
const openUnlockWindowSpy = jest.spyOn(notificationBackground as any, "openUnlockPopout");
sendExtensionRuntimeMessage(message);
await flushPromises();
expect(openUnlockWindowSpy).toHaveBeenCalled();
});
});
describe("getWebVaultUrlForNotification", () => {
it("returns the web vault url", async () => {
const message: NotificationBackgroundExtensionMessage = {
command: "getWebVaultUrlForNotification",
};
const webVaultUrl = "https://example.com";
const environmentServiceSpy = jest
.spyOn(environmentService, "getWebVaultUrl")
.mockReturnValueOnce(webVaultUrl);
sendExtensionRuntimeMessage(message);
await flushPromises();
expect(environmentServiceSpy).toHaveBeenCalled();
expect(environmentServiceSpy).toHaveReturnedWith(webVaultUrl);
});
});
});
});