/* Copyright (c) 2025 The Brave Authors. All rights reserved. * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at https://mozilla.org/MPL/2.0/. */ #include #include #include #include "base/check_deref.h" #include "base/functional/callback_helpers.h" #include "base/path_service.h" #include "base/test/bind.h" #include "base/test/run_until.h" #include "base/time/time.h" #include "brave/browser/brave_account/brave_account_service_factory.h" #include "brave/browser/email_aliases/email_aliases_service_factory.h" #include "brave/browser/ui/email_aliases/email_aliases_controller.h" #include "brave/browser/ui/webui/brave_settings_ui.h" #include "brave/components/brave_account/features.h" #include "brave/components/brave_account/mock_brave_account_authentication.h" #include "brave/components/constants/brave_paths.h" #include "brave/components/email_aliases/email_aliases_api.h" #include "brave/components/email_aliases/email_aliases_service.h" #include "brave/components/email_aliases/features.h" #include "brave/components/email_aliases/test_utils.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/renderer_context_menu/render_view_context_menu.h" #include "chrome/browser/renderer_context_menu/render_view_context_menu_browsertest_util.h" #include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_commands.h" #include "chrome/browser/ui/browser_window/public/browser_window_features.h" #include "chrome/browser/ui/views/bubble/webui_bubble_manager.h" #include "chrome/test/base/in_process_browser_test.h" #include "chrome/test/base/ui_test_utils.h" #include "components/grit/brave_components_strings.h" #include "components/user_prefs/user_prefs.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_view_host.h" #include "content/public/browser/render_widget_host.h" #include "content/public/browser/storage_partition.h" #include "content/public/browser/web_contents.h" #include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" #include "content/public/test/content_mock_cert_verifier.h" #include "content/public/test/test_navigation_observer.h" #include "services/network/public/cpp/network_switches.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/base/l10n/l10n_util.h" namespace email_aliases { namespace { constexpr char kSuccessEmail[] = "success@domain.com"; std::unique_ptr ManageHandler( const net::test_server::HttpRequest& request) { if (!request.GetURL().has_path() || !request.GetURL().path().starts_with("/manage")) { return nullptr; } auto response = std::make_unique(); if (request.method == net::test_server::HttpMethod::METHOD_GET) { auto make_entry = [](const std::string& alias) { email_aliases::AliasListEntry le; le.alias = alias; le.email = kSuccessEmail; le.status = "active"; return le; }; email_aliases::AliasListResponse list; list.result.push_back(make_entry("first@alias.com")); list.result.push_back(make_entry("second@alias.com")); list.result.push_back(make_entry("third@alias.com")); response->set_code(net::HTTP_OK); response->set_content_type("application/json"); response->set_content(*base::WriteJson(list.ToValue())); } else if (request.method == net::test_server::HttpMethod::METHOD_POST) { response->set_code(net::HTTP_OK); response->set_content_type("application/json"); email_aliases::GenerateAliasResponse generate; generate.alias = "new@alias.com"; generate.message = "created"; response->set_content(*base::WriteJson(generate.ToValue())); } else if (request.method == net::test_server::HttpMethod::METHOD_PUT) { response->set_code(net::HTTP_OK); response->set_content_type("application/json"); email_aliases::AliasEditedResponse save; save.message = "updated"; response->set_content(*base::WriteJson(save.ToValue())); } return response; } } // namespace class EmailAliasesBrowserTestBase : public InProcessBrowserTest { public: EmailAliasesBrowserTestBase() { BraveSettingsUI::ShouldExposeElementsForTesting() = true; } ~EmailAliasesBrowserTestBase() override { BraveSettingsUI::ShouldExposeElementsForTesting() = false; } void SetUpBrowserContextKeyedServices( content::BrowserContext* context) override { if (features::IsEmailAliasesEnabled()) { EmailAliasesServiceFactory::GetInstance()->SetTestingFactory( context, base::BindLambdaForTesting([&](content::BrowserContext* context) -> std::unique_ptr { return std::make_unique( brave_account_auth_.BindAndGetRemote(), context->GetDefaultStoragePartition() ->GetURLLoaderFactoryForBrowserProcess(), CHECK_DEREF(user_prefs::UserPrefs::Get(context))); })); } } void SetUpOnMainThread() override { https_server_.RegisterRequestHandler(base::BindRepeating(&ManageHandler)); https_server_.ServeFilesFromDirectory( base::PathService::CheckedGet(brave::DIR_TEST_DATA)); https_server_.StartAcceptingConnections(); mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK); ON_CALL(GetBraveAccountAuth(), GetServiceToken(brave_account::mojom::Service::kEmailAliases, testing::_)) .WillByDefault([](auto service, auto callback) { auto token = brave_account::mojom::GetServiceTokenResult::New(); token->serviceToken = "email-aliases-token"; std::move(callback).Run(base::ok(std::move(token))); }); } void SetUp() override { ASSERT_TRUE(https_server_.InitializeAndListen()); InProcessBrowserTest::SetUp(); } void SetUpCommandLine(base::CommandLine* command_line) override { InProcessBrowserTest::SetUpCommandLine(command_line); mock_cert_verifier_.SetUpCommandLine(command_line); command_line->AppendSwitchASCII( network::switches::kHostResolverRules, "MAP * " + https_server_.host_port_pair().ToString()); } void SetUpInProcessBrowserTestFixture() override { InProcessBrowserTest::SetUpInProcessBrowserTestFixture(); mock_cert_verifier_.SetUpInProcessBrowserTestFixture(); } void TearDownInProcessBrowserTestFixture() override { mock_cert_verifier_.TearDownInProcessBrowserTestFixture(); InProcessBrowserTest::TearDownInProcessBrowserTestFixture(); } content::WebContents* ActiveWebContents() { return browser()->tab_strip_model()->GetActiveWebContents(); } content::WebContents* Navigate(const GURL& url) { ui_test_utils::NavigateToURLWithDisposition( browser(), url, WindowOpenDisposition::CURRENT_TAB, ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP); return ActiveWebContents(); } void InjectHelpers(content::WebContents* contents) { ASSERT_TRUE(base::test::RunUntil([&]() { return !contents->GetLastCommittedURL().is_empty() && !contents->GetLastCommittedURL().IsAboutBlank(); })); ASSERT_TRUE(content::WaitForLoadStop(contents)); constexpr char kDeepQuery[] = R"js( function deepQuery(selector) { const query = (root, selector) =>{ const e = root.querySelector(selector); if (e) return e; for (const el of root.querySelectorAll('*')) { if (!el.shadowRoot) continue; const found = query(el.shadowRoot, selector); if (found) return found; } return null; } return query(document, selector); }; )js"; ASSERT_TRUE(content::ExecJs(contents, kDeepQuery)); } void Wait(const std::string& id, content::WebContents* contents = nullptr) { constexpr const char kScript[] = R"js( (async () => { let waiter = () => { return !deepQuery($1) }; while (waiter()) { await new Promise(r => setTimeout(r, 10)); } return true; })(); )js"; ASSERT_EQ(true, content::EvalJs((contents ? contents : ActiveWebContents()), content::JsReplace(kScript, id))); } void WaitDisappear(const std::string& selector, content::WebContents* contents = nullptr) { constexpr const char kScript[] = R"js( (async () => { let waiter = () => { return deepQuery($1) }; while (waiter()) { await new Promise(r => setTimeout(r, 10)); } return true; })(); )js"; ASSERT_EQ(true, content::EvalJs((contents ? contents : ActiveWebContents()), content::JsReplace(kScript, selector))); } void SetText(const std::string& id, const std::string& text, content::WebContents* contents = nullptr) { constexpr char kSetText[] = R"js( (() => { const element = deepQuery($1); element.value = $2; element.dispatchEvent(new Event('input', {bubbles: true})); element.dispatchEvent(new Event('change', {bubbles: true})); return true; })(); )js"; ASSERT_EQ(true, content::EvalJs((contents ? contents : ActiveWebContents()), content::JsReplace(kSetText, id, text))); } std::string GetText(const std::string& id) { constexpr char kGetText[] = R"js( deepQuery($1).value )js"; return content::EvalJs(ActiveWebContents(), content::JsReplace(kGetText, id)) .ExtractString(); } bool AwaitText(const std::string& id, const std::string& expected_text) { constexpr char kAwaitText[] = R"js( (async () => { let waiter = () => { return deepQuery($1).value !== $2 }; while (waiter()) { await new Promise(r => setTimeout(r, 10)); } return true; })() )js"; return content::EvalJs(ActiveWebContents(), content::JsReplace(kAwaitText, id, expected_text)) .ExtractBool(); } void Click(const std::string& id, content::WebContents* contents = nullptr) { constexpr const char kClick[] = R"js( (async () => { let waiter = () => { return !deepQuery($1) }; while (waiter()) { await new Promise(r => setTimeout(r, 10)); } return deepQuery($1).onClick(); })(); )js"; auto ignore = content::ExecJs((contents ? contents : ActiveWebContents()), content::JsReplace(kClick, id)); } EmailAliasesService* email_aliases_service() { return EmailAliasesServiceFactory::GetServiceForProfile( browser()->profile()); } void RunContextMenuOn(const std::string& element_id) { const int x = content::EvalJs(ActiveWebContents(), content::JsReplace("getElementX($1)", element_id)) .ExtractInt(); const int y = content::EvalJs(ActiveWebContents(), content::JsReplace("getElementY($1)", element_id)) .ExtractInt(); ASSERT_TRUE(content::ExecJs( ActiveWebContents(), content::JsReplace("document.getElementById($1).focus()", element_id))); ActiveWebContents() ->GetPrimaryMainFrame() ->GetRenderViewHost() ->GetWidget() ->ShowContextMenuAtPoint(gfx::Point(x, y), ui::mojom::MenuSourceType::kMouse); } testing::NiceMock& GetBraveAccountAuth() { return brave_account_auth_; } private: content::ContentMockCertVerifier mock_cert_verifier_; net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS}; testing::NiceMock brave_account_auth_; }; class EmailAliasesBrowserTest : public EmailAliasesBrowserTestBase { public: EmailAliasesBrowserTest() { feature_list_.InitWithFeatures( {{email_aliases::features::kEmailAliases}, brave_account::features::BraveAccountFeatureForTesting()}, {}); } private: base::test::ScopedFeatureList feature_list_; }; class EmailAliasesBrowserNoFeatureTest : public EmailAliasesBrowserTestBase { public: EmailAliasesBrowserNoFeatureTest() { feature_list_.InitAndDisableFeature(email_aliases::features::kEmailAliases); } private: base::test::ScopedFeatureList feature_list_; }; IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserNoFeatureTest, NoContextMenuItem) { Navigate(GURL("https://a.test/email_aliases/inputs.html")); ContextMenuWaiter menu_waiter(IDC_NEW_EMAIL_ALIAS); RunContextMenuOn("type-email"); menu_waiter.WaitForMenuOpenAndClose(); EXPECT_FALSE(std::ranges::contains(menu_waiter.GetCapturedEnabledCommandIds(), IDC_NEW_EMAIL_ALIAS)); EXPECT_FALSE(menu_waiter.IsCommandExecuted()); } IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, NoContextMenuItemOnNonsuitableField) { Navigate(GURL("https://a.test/email_aliases/inputs.html")); ContextMenuWaiter menu_waiter(IDC_NEW_EMAIL_ALIAS); RunContextMenuOn("type-url"); menu_waiter.WaitForMenuOpenAndClose(); EXPECT_FALSE(std::ranges::contains(menu_waiter.GetCapturedEnabledCommandIds(), IDC_NEW_EMAIL_ALIAS)); EXPECT_FALSE(menu_waiter.IsCommandExecuted()); } IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, ContextMenuNotAuthorized) { const GURL settings_page("chrome://settings/email-aliases"); Navigate(GURL("https://a.test/email_aliases/inputs.html")); content::TestNavigationObserver waiter(settings_page); waiter.StartWatchingNewWebContents(); ContextMenuWaiter menu_waiter(IDC_NEW_EMAIL_ALIAS); RunContextMenuOn("type-email"); menu_waiter.WaitForMenuOpenAndClose(); waiter.WaitForNavigationFinished(); EXPECT_EQ(ActiveWebContents()->GetLastCommittedURL(), settings_page); } IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, ContextMenuAuthorized) { auto* service = email_aliases_service(); { auto initilized = test::AuthStateObserver::Setup(service, true); } service->GetAuth()->SetAuthEmailForTesting(kSuccessEmail); Navigate(GURL("https://a.test/email_aliases/inputs.html")); InjectHelpers(ActiveWebContents()); EmailAliasesController::DisableAutoCloseBubbleForTesting(true); auto* email_aliases_controller = browser()->GetFeatures().email_aliases_controller(); EXPECT_EQ("", GetText("#type-email")); ContextMenuWaiter menu_waiter(IDC_NEW_EMAIL_ALIAS); RunContextMenuOn("type-email"); menu_waiter.WaitForMenuOpenAndClose(); // Wait for bubble. ASSERT_TRUE(base::test::RunUntil( [&]() { return !!email_aliases_controller->GetBubbleForTesting(); })); auto* bubble = email_aliases_controller->GetBubbleForTesting(); InjectHelpers(bubble); Wait("#create-alias-button:not([isDisabled=\"true\"])", bubble); Click("#create-alias-button:not([isDisabled=\"true\"])", bubble); // Wait for bubble close ASSERT_TRUE(base::test::RunUntil( [&]() { return !email_aliases_controller->GetBubbleForTesting(); })); EXPECT_TRUE(AwaitText("#type-email", "new@alias.com")); } IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, ContextMenuAuthorizedManage) { email_aliases_service()->GetAuth()->SetAuthEmailForTesting(kSuccessEmail); const GURL settings_page("chrome://settings/email-aliases"); Navigate(GURL("https://a.test/email_aliases/inputs.html")); content::TestNavigationObserver waiter(settings_page); waiter.StartWatchingNewWebContents(); EmailAliasesController::DisableAutoCloseBubbleForTesting(true); auto* email_aliases_controller = browser()->GetFeatures().email_aliases_controller(); ContextMenuWaiter menu_waiter(IDC_NEW_EMAIL_ALIAS); RunContextMenuOn("type-email"); menu_waiter.WaitForMenuOpenAndClose(); // Wait for bubble. ASSERT_TRUE(base::test::RunUntil( [&]() { return !!email_aliases_controller->GetBubbleForTesting(); })); auto* bubble = email_aliases_controller->GetBubbleForTesting(); InjectHelpers(bubble); Wait("#manage-button", bubble); Click("#manage-button", bubble); waiter.WaitForNavigationFinished(); EXPECT_EQ(ActiveWebContents()->GetLastCommittedURL(), settings_page); } IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, ContextMenuAuthorizedCancel) { email_aliases_service()->GetAuth()->SetAuthEmailForTesting(kSuccessEmail); Navigate(GURL("https://a.test/email_aliases/inputs.html")); InjectHelpers(ActiveWebContents()); EmailAliasesController::DisableAutoCloseBubbleForTesting(true); auto* email_aliases_controller = browser()->GetFeatures().email_aliases_controller(); ContextMenuWaiter menu_waiter(IDC_NEW_EMAIL_ALIAS); RunContextMenuOn("type-email"); menu_waiter.WaitForMenuOpenAndClose(); // Wait for bubble. ASSERT_TRUE(base::test::RunUntil( [&]() { return !!email_aliases_controller->GetBubbleForTesting(); })); auto* bubble = email_aliases_controller->GetBubbleForTesting(); InjectHelpers(bubble); Wait("#cancel-button", bubble); Click("#cancel-button", bubble); // Wait for bubble close ASSERT_TRUE(base::test::RunUntil( [&]() { return !email_aliases_controller->GetBubbleForTesting(); })); EXPECT_TRUE(AwaitText("#type-email", "")); // text not changed } IN_PROC_BROWSER_TEST_F(EmailAliasesBrowserTest, LogInLogOut) { // Prepare auth token email_aliases_service()->GetAuth()->SetAuthEmailForTesting(kSuccessEmail); // Settings in logged-in state Navigate(GURL("chrome://settings/email-aliases")); InjectHelpers(ActiveWebContents()); Wait("#create-new-item-button"); // Reset token email_aliases_service()->GetAuth()->SetAuthEmailForTesting(""); WaitDisappear("#create-new-item-button"); // Settings in sing-in state. // Logged-in again email_aliases_service()->GetAuth()->SetAuthEmailForTesting(kSuccessEmail); Wait("#create-new-item-button"); // Logged-in state. } } // namespace email_aliases