chromium/chrome/browser/ui/passwords/ui_utils_unittest.cc

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

#include "chrome/browser/ui/passwords/ui_utils.h"

#include <stddef.h>

#include <string>

#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/range/range.h"
#include "url/gurl.h"

namespace {

// ScopedResourceOverride allows overriding localised strings in the shared
// instance of the resource bundle, while restoring the bundle state on
// destruction.
class ScopedResourceOverride {
 public:
  ScopedResourceOverride()
      : had_shared_instance_(ui::ResourceBundle::HasSharedInstance()),
        bundle_(GetOrCreateSharedInstance()),
        app_locale_(g_browser_process->GetApplicationLocale()) {}

  ScopedResourceOverride(const ScopedResourceOverride&) = delete;
  ScopedResourceOverride& operator=(const ScopedResourceOverride&) = delete;

  ~ScopedResourceOverride() {
    if (had_shared_instance_) {
      // Reloading the resources will discard all overrides.
      bundle_.ReloadLocaleResources(app_locale_);
    } else {
      ui::ResourceBundle::CleanupSharedInstance();
    }
  }

  void OverrideLocaleStringResource(int string_id, const std::u16string& str) {
    bundle_.OverrideLocaleStringResource(string_id, str);
  }

 private:
  // Returns the shared resource bundle. Creates one if there was none.
  static ui::ResourceBundle& GetOrCreateSharedInstance() {
    if (!ui::ResourceBundle::HasSharedInstance()) {
      ui::ResourceBundle::InitSharedInstanceWithLocale(
          "en", nullptr, ui::ResourceBundle::LOAD_COMMON_RESOURCES);
    }
    return ui::ResourceBundle::GetSharedInstance();
  }

  const bool had_shared_instance_;  // Was there a shared bundle before?
  ui::ResourceBundle& bundle_;      // The shared bundle.
  const std::string app_locale_;
};

const struct {
  const char* const user_visible_url;
  const char* const form_origin_url;
  PasswordTitleType bubble_type;
  const char* const expected_domain_placeholder;  // domain name
} kDomainsTestCases[] = {
    // Same domains.
    {"http://example.com/landing", "http://example.com/login#form?value=3",
     PasswordTitleType::SAVE_PASSWORD, ""},
    {"http://example.com/landing", "http://example.com/login#form?value=3",
     PasswordTitleType::SAVE_PASSWORD, ""},

    // Different subdomains.
    {"https://a.example.com/landing",
     "https://b.example.com/login#form?value=3",
     PasswordTitleType::SAVE_PASSWORD, ""},
    {"https://a.example.com/landing",
     "https://b.example.com/login#form?value=3",
     PasswordTitleType::SAVE_PASSWORD, ""},

    // Different domains.
    {"https://another.org", "https://example.com:/login#form?value=3",
     PasswordTitleType::SAVE_PASSWORD, "example.com"},
    {"https://another.org", "https://example.com/login#form?value=3",
     PasswordTitleType::SAVE_PASSWORD, "example.com"},

    // Different domains and password form origin url with
    // default port for the scheme.
    {"https://another.org", "https://example.com:443/login#form?value=3",
     PasswordTitleType::SAVE_PASSWORD, "example.com"},
    {"https://another.org", "http://example.com:80/login#form?value=3",
     PasswordTitleType::SAVE_PASSWORD, "example.com"},

    // Different domains and password form origin url with
    // non-default port for the scheme.
    {"https://another.org", "https://example.com:8001/login#form?value=3",
     PasswordTitleType::SAVE_PASSWORD, "example.com:8001"},
    {"https://another.org", "https://example.com:8001/login#form?value=3",
     PasswordTitleType::SAVE_PASSWORD, "example.com:8001"},

    // Update bubble, same domains.
    {"http://example.com/landing", "http://example.com/login#form?value=3",
     PasswordTitleType::UPDATE_PASSWORD, ""},
    {"http://example.com/landing", "http://example.com/login#form?value=3",
     PasswordTitleType::UPDATE_PASSWORD, ""},

    // Update bubble, different domains.
    {"https://another.org", "http://example.com/login#form?value=3",
     PasswordTitleType::UPDATE_PASSWORD, "example.com"},
    {"https://another.org", "http://example.com/login#form?value=3",
     PasswordTitleType::UPDATE_PASSWORD, "example.com"},

    // Same domains, federated credential.
    {"http://example.com/landing", "http://example.com/login#form?value=3",
     PasswordTitleType::SAVE_ACCOUNT, ""},
    {"http://example.com/landing", "http://example.com/login#form?value=3",
     PasswordTitleType::SAVE_ACCOUNT, ""},

    // Different subdomains, federated credential.
    {"https://a.example.com/landing",
     "https://b.example.com/login#form?value=3",
     PasswordTitleType::SAVE_ACCOUNT, ""},
    {"https://a.example.com/landing",
     "https://b.example.com/login#form?value=3",
     PasswordTitleType::SAVE_ACCOUNT, ""}};

}  // namespace

// Test for GetSavePasswordDialogTitleText().
TEST(ManagePasswordsViewUtilTest, GetSavePasswordDialogTitleText) {
  for (size_t i = 0; i < std::size(kDomainsTestCases); ++i) {
    SCOPED_TRACE(testing::Message() << "user_visible_url = "
                                    << kDomainsTestCases[i].user_visible_url
                                    << ", form_origin_url = "
                                    << kDomainsTestCases[i].form_origin_url);

    std::u16string title = GetSavePasswordDialogTitleText(
        GURL(kDomainsTestCases[i].user_visible_url),
        url::Origin::Create(GURL(kDomainsTestCases[i].form_origin_url)),
        kDomainsTestCases[i].bubble_type);

    // Verify against expectations.
    std::u16string domain =
        base::ASCIIToUTF16(kDomainsTestCases[i].expected_domain_placeholder);
    EXPECT_TRUE(title.find(domain) != std::u16string::npos);
    if (kDomainsTestCases[i].bubble_type ==
        PasswordTitleType::UPDATE_PASSWORD) {
      EXPECT_TRUE(title.find(u"Update") != std::u16string::npos);
    } else {
      EXPECT_TRUE(title.find(u"Save") != std::u16string::npos);
    }
  }
}

// Check that empty localised strings do not cause a crash.
TEST(ManagePasswordsViewUtilTest, GetSavePasswordDialogTitleText_EmptyStrings) {
  ScopedResourceOverride resource_override;

  // Ensure that the resource bundle returns an empty string for the UI.
  resource_override.OverrideLocaleStringResource(IDS_SAVE_PASSWORD,
                                                 std::u16string());

  const GURL kExample("http://example.org");
  // The arguments passed below have this importance for the codepath:
  // * The first two URLs need to be the same, otherwise
  //   IDS_SAVE_PASSWORD_DIFFERENT_DOMAINS_TITLE will be used instead of
  //   IDS_SAVE_PASSWORD overridden above.
  // * |kBrandingEnabled| needs to be true, otherwise the code won't try to
  //   dereference placeholder offsets from the localised string, which
  //   triggers the crash in http://crbug.com/658902.
  // * SAVE_PASSWORD dialog type needs to be passed to match the
  //   IDS_SAVE_PASSWORD overridden above.
  std::u16string title =
      GetSavePasswordDialogTitleText(kExample, url::Origin::Create(kExample),
                                     PasswordTitleType::SAVE_PASSWORD);
  // Verify that the test did not pass just because
  // GetSavePasswordDialogTitleText changed the resource IDs it uses
  // (and hence did not get the overridden empty string). If the empty localised
  // string was used, the title and the range will be empty as well.
  EXPECT_THAT(title, testing::IsEmpty());
}

TEST(ManagePasswordsViewUtilTest, GetManagePasswordsDialogTitleText) {
  for (size_t i = 0; i < std::size(kDomainsTestCases); ++i) {
    SCOPED_TRACE(testing::Message() << "user_visible_url = "
                                    << kDomainsTestCases[i].user_visible_url
                                    << ", password_origin_url = "
                                    << kDomainsTestCases[i].form_origin_url);

    std::u16string title = GetManagePasswordsDialogTitleText(
        GURL(kDomainsTestCases[i].user_visible_url),
        url::Origin::Create(GURL(kDomainsTestCases[i].form_origin_url)), true);

    // Verify against expectations.
    std::u16string domain =
        base::ASCIIToUTF16(kDomainsTestCases[i].expected_domain_placeholder);
    EXPECT_TRUE(title.find(domain) != std::u16string::npos);
  }
}

TEST(ManagePasswordsViewUtilTest,
     GetConfirmationManagePasswordsDialogTitleText) {
  EXPECT_NE(std::u16string(), GetConfirmationManagePasswordsDialogTitleText());
}