// Copyright 2024 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/ash/language_packs/language_pack_font_service.h"
#include <memory>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include "ash/constants/ash_features.h"
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/ash/language_packs/language_pack_font_service_factory.h"
#include "chrome/browser/prefs/browser_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice.pb.h"
#include "chromeos/ash/components/dbus/dlcservice/fake_dlcservice_client.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/language/core/browser/language_prefs.h"
#include "components/language/core/browser/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/browser/browser_context.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace ash::language_packs {
namespace {
using ::testing::Bool;
using ::testing::Combine;
using ::testing::ElementsAre;
using ::testing::FieldsAre;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::Property;
using ::testing::Return;
using ::testing::StartsWith;
using ::testing::ValuesIn;
// `FakeDlcserviceClient::Install` adds DLCs to the return value of
// `GetExistingDlcs`, so we use that to observe whether any DLCs have been
// installed.
using GetExistingDlcsTestFuture =
base::test::TestFuture<std::string_view,
const dlcservice::DlcsWithContent&>;
using MockAddFontDir =
testing::MockFunction<LanguagePackFontService::AddFontDir>;
// Tests using this fixture should explicitly call `InitFeatureList`.
class LanguagePackFontServiceTest : public testing::Test {
public:
LanguagePackFontServiceTest()
: testing_prefs_(
std::make_unique<sync_preferences::TestingPrefServiceSyncable>()) {
::RegisterUserProfilePrefs(testing_prefs_->registry());
}
void InitFeatureList(bool load_after_download_during_login) {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
features::kLanguagePacksFonts,
{{features::kLanguagePacksFontsLoadAfterDownloadDuringLogin.name,
load_after_download_during_login ? "true" : "false"}});
}
void InitProfileWithServices() {
profile_ =
TestingProfile::Builder()
.SetPrefService(std::move(testing_prefs_))
.AddTestingFactory(
LanguagePackFontServiceFactory::GetInstance(),
base::BindRepeating(&LanguagePackFontServiceTest::CreateService,
base::Unretained(this)))
.Build();
}
PrefService* prefs() {
if (testing_prefs_) {
CHECK(!profile_);
return testing_prefs_.get();
}
return profile_->GetPrefs();
}
FakeDlcserviceClient* dlcservice_client() { return &dlcservice_client_; }
MockAddFontDir* add_font_dir() { return &add_font_dir_; }
private:
std::unique_ptr<KeyedService> CreateService(
content::BrowserContext* context) {
return std::make_unique<LanguagePackFontService>(
Profile::FromBrowserContext(context)->GetPrefs(),
base::BindRepeating(&MockAddFontDir::Call,
base::Unretained(&add_font_dir_)));
}
base::test::ScopedFeatureList scoped_feature_list_;
MockAddFontDir add_font_dir_;
FakeDlcserviceClient dlcservice_client_;
content::BrowserTaskEnvironment task_environment_;
// At any point in time, exactly one of the below `unique_ptr`s should be
// null.
// On construction, `testing_prefs_` will be created with `profile_` null.
// After `InitProfile()`, `profile_` will be created by moving in
// `testing_prefs_`, setting it to null.
std::unique_ptr<sync_preferences::TestingPrefServiceSyncable> testing_prefs_;
std::unique_ptr<TestingProfile> profile_;
};
// For understandability, "load after download during login" will be abbreviated
// to "LADDL" here and below.
//
// Tests using this fixture should NOT call `InitFeatureList`, as it is done
// automatically in `SetUp()`.
class LanguagePackFontServiceLaddlTest
: public LanguagePackFontServiceTest,
public testing::WithParamInterface<bool> {
public:
void SetUp() override {
LanguagePackFontServiceTest::SetUp();
InitFeatureList(/*load_after_download_during_login=*/GetParam());
}
};
INSTANTIATE_TEST_SUITE_P(
,
LanguagePackFontServiceLaddlTest,
Bool(),
[](const testing::TestParamInfo<bool>& info) {
return base::StrCat({"Laddl", info.param ? "Enabled" : "Disabled"});
});
struct ValidFontLanguageTestCase {
std::string test_name;
std::string_view preferred_languages_one_locale;
std::string_view preferred_languages_two_locales;
std::string dlc_prefix;
std::string dlc_path;
};
static const ValidFontLanguageTestCase kValidFontLanguageTestCases[] = {
{"Japanese", "zz,ja", "zz,ja,ja-JP", "extrafonts-ja", "/path/for/ja"},
{"Korean", "zz,ko", "zz,ko,ko-KR", "extrafonts-ko", "/path/for/ko"}};
// Tests using this fixture should explicitly call `InitFeatureList`.
class LanguagePackFontServiceValidFontLanguageTest
: public LanguagePackFontServiceTest,
public testing::WithParamInterface<ValidFontLanguageTestCase> {};
INSTANTIATE_TEST_SUITE_P(
,
LanguagePackFontServiceValidFontLanguageTest,
ValuesIn(kValidFontLanguageTestCases),
[](const testing::TestParamInfo<ValidFontLanguageTestCase>& info) {
return info.param.test_name;
});
using LaddlValidFontLanguageTestCase =
std::tuple<bool, ValidFontLanguageTestCase>;
// Tests using this fixture should NOT call `InitFeatureList`, as it is done
// automatically in `SetUp()`.
class LanguagePackFontServiceLaddlValidFontLanguageTest
: public LanguagePackFontServiceTest,
public testing::WithParamInterface<LaddlValidFontLanguageTestCase> {
public:
void SetUp() override {
LanguagePackFontServiceTest::SetUp();
InitFeatureList(
/*load_after_download_during_login=*/std::get<0>(GetParam()));
}
const ValidFontLanguageTestCase& GetValidFontLanguageParam() {
return std::get<1>(GetParam());
}
};
INSTANTIATE_TEST_SUITE_P(
,
LanguagePackFontServiceLaddlValidFontLanguageTest,
Combine(Bool(), ValuesIn(kValidFontLanguageTestCases)),
[](const testing::TestParamInfo<LaddlValidFontLanguageTestCase>& info) {
return base::StrCat({"Laddl",
std::get<0>(info.param) ? "Enabled" : "Disabled",
std::get<1>(info.param).test_name});
});
TEST_P(LanguagePackFontServiceLaddlTest,
InstallNothingOnUnrelatedLocaleChange) {
// Ensure that we don't install any DLCs / add any fonts to begin with.
// Both zz and xx (used below) are not valid ISO 639 locales as of 2024.
prefs()->SetString(language::prefs::kPreferredLanguages, "zz");
InitProfileWithServices();
prefs()->SetString(language::prefs::kPreferredLanguages, "zz,xx");
base::RunLoop().RunUntilIdle();
GetExistingDlcsTestFuture future;
dlcservice_client()->GetExistingDlcs(future.GetCallback());
const dlcservice::DlcsWithContent& dlcs = future.Get<1>();
EXPECT_THAT(dlcs.dlc_infos(), IsEmpty());
}
TEST_P(LanguagePackFontServiceLaddlValidFontLanguageTest,
InstallValidLanguageOnValidLanguageLocaleChange) {
const ValidFontLanguageTestCase& test_case = GetValidFontLanguageParam();
prefs()->SetString(language::prefs::kPreferredLanguages, "zz");
InitProfileWithServices();
prefs()->SetString(language::prefs::kPreferredLanguages,
test_case.preferred_languages_one_locale);
base::RunLoop().RunUntilIdle();
GetExistingDlcsTestFuture future;
dlcservice_client()->GetExistingDlcs(future.GetCallback());
const dlcservice::DlcsWithContent& dlcs = future.Get<1>();
EXPECT_THAT(dlcs.dlc_infos(),
ElementsAre(Property(&dlcservice::DlcsWithContent::DlcInfo::id,
StartsWith(test_case.dlc_prefix))));
}
TEST_P(LanguagePackFontServiceLaddlValidFontLanguageTest,
InstallValidLanguageOnlyOnceOnMultipleValidLanguageLocalesChange) {
const ValidFontLanguageTestCase& test_case = GetValidFontLanguageParam();
prefs()->SetString(language::prefs::kPreferredLanguages, "zz");
InitProfileWithServices();
prefs()->SetString(language::prefs::kPreferredLanguages,
test_case.preferred_languages_two_locales);
base::RunLoop().RunUntilIdle();
GetExistingDlcsTestFuture future;
dlcservice_client()->GetExistingDlcs(future.GetCallback());
const dlcservice::DlcsWithContent& dlcs = future.Get<1>();
EXPECT_THAT(dlcs.dlc_infos(),
ElementsAre(Property(&dlcservice::DlcsWithContent::DlcInfo::id,
StartsWith(test_case.dlc_prefix))));
}
TEST_P(LanguagePackFontServiceLaddlTest,
InstallNothingOnInitWithUnrelatedLocales) {
{
dlcservice::DlcState state;
state.set_state(dlcservice::DlcState::State::DlcState_State_NOT_INSTALLED);
dlcservice_client()->set_dlc_state("extrafonts-ja", std::move(state));
}
prefs()->SetString(language::prefs::kPreferredLanguages, "zz,xx");
InitProfileWithServices();
base::RunLoop().RunUntilIdle();
GetExistingDlcsTestFuture future;
dlcservice_client()->GetExistingDlcs(future.GetCallback());
const dlcservice::DlcsWithContent& dlcs = future.Get<1>();
EXPECT_THAT(dlcs.dlc_infos(), IsEmpty());
}
TEST_P(LanguagePackFontServiceLaddlValidFontLanguageTest,
InstallValidLanguageOnInitWithValidLanguageLocale) {
const ValidFontLanguageTestCase& test_case = GetValidFontLanguageParam();
{
dlcservice::DlcState state;
state.set_state(dlcservice::DlcState::State::DlcState_State_NOT_INSTALLED);
dlcservice_client()->set_dlc_state(test_case.dlc_prefix, std::move(state));
}
prefs()->SetString(language::prefs::kPreferredLanguages,
test_case.preferred_languages_one_locale);
InitProfileWithServices();
base::RunLoop().RunUntilIdle();
GetExistingDlcsTestFuture future;
dlcservice_client()->GetExistingDlcs(future.GetCallback());
const dlcservice::DlcsWithContent& dlcs = future.Get<1>();
EXPECT_THAT(dlcs.dlc_infos(),
ElementsAre(Property(&dlcservice::DlcsWithContent::DlcInfo::id,
StartsWith(test_case.dlc_prefix))));
}
TEST_P(LanguagePackFontServiceLaddlValidFontLanguageTest,
InstallValidLanguageOnlyOnceOnInitWithMultipleValidLanguageLocales) {
const ValidFontLanguageTestCase& test_case = GetValidFontLanguageParam();
{
dlcservice::DlcState state;
state.set_state(dlcservice::DlcState::State::DlcState_State_NOT_INSTALLED);
dlcservice_client()->set_dlc_state(test_case.dlc_prefix, std::move(state));
}
prefs()->SetString(language::prefs::kPreferredLanguages,
test_case.preferred_languages_two_locales);
InitProfileWithServices();
base::RunLoop().RunUntilIdle();
GetExistingDlcsTestFuture future;
dlcservice_client()->GetExistingDlcs(future.GetCallback());
const dlcservice::DlcsWithContent& dlcs = future.Get<1>();
EXPECT_THAT(dlcs.dlc_infos(),
ElementsAre(Property(&dlcservice::DlcsWithContent::DlcInfo::id,
StartsWith(test_case.dlc_prefix))));
}
constexpr std::string kUnusedDlcPath = "/path/to/unused/dlc";
TEST_P(LanguagePackFontServiceLaddlTest, AddNothingOnUnrelatedLocaleChange) {
ON_CALL(*add_font_dir(), Call).WillByDefault(Return(true));
EXPECT_CALL(*add_font_dir(), Call).Times(0);
{
dlcservice::DlcState state;
state.set_state(dlcservice::DlcState::State::DlcState_State_INSTALLED);
state.set_root_path(kUnusedDlcPath);
dlcservice_client()->set_dlc_state("extrafonts-ja", std::move(state));
}
prefs()->SetString(language::prefs::kPreferredLanguages, "zz");
InitProfileWithServices();
prefs()->SetString(language::prefs::kPreferredLanguages, "zz,xx");
base::RunLoop().RunUntilIdle();
}
TEST_P(LanguagePackFontServiceLaddlValidFontLanguageTest,
AddNothingOnValidLanguageLocaleChange) {
const ValidFontLanguageTestCase& test_case = GetValidFontLanguageParam();
ON_CALL(*add_font_dir(), Call).WillByDefault(Return(true));
EXPECT_CALL(*add_font_dir(), Call).Times(0);
{
dlcservice::DlcState state;
state.set_state(dlcservice::DlcState::State::DlcState_State_INSTALLED);
state.set_root_path(test_case.dlc_path);
dlcservice_client()->set_dlc_state(test_case.dlc_prefix, std::move(state));
}
prefs()->SetString(language::prefs::kPreferredLanguages, "zz");
InitProfileWithServices();
prefs()->SetString(language::prefs::kPreferredLanguages,
test_case.preferred_languages_one_locale);
base::RunLoop().RunUntilIdle();
}
TEST_P(LanguagePackFontServiceLaddlTest, AddNothingOnInitWithUnrelatedLocale) {
ON_CALL(*add_font_dir(), Call).WillByDefault(Return(true));
EXPECT_CALL(*add_font_dir(), Call).Times(0);
{
dlcservice::DlcState state;
state.set_state(dlcservice::DlcState::State::DlcState_State_INSTALLED);
state.set_root_path(kUnusedDlcPath);
dlcservice_client()->set_dlc_state("extrafonts-ja", std::move(state));
}
prefs()->SetString(language::prefs::kPreferredLanguages, "zz,xx");
InitProfileWithServices();
base::RunLoop().RunUntilIdle();
}
TEST_P(
LanguagePackFontServiceValidFontLanguageTest,
AddNothingOnInitWithValidLanguageLocaleWhenNotDownloadedWithLaddlDisabled) {
const ValidFontLanguageTestCase& test_case = GetParam();
InitFeatureList(/*load_after_download_during_login=*/false);
ON_CALL(*add_font_dir(), Call).WillByDefault(Return(true));
EXPECT_CALL(*add_font_dir(), Call).Times(0);
dlcservice::DlcState state;
state.set_id(test_case.dlc_prefix);
state.set_state(dlcservice::DlcState::State::DlcState_State_NOT_INSTALLED);
state.set_is_verified(false);
dlcservice_client()->set_install_root_path(test_case.dlc_path);
dlcservice_client()->set_dlc_state(test_case.dlc_prefix, state);
prefs()->SetString(language::prefs::kPreferredLanguages,
test_case.preferred_languages_one_locale);
InitProfileWithServices();
base::RunLoop().RunUntilIdle();
}
TEST_P(
LanguagePackFontServiceValidFontLanguageTest,
AddValidLanguageOnInitWithValidLanguageLocaleWhenNotDownloadedWithLaddlEnabled) {
const ValidFontLanguageTestCase& test_case = GetParam();
InitFeatureList(/*load_after_download_during_login=*/true);
ON_CALL(*add_font_dir(), Call).WillByDefault(Return(true));
EXPECT_CALL(*add_font_dir(), Call)
.With(FieldsAre(Property(&base::FilePath::value, test_case.dlc_path)))
.Times(1);
{
dlcservice::DlcState state;
state.set_id(test_case.dlc_prefix);
state.set_state(dlcservice::DlcState::State::DlcState_State_NOT_INSTALLED);
state.set_is_verified(false);
dlcservice_client()->set_install_root_path(test_case.dlc_path);
dlcservice_client()->set_dlc_state(test_case.dlc_prefix, std::move(state));
}
prefs()->SetString(language::prefs::kPreferredLanguages,
test_case.preferred_languages_one_locale);
InitProfileWithServices();
base::RunLoop().RunUntilIdle();
}
TEST_P(LanguagePackFontServiceLaddlValidFontLanguageTest,
AddValidLanguageOnInitWithValidLanguageLocale) {
const ValidFontLanguageTestCase& test_case = GetValidFontLanguageParam();
ON_CALL(*add_font_dir(), Call).WillByDefault(Return(true));
EXPECT_CALL(*add_font_dir(), Call)
.With(FieldsAre(Property(&base::FilePath::value, test_case.dlc_path)))
.Times(1);
{
dlcservice::DlcState state;
state.set_state(dlcservice::DlcState::State::DlcState_State_INSTALLED);
state.set_root_path(test_case.dlc_path);
dlcservice_client()->set_install_root_path(test_case.dlc_path);
dlcservice_client()->set_dlc_state(test_case.dlc_prefix, std::move(state));
}
prefs()->SetString(language::prefs::kPreferredLanguages,
test_case.preferred_languages_one_locale);
InitProfileWithServices();
base::RunLoop().RunUntilIdle();
}
TEST_P(
LanguagePackFontServiceLaddlValidFontLanguageTest,
AddValidLanguageOnInitWithValidLanguageLocaleWhenDownloadedButNotMounted) {
const ValidFontLanguageTestCase& test_case = GetValidFontLanguageParam();
ON_CALL(*add_font_dir(), Call).WillByDefault(Return(true));
EXPECT_CALL(*add_font_dir(), Call)
.With(FieldsAre(Property(&base::FilePath::value, test_case.dlc_path)))
.Times(1);
{
dlcservice::DlcState state;
state.set_id(test_case.dlc_prefix);
state.set_state(dlcservice::DlcState::State::DlcState_State_NOT_INSTALLED);
state.set_is_verified(true);
dlcservice_client()->set_install_root_path(test_case.dlc_path);
dlcservice_client()->set_dlc_state(test_case.dlc_prefix, std::move(state));
}
prefs()->SetString(language::prefs::kPreferredLanguages,
test_case.preferred_languages_one_locale);
InitProfileWithServices();
base::RunLoop().RunUntilIdle();
}
TEST_P(LanguagePackFontServiceLaddlValidFontLanguageTest,
AddValidLanguageOnlyOnceOnInitWithMultipleValidLanguageLocales) {
const ValidFontLanguageTestCase& test_case = GetValidFontLanguageParam();
ON_CALL(*add_font_dir(), Call).WillByDefault(Return(true));
EXPECT_CALL(*add_font_dir(), Call)
.With(FieldsAre(Property(&base::FilePath::value, test_case.dlc_path)))
.Times(1);
{
dlcservice::DlcState state;
state.set_state(dlcservice::DlcState::State::DlcState_State_INSTALLED);
state.set_root_path(test_case.dlc_path);
dlcservice_client()->set_install_root_path(test_case.dlc_path);
dlcservice_client()->set_dlc_state(test_case.dlc_prefix, std::move(state));
}
prefs()->SetString(language::prefs::kPreferredLanguages,
test_case.preferred_languages_two_locales);
InitProfileWithServices();
base::RunLoop().RunUntilIdle();
}
} // namespace
} // namespace ash::language_packs