chromium/chrome/browser/ui/webui/ash/settings/pages/apps/app_parental_controls_handler_browsertest.cc

// 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/ui/webui/ash/settings/pages/apps/app_parental_controls_handler.h"

#include <map>
#include <memory>
#include <string>
#include <vector>

#include "ash/components/arc/mojom/app.mojom.h"
#include "ash/components/arc/test/arc_util_test_support.h"
#include "ash/components/arc/test/connection_holder_util.h"
#include "ash/components/arc/test/fake_app_instance.h"
#include "ash/constants/ash_pref_names.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/values.h"
#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
#include "chrome/browser/ash/app_list/arc/arc_app_utils.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/arc/session/arc_session_manager.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/webui/ash/settings/services/settings_manager/os_settings_manager.h"
#include "chrome/browser/ui/webui/ash/settings/services/settings_manager/os_settings_manager_factory.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash::settings {

namespace {

arc::mojom::ArcPackageInfoPtr CreateArcAppPackage(
    const std::string& package_name) {
  auto package = arc::mojom::ArcPackageInfo::New();
  package->package_name = package_name;
  package->package_version = 1;
  package->last_backup_android_id = 1;
  package->last_backup_time = 1;
  package->sync = false;
  return package;
}

arc::mojom::AppInfoPtr CreateArcAppInfo(const std::string& package_name) {
  return arc::mojom::AppInfo::New(package_name, package_name,
                                  base::StrCat({package_name, ".", "activity"}),
                                  true /* sticky */);
}

// Helper class that initializes a run loop and waits for an app update for the
// specified app before quitting the run loop.
class AppUpdateWaiter {
 public:
  AppUpdateWaiter() = default;
  ~AppUpdateWaiter() = default;

  void SetUp(const std::string& app_id) {
    condition_met_ = false;
    app_id_ = app_id;
    run_loop_ = std::make_unique<base::RunLoop>();
  }

  void Wait() {
    if (!condition_met_) {
      run_loop_->Run();
    }
  }

  void MaybeStop(const std::string& app_id) {
    if (app_id != app_id_) {
      return;
    }
    if (run_loop_->running()) {
      run_loop_->Quit();
    } else {
      run_loop_.reset();
    }
    condition_met_ = true;
  }

 private:
  bool condition_met_;
  std::string app_id_;
  std::unique_ptr<base::RunLoop> run_loop_;
};

// Implementation of the AppParentalControlsObserver for testing purposes.
class AppParentalControlsTestObserver
    : public app_parental_controls::mojom::AppParentalControlsObserver {
 public:
  AppParentalControlsTestObserver() = default;
  ~AppParentalControlsTestObserver() override = default;

  void OnAppInstalledOrUpdated(
      app_parental_controls::mojom::AppPtr app) override {
    if (app_readiness_changed_.contains(app->id)) {
      ++app_readiness_changed_[app->id];
    } else {
      app_readiness_changed_[app->id] = 1;
    }
    waiter_.MaybeStop(app->id);
    recently_updated_app_ = std::move(app);
  }

  void OnAppUninstalled(app_parental_controls::mojom::AppPtr app) override {
    waiter_.MaybeStop(app->id);
    recently_uninstalled_app_ = std::move(app);
  }

  mojo::PendingRemote<app_parental_controls::mojom::AppParentalControlsObserver>
  GenerateRemote() {
    return receiver_.BindNewPipeAndPassRemote();
  }

  // Configures the `waiter_` object to wait for an update of the app identified
  // by `app_id`. `WaitForAppUpdate()` has to be called after this to actually
  // start the `RunLoop`.
  void SetUpWaiterForAppUpdate(const std::string& app_id) {
    waiter_.SetUp(app_id);
  }

  // `SetUpWaiterForAppUpdate()` must be called before calling
  // `WaitForAppUpdate()`. The behaviour can be unpredictable otherwise.
  void WaitForAppUpdate() { waiter_.Wait(); }

  const app_parental_controls::mojom::AppPtr& recently_updated_app() const {
    return recently_updated_app_;
  }

  const app_parental_controls::mojom::AppPtr& recently_uninstalled_app() const {
    return recently_uninstalled_app_;
  }

  int GetReadinessChangeCount(const std::string& app_id) const {
    return app_readiness_changed_.contains(app_id)
               ? app_readiness_changed_.at(app_id)
               : 0;
  }

 private:
  app_parental_controls::mojom::AppPtr recently_updated_app_;
  app_parental_controls::mojom::AppPtr recently_uninstalled_app_;
  std::map<std::string, int> app_readiness_changed_;
  AppUpdateWaiter waiter_;

  mojo::Receiver<app_parental_controls::mojom::AppParentalControlsObserver>
      receiver_{this};
};
}  // namespace

class AppParentalControlsHandlerBrowserTest : public InProcessBrowserTest {
 protected:
  AppParentalControlsHandlerBrowserTest() = default;
  ~AppParentalControlsHandlerBrowserTest() override = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    arc::SetArcAvailableCommandLineForTesting(command_line);
  }

  void SetUpInProcessBrowserTestFixture() override {
    arc::ArcSessionManager::SetUiEnabledForTesting(false);
  }

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    arc::SetArcPlayStoreEnabledForProfile(profile(), true);

    arc_app_list_prefs_ = ArcAppListPrefs::Get(profile());
    EXPECT_TRUE(arc_app_list_prefs_);

    base::RunLoop run_loop;
    arc_app_list_prefs_->SetDefaultAppsReadyCallback(run_loop.QuitClosure());
    run_loop.Run();

    arc_app_instance_ =
        std::make_unique<arc::FakeAppInstance>(arc_app_list_prefs_);

    observer_ = std::make_unique<AppParentalControlsTestObserver>();
  }

  void TearDownOnMainThread() override {
    arc_app_list_prefs_->app_connection_holder()->CloseInstance(
        arc_app_instance_.get());
    arc_app_instance_.reset();
    arc::ArcSessionManager::Get()->Shutdown();
    arc_app_list_prefs_ = nullptr;
  }

  std::string InstallArcApp(const std::string& package_name) {
    base::RunLoop run_loop;
    arc_app_instance_->SendPackageAdded(
        CreateArcAppPackage(package_name)->Clone());

    std::vector<arc::mojom::AppInfoPtr> apps;
    apps.emplace_back(CreateArcAppInfo(package_name));

    arc_app_instance_->SendPackageAppListRefreshed(package_name, apps);
    run_loop.RunUntilIdle();

    return arc::ArcPackageNameToAppId(package_name, profile());
  }

  void UninstallArcPackage(const std::string& package_name) {
    arc_app_instance_->UninstallPackage(package_name);
  }

  void InstallNonArcApps() {
    auto web_app_install_info =
        web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
            GURL("https://example.org"));
    webapps::AppId app_id = web_app::test::InstallWebApp(
        profile(), std::move(web_app_install_info));
    ash::SystemWebAppManager::Get(profile())->InstallSystemAppsForTesting();
  }

  AppParentalControlsHandler* GetHandler() {
    return OsSettingsManagerFactory::GetForProfile(profile())
        ->app_parental_controls_handler();
  }

  Profile* profile() { return browser()->profile(); }
  AppParentalControlsTestObserver* observer() { return observer_.get(); }

 private:
  raw_ptr<ArcAppListPrefs> arc_app_list_prefs_ = nullptr;
  std::unique_ptr<arc::FakeAppInstance> arc_app_instance_;
  std::unique_ptr<AppParentalControlsTestObserver> observer_;
};

IN_PROC_BROWSER_TEST_F(AppParentalControlsHandlerBrowserTest,
                       OnlyManageableArcAppsFetched) {
  AppParentalControlsHandler* handler = GetHandler();
  InstallNonArcApps();
  std::string arc_app_id1 = InstallArcApp("com.example.app1");
  std::string arc_app_id2 = InstallArcApp("com.example.app2");

  base::RunLoop run_loop;
  handler->GetApps(base::BindLambdaForTesting(
      [&](std::vector<app_parental_controls::mojom::AppPtr> apps) -> void {
        EXPECT_EQ(apps.size(), 2u);
        EXPECT_EQ(apps[0]->id, arc_app_id2);
        EXPECT_EQ(apps[1]->id, arc_app_id1);
        run_loop.Quit();
      }));
  run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(AppParentalControlsHandlerBrowserTest,
                       BlockAndUnblockApp) {
  AppParentalControlsHandler* handler = GetHandler();
  handler->AddObserver(observer()->GenerateRemote());

  std::string arc_app_id = InstallArcApp("com.example.app1");

  EXPECT_EQ(observer()->GetReadinessChangeCount(arc_app_id), 1);
  EXPECT_EQ(observer()->recently_updated_app()->id, arc_app_id);

  observer()->SetUpWaiterForAppUpdate(arc_app_id);
  handler->UpdateApp(arc_app_id, /*is_blocked=*/true);
  observer()->WaitForAppUpdate();

  EXPECT_EQ(observer()->GetReadinessChangeCount(arc_app_id), 2);
  EXPECT_EQ(observer()->recently_updated_app()->id, arc_app_id);

  base::RunLoop run_loop;
  handler->GetApps(base::BindLambdaForTesting(
      [&](std::vector<app_parental_controls::mojom::AppPtr> apps) -> void {
        EXPECT_EQ(apps.size(), 1u);
        EXPECT_EQ(apps[0]->id, arc_app_id);
        EXPECT_EQ(apps[0]->is_blocked, true);
        run_loop.Quit();
      }));
  run_loop.Run();

  observer()->SetUpWaiterForAppUpdate(arc_app_id);
  handler->UpdateApp(arc_app_id, /*is_blocked=*/false);
  observer()->WaitForAppUpdate();

  EXPECT_EQ(observer()->GetReadinessChangeCount(arc_app_id), 3);
  EXPECT_EQ(observer()->recently_updated_app()->id, arc_app_id);

  base::RunLoop run_loop2;
  handler->GetApps(base::BindLambdaForTesting(
      [&](std::vector<app_parental_controls::mojom::AppPtr> apps) -> void {
        EXPECT_EQ(apps.size(), 1u);
        EXPECT_EQ(apps[0]->id, arc_app_id);
        EXPECT_EQ(apps[0]->is_blocked, false);
        run_loop2.Quit();
      }));
  run_loop2.Run();
}

IN_PROC_BROWSER_TEST_F(AppParentalControlsHandlerBrowserTest, UninstallApp) {
  AppParentalControlsHandler* handler = GetHandler();
  handler->AddObserver(observer()->GenerateRemote());

  std::string arc_app_package1 = "com.example.app1";
  std::string arc_app_id1 = InstallArcApp(arc_app_package1);
  std::string arc_app_id2 = InstallArcApp("com.example.app2");

  base::RunLoop run_loop;
  handler->GetApps(base::BindLambdaForTesting(
      [&](std::vector<app_parental_controls::mojom::AppPtr> apps) -> void {
        EXPECT_EQ(apps.size(), 2u);
        run_loop.Quit();
      }));
  run_loop.Run();

  observer()->SetUpWaiterForAppUpdate(arc_app_id1);
  UninstallArcPackage(arc_app_package1);
  observer()->WaitForAppUpdate();

  base::RunLoop run_loop2;
  handler->GetApps(base::BindLambdaForTesting(
      [&](std::vector<app_parental_controls::mojom::AppPtr> apps) -> void {
        EXPECT_EQ(apps.size(), 1u);
        EXPECT_EQ(apps[0]->id, arc_app_id2);
        run_loop2.Quit();
      }));
  run_loop2.Run();

  EXPECT_EQ(observer()->recently_uninstalled_app()->id, arc_app_id1);
}

IN_PROC_BROWSER_TEST_F(AppParentalControlsHandlerBrowserTest,
                       OnControlsDisabled) {
  AppParentalControlsHandler* handler = GetHandler();
  handler->AddObserver(observer()->GenerateRemote());

  std::string arc_app_id = InstallArcApp("com.example.app1");

  EXPECT_EQ(observer()->GetReadinessChangeCount(arc_app_id), 1);
  EXPECT_EQ(observer()->recently_updated_app()->id, arc_app_id);

  std::string pin = "123456";
  base::RunLoop run_loop;
  handler->SetUpPin(
      pin, base::BindLambdaForTesting(([&](bool isSuccess) -> void {
        ASSERT_TRUE(isSuccess);
        ASSERT_TRUE(profile()->GetPrefs()->GetBoolean(
            prefs::kOnDeviceAppControlsSetupCompleted));
        EXPECT_EQ(
            profile()->GetPrefs()->GetString(prefs::kOnDeviceAppControlsPin),
            pin);
        run_loop.Quit();
      })));

  observer()->SetUpWaiterForAppUpdate(arc_app_id);
  handler->UpdateApp(arc_app_id, /*is_blocked=*/true);
  observer()->WaitForAppUpdate();

  EXPECT_EQ(observer()->GetReadinessChangeCount(arc_app_id), 2);
  EXPECT_EQ(observer()->recently_updated_app()->id, arc_app_id);

  observer()->SetUpWaiterForAppUpdate(arc_app_id);
  handler->OnControlsDisabled();
  observer()->WaitForAppUpdate();

  EXPECT_EQ(observer()->GetReadinessChangeCount(arc_app_id), 3);
  EXPECT_EQ(observer()->recently_updated_app()->id, arc_app_id);

  ASSERT_FALSE(profile()->GetPrefs()->GetBoolean(
      prefs::kOnDeviceAppControlsSetupCompleted));
  EXPECT_EQ(profile()->GetPrefs()->GetString(prefs::kOnDeviceAppControlsPin),
            std::string());
}

IN_PROC_BROWSER_TEST_F(AppParentalControlsHandlerBrowserTest,
                       ValidatePinSuccess) {
  AppParentalControlsHandler* handler = GetHandler();

  base::RunLoop run_loop;
  handler->ValidatePin(
      "123456",
      base::BindLambdaForTesting(
          [&](app_parental_controls::mojom::PinValidationResult result)
              -> void {
            EXPECT_EQ(result, app_parental_controls::mojom::
                                  PinValidationResult::kPinValidationSuccess);
            run_loop.Quit();
          }));
  run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(AppParentalControlsHandlerBrowserTest,
                       ValidatePinEmptyString) {
  AppParentalControlsHandler* handler = GetHandler();

  base::RunLoop run_loop;
  handler->ValidatePin(
      std::string(),
      base::BindLambdaForTesting([&](app_parental_controls::mojom::
                                         PinValidationResult result) -> void {
        EXPECT_EQ(
            result,
            app_parental_controls::mojom::PinValidationResult::kPinLengthError);
        run_loop.Quit();
      }));
  run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(AppParentalControlsHandlerBrowserTest,
                       ValidatePinTooShortError) {
  AppParentalControlsHandler* handler = GetHandler();

  base::RunLoop run_loop;
  handler->ValidatePin(
      "1234",
      base::BindLambdaForTesting([&](app_parental_controls::mojom::
                                         PinValidationResult result) -> void {
        EXPECT_EQ(
            result,
            app_parental_controls::mojom::PinValidationResult::kPinLengthError);
        run_loop.Quit();
      }));
  run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(AppParentalControlsHandlerBrowserTest,
                       ValidatePinTooLongError) {
  AppParentalControlsHandler* handler = GetHandler();

  base::RunLoop run_loop;
  handler->ValidatePin(
      "1234567",
      base::BindLambdaForTesting([&](app_parental_controls::mojom::
                                         PinValidationResult result) -> void {
        EXPECT_EQ(
            result,
            app_parental_controls::mojom::PinValidationResult::kPinLengthError);
        run_loop.Quit();
      }));
  run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(AppParentalControlsHandlerBrowserTest,
                       ValidatePinNumericError) {
  AppParentalControlsHandler* handler = GetHandler();

  base::RunLoop run_loop;
  handler->ValidatePin(
      "1a3%56",
      base::BindLambdaForTesting(
          [&](app_parental_controls::mojom::PinValidationResult result)
              -> void {
            EXPECT_EQ(result, app_parental_controls::mojom::
                                  PinValidationResult::kPinNumericError);
            run_loop.Quit();
          }));
  run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(AppParentalControlsHandlerBrowserTest,
                       ValidatePinWithSpecialCharactersNumericError) {
  AppParentalControlsHandler* handler = GetHandler();

  base::RunLoop run_loop;
  handler->ValidatePin(
      "1a3%รป", base::BindLambdaForTesting(
                   [&](app_parental_controls::mojom::PinValidationResult result)
                       -> void {
                     EXPECT_EQ(result,
                               app_parental_controls::mojom::
                                   PinValidationResult::kPinNumericError);
                     run_loop.Quit();
                   }));
  run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(AppParentalControlsHandlerBrowserTest, SetUpValidPin) {
  AppParentalControlsHandler* handler = GetHandler();

  std::string pin = "123456";
  base::RunLoop run_loop;
  handler->SetUpPin(
      pin, base::BindLambdaForTesting([&](bool is_success) -> void {
        ASSERT_TRUE(is_success);
        ASSERT_TRUE(profile()->GetPrefs()->GetBoolean(
            prefs::kOnDeviceAppControlsSetupCompleted));
        EXPECT_EQ(
            profile()->GetPrefs()->GetString(prefs::kOnDeviceAppControlsPin),
            pin);
        run_loop.Quit();
      }));
  run_loop.Run();
}

IN_PROC_BROWSER_TEST_F(AppParentalControlsHandlerBrowserTest, SetUpInvalidPin) {
  AppParentalControlsHandler* handler = GetHandler();

  base::RunLoop run_loop;
  handler->SetUpPin("1a3%56",
                    base::BindLambdaForTesting([&](bool is_success) -> void {
                      ASSERT_FALSE(is_success);
                      run_loop.Quit();
                    }));
  run_loop.Run();
}

}  // namespace ash::settings