chromium/ios/chrome/browser/search_engines/model/search_engine_tab_helper_unittest.mm

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/search_engines/model/search_engine_tab_helper.h"

#import "base/containers/contains.h"
#import "base/functional/bind.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/bind.h"
#import "base/test/ios/wait_util.h"
#import "components/favicon/core/favicon_service.h"
#import "components/favicon/ios/web_favicon_driver.h"
#import "components/keyed_service/core/service_access_type.h"
#import "components/search_engines/search_engines_test_environment.h"
#import "components/search_engines/template_url_service.h"
#import "ios/chrome/browser/favicon/model/favicon_service_factory.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/web/model/chrome_web_client.h"
#import "ios/web/public/test/scoped_testing_web_client.h"
#import "ios/web/public/test/web_state_test_util.h"
#import "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/test/web_view_interaction_test_util.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "net/test/embedded_test_server/http_request.h"
#import "net/test/embedded_test_server/http_response.h"
#import "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"

using base::test::ios::WaitUntilConditionOrTimeout;
using base::test::ios::kWaitForJSCompletionTimeout;
using web::test::SubmitWebViewFormWithId;

namespace {
const char kOpenSearchXmlFilePath[] =
    "/ios/testing/data/http_server_files/opensearch.xml";
const char kPonyHtmlFilePath[] =
    "/ios/testing/data/http_server_files/pony.html";
}

// Test fixture for SearchEngineTabHelper class.
class SearchEngineTabHelperTest : public PlatformTest {
 public:
  SearchEngineTabHelperTest(const SearchEngineTabHelperTest&) = delete;
  SearchEngineTabHelperTest& operator=(const SearchEngineTabHelperTest&) =
      delete;

 protected:
  SearchEngineTabHelperTest()
      : web_client_(std::make_unique<ChromeWebClient>()) {}

  void SetUp() override {
    PlatformTest::SetUp();

    TestChromeBrowserState::Builder builder;
    builder.AddTestingFactory(
        ios::TemplateURLServiceFactory::GetInstance(),
        base::BindLambdaForTesting(
            [this](web::BrowserState*) -> std::unique_ptr<KeyedService> {
              std::unique_ptr<TemplateURLService> model =
                  search_engines_test_environment_.ReleaseTemplateURLService();
              return model;
            }));

    browser_state_ = std::move(builder).Build();
    web::WebState::CreateParams params(browser_state_.get());
    web_state_ = web::WebState::Create(params);
    web_state_->GetView();
    web_state_->SetKeepRenderProcessAlive(true);

    favicon::WebFaviconDriver::CreateForWebState(
        web_state(),
        ios::FaviconServiceFactory::GetForBrowserState(
            browser_state_.get(), ServiceAccessType::IMPLICIT_ACCESS));
    SearchEngineTabHelper::CreateForWebState(web_state());
    server_.ServeFilesFromSourceDirectory(".");
    ASSERT_TRUE(server_.Start());
    template_url_service()->Load();
  }

  // Returns the testing TemplateURLService.
  TemplateURLService* template_url_service() {
    ChromeBrowserState* browser_state =
        ChromeBrowserState::FromBrowserState(browser_state_.get());
    return ios::TemplateURLServiceFactory::GetForBrowserState(browser_state);
  }

  web::WebState* web_state() { return web_state_.get(); }

  search_engines::SearchEnginesTestEnvironment search_engines_test_environment_;
  web::ScopedTestingWebClient web_client_;
  web::WebTaskEnvironment task_environment_{
      web::WebTaskEnvironment::MainThreadType::IO};
  data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  std::unique_ptr<web::WebState> web_state_;

  net::EmbeddedTestServer server_;
};

// Tests that SearchEngineTabHelper can add TemplateURL to TemplateURLService
// when a OSDD <link> is found in web page.
TEST_F(SearchEngineTabHelperTest, AddTemplateURLByOpenSearch) {
  GURL page_url("https://chromium.test");
  GURL osdd_url = server_.GetURL(kOpenSearchXmlFilePath);

  // Record the original TemplateURLs in TemplateURLService.
  std::vector<raw_ptr<TemplateURL, VectorExperimental>> old_urls =
      template_url_service()->GetTemplateURLs();

  // Load an empty page, and send a message of openSearchUrl from Js.
  web::test::LoadHtml(@"<html></html>", page_url, web_state());
  SearchEngineTabHelper::FromWebState(web_state())
      ->AddTemplateURLByOSDD(page_url, osdd_url);

  // Wait for TemplateURL added to TemplateURLService.
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    base::RunLoop().RunUntilIdle();
    return template_url_service()->GetTemplateURLs().size() ==
           old_urls.size() + 1;
  }));

  // TemplateURLService doesn't guarantee that newly added TemplateURL is
  // appended, so we should check in a general way that only one TemplateURL is
  // added and others remain untouched.
  TemplateURL* new_url = nullptr;
  for (TemplateURL* url : template_url_service()->GetTemplateURLs()) {
    if (!base::Contains(old_urls, url)) {
      ASSERT_FALSE(new_url);
      new_url = url;
    }
  }
  ASSERT_TRUE(new_url);
  EXPECT_EQ(u"chromium.test", new_url->data().keyword());
  EXPECT_EQ(u"Chrooome", new_url->data().short_name());
  EXPECT_EQ(
      "https://chromium.test/index.php?title=chrooome&search={searchTerms}",
      new_url->data().url());
  EXPECT_EQ(GURL("https://chromium.test/favicon.ico"),
            new_url->data().favicon_url);
}

// Tests that SearchEngineTabHelper can add TemplateURL to TemplateURLService
// when a <form> submission generates a searchable URL and leads to a successful
// navigation.
TEST_F(SearchEngineTabHelperTest, AddTemplateURLBySearchableURL) {
  GURL page_url("https://chromium.test");
  GURL searchable_url(
      "https://chromium.test/index.php?title=chrooome&search={searchTerms}");
  // HTML of the search engine page, with a <form> navigates to pony.html as
  // search result page.
  NSString* html = [NSString
      stringWithFormat:@"<html><form id='f' action='%s'></form></html>",
                       server_.GetURL(kPonyHtmlFilePath).spec().c_str()];

  // Record the original TemplateURLs in TemplateURLService.
  std::vector<raw_ptr<TemplateURL, VectorExperimental>> old_urls =
      template_url_service()->GetTemplateURLs();

  // Load an empty page, and send a message of openSearchUrl from Js.
  web::test::LoadHtml(html, page_url, web_state());
  SearchEngineTabHelper::FromWebState(web_state())
      ->SetSearchableUrl(searchable_url);
  SubmitWebViewFormWithId(web_state(), "f");

  // Wait for TemplateURL added to TemplateURLService.
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    base::RunLoop().RunUntilIdle();
    return template_url_service()->GetTemplateURLs().size() ==
           old_urls.size() + 1;
  }));

  // TemplateURLService doesn't guarantee that newly added TemplateURL is
  // appended, so we should check in a general way that only one TemplateURL is
  // added and others remain untouched.
  TemplateURL* new_url = nullptr;
  for (TemplateURL* url : template_url_service()->GetTemplateURLs()) {
    if (!base::Contains(old_urls, url)) {
      ASSERT_FALSE(new_url);
      new_url = url;
    }
  }
  ASSERT_TRUE(new_url);
  EXPECT_EQ(u"chromium.test", new_url->data().keyword());
  EXPECT_EQ(u"chromium.test", new_url->data().short_name());
  EXPECT_EQ(searchable_url.spec(), new_url->data().url());
  const GURL expected_favicon_url = GURL(page_url.spec() + "favicon.ico");
  EXPECT_EQ(expected_favicon_url, new_url->data().favicon_url);
}

// Test fixture for SearchEngineTabHelper class in incognito mode.
class SearchEngineTabHelperIncognitoTest : public SearchEngineTabHelperTest {
 public:
  SearchEngineTabHelperIncognitoTest(
      const SearchEngineTabHelperIncognitoTest&) = delete;
  SearchEngineTabHelperIncognitoTest& operator=(
      const SearchEngineTabHelperIncognitoTest&) = delete;

 protected:
  SearchEngineTabHelperIncognitoTest() {}

  void SetUp() override {
    SearchEngineTabHelperTest::SetUp();

    ChromeBrowserState* incognito_browser_state =
        browser_state_->GetOffTheRecordChromeBrowserState();

    // TemplateURLServiceFactory redirects to the original profile, so it
    // doesn't really matter which browser state is used in tests to interact
    // with TemplateURLService.
    ASSERT_EQ(template_url_service(),
              ios::TemplateURLServiceFactory::GetForBrowserState(
                  incognito_browser_state));

    web::WebState::CreateParams params(incognito_browser_state);
    incognito_web_state_ = web::WebState::Create(params);
    incognito_web_state_->SetKeepRenderProcessAlive(true);

    // SearchEngineTabHelper depends on WebFaviconDriver, which must be created
    // before and using the original (non-incognito) browser state, in
    // consistency with the logic in AttachTabHelpers().
    favicon::WebFaviconDriver::CreateForWebState(
        incognito_web_state(),
        ios::FaviconServiceFactory::GetForBrowserState(
            browser_state_.get(), ServiceAccessType::IMPLICIT_ACCESS));
    SearchEngineTabHelper::CreateForWebState(incognito_web_state());
  }

  web::WebState* incognito_web_state() { return incognito_web_state_.get(); }

 private:
  std::unique_ptr<web::WebState> incognito_web_state_;
};

// Tests that SearchEngineTabHelper doesn't add TemplateURL to
// TemplateURLService when a OSDD <link> is found in web page under incognito
// mode.
TEST_F(SearchEngineTabHelperIncognitoTest,
       NotAddTemplateURLByOpenSearchUnderIncognito) {
  GURL page_url("https://chromium.test");
  GURL osdd_url = server_.GetURL(kOpenSearchXmlFilePath);

  // Record the original TemplateURLs in TemplateURLService.
  std::vector<raw_ptr<TemplateURL, VectorExperimental>> old_urls =
      template_url_service()->GetTemplateURLs();

  // Load an empty page, and send a message of openSearchUrl from Js.
  web::test::LoadHtml(@"<html></html>", page_url, incognito_web_state());
  SearchEngineTabHelper::FromWebState(incognito_web_state())
      ->AddTemplateURLByOSDD(page_url, osdd_url);

  // No new TemplateURL should be added to TemplateURLService, wait for timeout.
  ASSERT_FALSE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    base::RunLoop().RunUntilIdle();
    return template_url_service()->GetTemplateURLs().size() ==
           old_urls.size() + 1;
  }));
  EXPECT_EQ(old_urls, template_url_service()->GetTemplateURLs());
}

// Tests that SearchEngineTabHelper doesn't add TemplateURL to
// TemplateURLService when a <form> submission generates a searchable URL and
// leads to a successful navigation under incognito mode.
TEST_F(SearchEngineTabHelperIncognitoTest,
       NotAddTemplateURLBySearchableURLUnderIncognito) {
  GURL page_url("https://chromium.test");
  GURL searchable_url(
      "https://chromium.test/index.php?title=chrooome&search={searchTerms}");
  // HTML of the search engine page, with a <form> navigates to pony.html as
  // search result page.
  NSString* html = [NSString
      stringWithFormat:@"<html><form id='f' action='%s'></form></html>",
                       server_.GetURL(kPonyHtmlFilePath).spec().c_str()];

  // Record the original TemplateURLs in TemplateURLService.
  std::vector<raw_ptr<TemplateURL, VectorExperimental>> old_urls =
      template_url_service()->GetTemplateURLs();

  // Load an empty page, and send a message of openSearchUrl from Js.
  web::test::LoadHtml(html, page_url, incognito_web_state());
  SearchEngineTabHelper::FromWebState(incognito_web_state())
      ->SetSearchableUrl(searchable_url);
  SubmitWebViewFormWithId(incognito_web_state(), "f");

  // Wait for TemplateURL added to TemplateURLService.
  ASSERT_FALSE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    base::RunLoop().RunUntilIdle();
    return template_url_service()->GetTemplateURLs().size() ==
           old_urls.size() + 1;
  }));
  EXPECT_EQ(old_urls, template_url_service()->GetTemplateURLs());
}