chromium/ios/chrome/browser/passwords/model/well_known_change_password_tab_helper_unittest.mm

// Copyright 2020 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/passwords/model/well_known_change_password_tab_helper.h"

#import <Foundation/Foundation.h>

#import "base/memory/raw_ptr.h"
#import "base/test/bind.h"
#import "base/test/scoped_feature_list.h"
#import "components/affiliations/core/browser/mock_affiliation_service.h"
#import "components/password_manager/core/browser/well_known_change_password/well_known_change_password_util.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/ukm/test_ukm_recorder.h"
#import "ios/chrome/browser/affiliations/model/ios_chrome_affiliation_service_factory.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/test/fakes/fake_web_client.h"
#import "ios/web/public/test/fakes/fake_web_state_delegate.h"
#import "ios/web/public/test/navigation_test_util.h"
#import "ios/web/public/test/scoped_testing_web_client.h"
#import "ios/web/public/test/task_observer_util.h"
#import "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/test/web_view_content_test_util.h"
#import "net/cert/x509_certificate.h"
#import "net/http/http_status_code.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/metrics/public/cpp/ukm_builders.h"
#import "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#import "services/network/test/test_url_loader_factory.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"

namespace {

using affiliations::FacetURI;
using base::test::ios::WaitUntilConditionOrTimeout;
using net::test_server::BasicHttpResponse;
using net::test_server::EmbeddedTestServer;
using net::test_server::EmbeddedTestServerHandle;
using net::test_server::HttpRequest;
using net::test_server::HttpResponse;
using password_manager::kWellKnownChangePasswordPath;
using password_manager::kWellKnownNotExistingResourcePath;
using password_manager::WellKnownChangePasswordResult;
using ::testing::NiceMock;

// ServerResponse describes how a server should respond to a given path.
struct ServerResponse {
  net::HttpStatusCode status_code;
  std::vector<std::pair<std::string, std::string>> headers;
};

constexpr char kMockChangePasswordPath[] = "/change-password-override";

// Re-implementation of web::LoadUrl() that allows specifying a custom page
// transition.
void LoadUrlWithTransition(web::WebState* web_state,
                           const GURL& url,
                           ui::PageTransition transition) {
  web::NavigationManager* navigation_manager =
      web_state->GetNavigationManager();
  web::NavigationManager::WebLoadParams params(url);
  params.transition_type = transition;
  navigation_manager->LoadURLWithParams(params);
}

std::unique_ptr<KeyedService> MakeMockAffiliationService(web::BrowserState*) {
  return std::make_unique<NiceMock<affiliations::MockAffiliationService>>();
}

}  // namespace

// This test uses a mockserver to simulate different response. To handle the
// url_loader requests we also mock the response for the url_loader_factory.
class WellKnownChangePasswordTabHelperTest : public PlatformTest {
 public:
  using UkmBuilder =
      ukm::builders::PasswordManager_WellKnownChangePasswordResult;
  WellKnownChangePasswordTabHelperTest()
      : web_client_(std::make_unique<web::FakeWebClient>()) {
    test_server_->RegisterRequestHandler(base::BindRepeating(
        &WellKnownChangePasswordTabHelperTest::HandleRequest,
        base::Unretained(this)));

    TestChromeBrowserState::Builder builder;
    builder.AddTestingFactory(IOSChromeAffiliationServiceFactory::GetInstance(),
                              base::BindRepeating(&MakeMockAffiliationService));
    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);
  }

  void SetUp() override {
    PlatformTest::SetUp();
    EXPECT_TRUE(test_server_->InitializeAndListen());
    test_server_->StartAcceptingConnections();

    affiliation_service_ = static_cast<affiliations::MockAffiliationService*>(
        IOSChromeAffiliationServiceFactory::GetForBrowserState(
            browser_state_.get()));

    web_state()->SetDelegate(&delegate_);
    password_manager::WellKnownChangePasswordTabHelper::CreateForWebState(
        web_state());
    browser_state_->SetSharedURLLoaderFactory(
        base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
            &test_url_loader_factory_));
    test_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
  }

  // Sets a response for the `test_url_loader_factory_` with the `test_server_`
  // as the host.
  void SetUrlLoaderResponse(const std::string& path,
                            net::HttpStatusCode status_code) {
    test_url_loader_factory_.AddResponse(test_server_->GetURL(path).spec(), "",
                                         status_code);
  }

  void ExpectUkmMetric(WellKnownChangePasswordResult expected) {
    auto entries = test_recorder_->GetEntriesByName(UkmBuilder::kEntryName);
    // Expect one recorded metric.
    ASSERT_EQ(1, static_cast<int>(entries.size()));
    test_recorder_->ExpectEntryMetric(
        entries[0], UkmBuilder::kWellKnownChangePasswordResultName,
        static_cast<int64_t>(expected));
  }
  // Returns the url after the navigation is complete.
  GURL GetNavigatedUrl() const;

  // Sets if change passwords URL can be obtained.
  void SetChangePasswordURLForAffiliationService(
      const GURL& change_password_url) {
    EXPECT_CALL(*affiliation_service_, GetChangePasswordURL)
        .WillRepeatedly(testing::Return(change_password_url));
  }

  // Maps a path to a ServerResponse config object.
  base::flat_map<std::string, ServerResponse> path_response_map_;
  std::unique_ptr<EmbeddedTestServer> test_server_ =
      std::make_unique<EmbeddedTestServer>();
  std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_recorder_;

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

  web::ScopedTestingWebClient web_client_;
  web::WebTaskEnvironment task_environment_{
      web::WebTaskEnvironment::MainThreadType::IO};
  std::unique_ptr<TestChromeBrowserState> browser_state_;
  std::unique_ptr<web::WebState> web_state_;

 private:
  // Returns a response for the given request. Uses `path_response_map_` to
  // construct the response. Returns nullptr when the path is not defined in
  // `path_response_map_`.
  std::unique_ptr<HttpResponse> HandleRequest(const HttpRequest& request);

  base::test::ScopedFeatureList feature_list_;
  network::TestURLLoaderFactory test_url_loader_factory_;
  web::FakeWebStateDelegate delegate_;
  raw_ptr<affiliations::MockAffiliationService> affiliation_service_ = nullptr;
};

GURL WellKnownChangePasswordTabHelperTest::GetNavigatedUrl() const {
  GURL url = web_state()->GetLastCommittedURL();
  // When redirecting with WebState::OpenURL() `web_state_` is not
  // updated, we only see the registered request in
  // FakeWebStateDelegate::last_open_url_request().
  if (delegate_.last_open_url_request()) {
    url = delegate_.last_open_url_request()->params.url;
  }
  return url;
}

std::unique_ptr<HttpResponse>
WellKnownChangePasswordTabHelperTest::HandleRequest(
    const HttpRequest& request) {
  GURL absolute_url = test_server_->GetURL(request.relative_url);
  std::string path = absolute_url.path();
  auto it = path_response_map_.find(absolute_url.path_piece());
  if (it == path_response_map_.end()) {
    return nullptr;
  }

  const ServerResponse& config = it->second;
  auto http_response = std::make_unique<BasicHttpResponse>();
  http_response->set_code(config.status_code);
  http_response->set_content_type("text/plain");
  for (auto header_pair : config.headers) {
    http_response->AddCustomHeader(header_pair.first, header_pair.second);
  }
  return http_response;
}

TEST_F(WellKnownChangePasswordTabHelperTest, SupportForChangePassword) {
  path_response_map_[kWellKnownChangePasswordPath] = {net::HTTP_OK, {}};

  SetUrlLoaderResponse(kWellKnownNotExistingResourcePath, net::HTTP_NOT_FOUND);

  web::test::LoadUrl(web_state(),
                     test_server_->GetURL(kWellKnownChangePasswordPath));
  ASSERT_TRUE(web::test::WaitUntilLoaded(web_state()));
  EXPECT_EQ(GetNavigatedUrl().path(), kWellKnownChangePasswordPath);
  ExpectUkmMetric(WellKnownChangePasswordResult::kUsedWellKnownChangePassword);
}

TEST_F(WellKnownChangePasswordTabHelperTest,
       SupportForChangePassword_WithRedirect) {
  path_response_map_[kWellKnownChangePasswordPath] = {
      net::HTTP_PERMANENT_REDIRECT,
      {std::make_pair("Location", "/change-password")}};
  path_response_map_["/change-password"] = {net::HTTP_OK, {}};

  SetUrlLoaderResponse(kWellKnownNotExistingResourcePath, net::HTTP_NOT_FOUND);

  web::test::LoadUrl(web_state(),
                     test_server_->GetURL(kWellKnownChangePasswordPath));
  ASSERT_TRUE(web::test::WaitUntilLoaded(web_state()));
  EXPECT_EQ(GetNavigatedUrl().path(), "/change-password");
  ExpectUkmMetric(WellKnownChangePasswordResult::kUsedWellKnownChangePassword);
}

TEST_F(WellKnownChangePasswordTabHelperTest,
       NoSupportForChangePassword_NotFound) {
  path_response_map_[kWellKnownChangePasswordPath] = {net::HTTP_NOT_FOUND, {}};
  path_response_map_["/"] = {net::HTTP_OK, {}};
  SetUrlLoaderResponse(kWellKnownNotExistingResourcePath, net::HTTP_NOT_FOUND);

  web::test::LoadUrl(web_state(),
                     test_server_->GetURL(kWellKnownChangePasswordPath));
  ASSERT_TRUE(web::test::WaitUntilLoaded(web_state()));
  EXPECT_EQ(GetNavigatedUrl().path(), "/");
  ExpectUkmMetric(WellKnownChangePasswordResult::kFallbackToOriginUrl);
}

TEST_F(WellKnownChangePasswordTabHelperTest, NoSupportForChangePassword_Ok) {
  path_response_map_[kWellKnownChangePasswordPath] = {net::HTTP_OK, {}};
  path_response_map_["/"] = {net::HTTP_OK, {}};
  SetUrlLoaderResponse(kWellKnownNotExistingResourcePath, net::HTTP_OK);

  web::test::LoadUrl(web_state(),
                     test_server_->GetURL(kWellKnownChangePasswordPath));
  ASSERT_TRUE(web::test::WaitUntilLoaded(web_state()));
  EXPECT_EQ(GetNavigatedUrl().path(), "/");
  ExpectUkmMetric(WellKnownChangePasswordResult::kFallbackToOriginUrl);
}

TEST_F(WellKnownChangePasswordTabHelperTest,
       NoSupportForChangePassword_WithRedirect) {
  path_response_map_[kWellKnownChangePasswordPath] = {
      net::HTTP_PERMANENT_REDIRECT, {std::make_pair("Location", "/not-found")}};
  path_response_map_["/not-found"] = {net::HTTP_NOT_FOUND, {}};
  SetUrlLoaderResponse(kWellKnownNotExistingResourcePath, net::HTTP_OK);
  web::test::LoadUrl(web_state(),
                     test_server_->GetURL(kWellKnownChangePasswordPath));
  ASSERT_TRUE(web::test::WaitUntilLoaded(web_state()));
  EXPECT_EQ(GetNavigatedUrl().path(), "/");
  ExpectUkmMetric(WellKnownChangePasswordResult::kFallbackToOriginUrl);
}

TEST_F(WellKnownChangePasswordTabHelperTest,
       NoSupportForChangePassword_WithOverride) {
  SetChangePasswordURLForAffiliationService(
      test_server_->GetURL(kMockChangePasswordPath));
  path_response_map_[kWellKnownChangePasswordPath] = {
      net::HTTP_PERMANENT_REDIRECT, {std::make_pair("Location", "/not-found")}};
  path_response_map_["/not-found"] = {net::HTTP_NOT_FOUND, {}};
  SetUrlLoaderResponse(kWellKnownNotExistingResourcePath, net::HTTP_OK);
  web::test::LoadUrl(web_state(),
                     test_server_->GetURL(kWellKnownChangePasswordPath));
  ASSERT_TRUE(web::test::WaitUntilLoaded(web_state()));
  EXPECT_EQ(GetNavigatedUrl().path(), kMockChangePasswordPath);
  ExpectUkmMetric(WellKnownChangePasswordResult::kFallbackToOverrideUrl);
}

TEST_F(WellKnownChangePasswordTabHelperTest,
       NoSupportForChangePasswordForLinks) {
  path_response_map_[kWellKnownChangePasswordPath] = {net::HTTP_OK, {}};
  LoadUrlWithTransition(web_state(),
                        test_server_->GetURL(kWellKnownChangePasswordPath),
                        ui::PAGE_TRANSITION_LINK);
  ASSERT_TRUE(web::test::WaitUntilLoaded(web_state()));
  EXPECT_EQ(GetNavigatedUrl().path(), kWellKnownChangePasswordPath);

  // In the case of PAGE_TRANSITION_LINK the tab helper should not be active and
  // no metrics should be recorded.
  EXPECT_TRUE(test_recorder_->GetEntriesByName(UkmBuilder::kEntryName).empty());
}

TEST_F(WellKnownChangePasswordTabHelperTest,
       NoSupportForChangePassword_AffiliationServiceReturnsWellKnownUrl) {
  SetChangePasswordURLForAffiliationService(
      test_server_->GetURL(kWellKnownChangePasswordPath));
  path_response_map_[kWellKnownChangePasswordPath] = {net::HTTP_NOT_FOUND, {}};
  path_response_map_["/"] = {net::HTTP_OK, {}};
  SetUrlLoaderResponse(kWellKnownNotExistingResourcePath, net::HTTP_NOT_FOUND);

  web::test::LoadUrl(web_state(),
                     test_server_->GetURL(kWellKnownChangePasswordPath));
  ASSERT_TRUE(web::test::WaitUntilLoaded(web_state()));
  EXPECT_EQ(GetNavigatedUrl().path(), kWellKnownChangePasswordPath);
  ExpectUkmMetric(WellKnownChangePasswordResult::kUsedWellKnownChangePassword);
}