chromium/ash/webui/projector_app/test/untrusted_projector_page_handler_impl_unittest.cc

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

#include "ash/webui/projector_app/untrusted_projector_page_handler_impl.h"

#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/projector/projector_new_screencast_precondition.h"
#include "ash/public/cpp/test/mock_projector_controller.h"
#include "ash/webui/projector_app/mojom/untrusted_projector.mojom.h"
#include "ash/webui/projector_app/public/mojom/projector_types.mojom.h"
#include "ash/webui/projector_app/test/mock_app_client.h"
#include "base/files/safe_base_name.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest-death-test.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

namespace {

constexpr char kTestUserEmail[] = "[email protected]";
const char kVideoFileId[] = "video_file_id";
const char kResourceKey[] = "resource_key";

constexpr char kTestXhrUrl[] =
    "https://www.googleapis.com/drive/v3/files/fileID";
constexpr char kTestXhrUnsupportedUrl[] = "https://www.example.com";
constexpr ash::projector::mojom::RequestType kTestXhrMethod =
    ash::projector::mojom::RequestType::kPost;
constexpr char kTestXhrRequestBody[] = "{}";
constexpr char kTestXhrHeaderKey[] = "X-Goog-Drive-Resource-Keys";
constexpr char kTestXhrHeaderValue[] = "resource-key";
constexpr char kTestResponseBody[] = "{}";

// MOCK the Projector page instance in the WebUI renderer.
class MockUntrustedProjectorPageJs
    : public projector::mojom::UntrustedProjectorPage {
 public:
  MockUntrustedProjectorPageJs() = default;
  MockUntrustedProjectorPageJs(const MockUntrustedProjectorPageJs&) = delete;
  MockUntrustedProjectorPageJs& operator=(const MockUntrustedProjectorPageJs&) =
      delete;
  ~MockUntrustedProjectorPageJs() override = default;

  MOCK_METHOD1(OnNewScreencastPreconditionChanged,
               void(const NewScreencastPrecondition& precondition));
  MOCK_METHOD1(OnSodaInstallProgressUpdated, void(int32_t));
  MOCK_METHOD0(OnSodaInstalled, void());
  MOCK_METHOD0(OnSodaInstallError, void());
  MOCK_METHOD1(OnScreencastsStateChange,
               void(std::vector<projector::mojom::PendingScreencastPtr>
                        pending_screencasts));

  void FlushReceiverForTesting() { receiver_.FlushForTesting(); }

  void FlushRemoteForTesting() { page_handler_.FlushForTesting(); }

  mojo::Receiver<projector::mojom::UntrustedProjectorPage>& receiver() {
    return receiver_;
  }
  mojo::Remote<projector::mojom::UntrustedProjectorPageHandler>&
  page_handler() {
    return page_handler_;
  }

 private:
  mojo::Receiver<projector::mojom::UntrustedProjectorPage> receiver_{this};
  mojo::Remote<projector::mojom::UntrustedProjectorPageHandler> page_handler_;
};

}  // namespace

class UntrustedProjectorPageHandlerImplUnitTest : public testing::Test {
 public:
  UntrustedProjectorPageHandlerImplUnitTest() = default;
  UntrustedProjectorPageHandlerImplUnitTest(
      const UntrustedProjectorPageHandlerImplUnitTest&) = delete;
  UntrustedProjectorPageHandlerImplUnitTest& operator=(
      const UntrustedProjectorPageHandlerImplUnitTest&) = delete;
  ~UntrustedProjectorPageHandlerImplUnitTest() override = default;

  void SetUp() override {
    auto* registry = pref_service_.registry();
    registry->RegisterBooleanPref(ash::prefs::kProjectorCreationFlowEnabled,
                                  false);
    registry->RegisterBooleanPref(
        ash::prefs::kProjectorExcludeTranscriptDialogShown, false);
    registry->RegisterIntegerPref(
        ash::prefs::kProjectorGalleryOnboardingShowCount, 0);
    registry->RegisterIntegerPref(
        ash::prefs::kProjectorViewerOnboardingShowCount, 0);

    page_ = std::make_unique<MockUntrustedProjectorPageJs>();
    handler_impl_ = std::make_unique<UntrustedProjectorPageHandlerImpl>(
        page().page_handler().BindNewPipeAndPassReceiver(),
        page().receiver().BindNewPipeAndPassRemote(), &pref_service_);
  }

  void TearDown() override {
    handler_impl_.reset();
    page_.reset();
  }

  MockProjectorController& controller() { return mock_controller_; }
  MockUntrustedProjectorPageJs& page() { return *page_; }
  UntrustedProjectorPageHandlerImpl& handler() { return *handler_impl_; }
  MockAppClient& mock_app_client() { return mock_app_client_; }

 protected:
  void TestUserPref(projector::mojom::PrefsThatProjectorCanAskFor pref,
                    base::Value value);

 private:
  base::test::SingleThreadTaskEnvironment task_environment_;

  MockProjectorController mock_controller_;
  MockAppClient mock_app_client_;
  std::unique_ptr<MockUntrustedProjectorPageJs> page_;
  std::unique_ptr<UntrustedProjectorPageHandlerImpl> handler_impl_;
  TestingPrefServiceSimple pref_service_;
};

void UntrustedProjectorPageHandlerImplUnitTest::TestUserPref(
    projector::mojom::PrefsThatProjectorCanAskFor pref,
    base::Value value) {
  base::test::TestFuture<void> set_pref_future;
  page().page_handler()->SetUserPref(pref, value.Clone(),
                                     set_pref_future.GetCallback());
  set_pref_future.Get();

  base::test::TestFuture<base::Value> get_pref_future;
  page().page_handler()->GetUserPref(pref, get_pref_future.GetCallback());

  EXPECT_EQ(get_pref_future.Get(), value);
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, CanStartProjectorSession) {
  NewScreencastPrecondition precondition = NewScreencastPrecondition(
      NewScreencastPreconditionState::kEnabled,
      {NewScreencastPreconditionReason::kEnabledBySoda});

  ON_CALL(controller(), GetNewScreencastPrecondition)
      .WillByDefault(testing::Return(precondition));

  base::test::TestFuture<const NewScreencastPrecondition&>
      new_screencast_precondition_future;

  page().page_handler()->GetNewScreencastPrecondition(
      new_screencast_precondition_future.GetCallback());

  const auto& result = new_screencast_precondition_future.Get();
  EXPECT_EQ(result.state, ash::NewScreencastPreconditionState::kEnabled);
  EXPECT_EQ(result.reasons.size(), 1u);
  EXPECT_EQ(result.reasons[0],
            ash::NewScreencastPreconditionReason::kEnabledBySoda);
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest,
       NewScreencastPreconditionChanged) {
  EXPECT_CALL(page(), OnNewScreencastPreconditionChanged(testing::_)).Times(1);
  NewScreencastPrecondition precondition = NewScreencastPrecondition(
      NewScreencastPreconditionState::kEnabled,
      {NewScreencastPreconditionReason::kEnabledBySoda});
  handler().OnNewScreencastPreconditionChanged(precondition);
  page().FlushReceiverForTesting();
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, OnSodaProgress) {
  EXPECT_CALL(page(), OnSodaInstallProgressUpdated(50)).Times(1);
  handler().OnSodaProgress(50);
  page().FlushReceiverForTesting();
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, OnSodaInstalled) {
  EXPECT_CALL(page(), OnSodaInstalled()).Times(1);
  handler().OnSodaInstalled();
  page().FlushReceiverForTesting();
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, OnSodaError) {
  EXPECT_CALL(page(), OnSodaInstallError()).Times(1);
  handler().OnSodaError();
  page().FlushReceiverForTesting();
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, ShouldDownloadSoda) {
  ON_CALL(mock_app_client(), ShouldDownloadSoda())
      .WillByDefault(testing::Return(true));
  base::test::TestFuture<bool> should_download_soda_future;
  page().page_handler()->ShouldDownloadSoda(
      should_download_soda_future.GetCallback());
  EXPECT_TRUE(should_download_soda_future.Get());
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, InstallSoda) {
  ON_CALL(mock_app_client(), InstallSoda()).WillByDefault(testing::Return());
  base::test::TestFuture<bool> install_triggered_future;
  page().page_handler()->InstallSoda(install_triggered_future.GetCallback());
  EXPECT_TRUE(install_triggered_future.Get());
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, GetPendingScreencasts) {
  const std::string name = "test_pending_screencast";
  const std::string path = "/root/projector_data/test_pending_screencast";
  const PendingScreencastContainerSet expected_screencasts{
      ash::PendingScreencastContainer(
          /*container_dir=*/base::FilePath(path), /*name=*/name,
          /*total_size_in_bytes=*/1, /*bytes_transferred=*/0)};

  ON_CALL(mock_app_client(), GetPendingScreencasts())
      .WillByDefault(testing::ReturnRef(expected_screencasts));

  base::test::TestFuture<
      std::vector<ash::projector::mojom::PendingScreencastPtr>>
      install_triggered_future;

  page().page_handler()->GetPendingScreencasts(
      install_triggered_future.GetCallback());
  const auto& pending_screencasts = install_triggered_future.Get();
  EXPECT_EQ(pending_screencasts.size(), 1u);
  const auto& pending_screencast = pending_screencasts[0];
  EXPECT_EQ(pending_screencast->name, name);
  EXPECT_EQ(pending_screencast->upload_failed, false);
  EXPECT_EQ(pending_screencast->created_time, 0.0);
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, OnScreencastsStateChange) {
  EXPECT_CALL(page(), OnScreencastsStateChange(testing::_)).Times(1);
  handler().OnScreencastsPendingStatusChanged(PendingScreencastContainerSet());
  page().FlushReceiverForTesting();
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, TestPrefs) {
  TestUserPref(projector::mojom::PrefsThatProjectorCanAskFor::
                   kProjectorCreationFlowEnabled,
               /*value=*/base::Value(true));

  TestUserPref(projector::mojom::PrefsThatProjectorCanAskFor::
                   kProjectorExcludeTranscriptDialogShown,
               /*value=*/base::Value(true));

  TestUserPref(projector::mojom::PrefsThatProjectorCanAskFor::
                   kProjectorViewerOnboardingShowCount,
               /*value=*/base::Value(3));
  TestUserPref(projector::mojom::PrefsThatProjectorCanAskFor::
                   kProjectorGalleryOnboardingShowCount,
               /*value=*/base::Value(4));
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, OpenFeedbackDialog) {
  EXPECT_CALL(mock_app_client(), OpenFeedbackDialog()).Times(1);
  base::test::TestFuture<void> open_feedback_future;
  page().page_handler()->OpenFeedbackDialog(open_feedback_future.GetCallback());
  EXPECT_TRUE(open_feedback_future.Wait());
}

class ProjectorSessionStartUnitTest
    : public ::testing::WithParamInterface<NewScreencastPrecondition>,
      public UntrustedProjectorPageHandlerImplUnitTest {
 public:
  ProjectorSessionStartUnitTest() = default;
  ProjectorSessionStartUnitTest(const ProjectorSessionStartUnitTest&) = delete;
  ProjectorSessionStartUnitTest& operator=(
      const ProjectorSessionStartUnitTest&) = delete;
  ~ProjectorSessionStartUnitTest() override = default;
};

TEST_P(ProjectorSessionStartUnitTest, ProjectorSessionTest) {
  const auto& precondition = GetParam();
  EXPECT_CALL(controller(), GetNewScreencastPrecondition());
  ON_CALL(controller(), GetNewScreencastPrecondition)
      .WillByDefault(testing::Return(precondition));

  bool expected_success =
      precondition.state == NewScreencastPreconditionState::kEnabled;

  const auto kFolderId = base::SafeBaseName::Create("folderId").value();

  EXPECT_CALL(controller(), StartProjectorSession(kFolderId))
      .Times(expected_success ? 1 : 0);

  base::test::TestFuture<bool> start_projector_session_future;
  page().page_handler()->StartProjectorSession(
      kFolderId, start_projector_session_future.GetCallback());
  EXPECT_EQ(start_projector_session_future.Get(), expected_success);
}

INSTANTIATE_TEST_SUITE_P(
    SessionStartSuccessFailTest,
    ProjectorSessionStartUnitTest,
    ::testing::Values(
        NewScreencastPrecondition(NewScreencastPreconditionState::kEnabled, {}),
        NewScreencastPrecondition(
            NewScreencastPreconditionState::kDisabled,
            {NewScreencastPreconditionReason::kInProjectorSession})));

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, SafeBaseNameTest) {
  const auto valid_path = base::FilePath("folderName");
  const auto failing_path_1 = base::FilePath("parent1/folderName");
  const auto failing_path_2 = base::FilePath("../folderId");
  const auto failing_path_3 = base::FilePath("../");
  const auto failing_path_4 = base::FilePath("parent1/../../folderName");

  EXPECT_EQ(base::SafeBaseName::Create(valid_path)->path(), valid_path);
  EXPECT_NE(base::SafeBaseName::Create(failing_path_1)->path(), failing_path_1);
  EXPECT_NE(base::SafeBaseName::Create(failing_path_2)->path(), failing_path_2);
  EXPECT_NE(base::SafeBaseName::Create(failing_path_4)->path(), failing_path_4);

  // The safe base name would not even be created in this instance.
  EXPECT_FALSE(base::SafeBaseName::Create(failing_path_3));
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, SendXhr) {
  mock_app_client().test_url_loader_factory().AddResponse(kTestXhrUrl,
                                                          kTestResponseBody);
  const base::flat_map<std::string, std::string> headers{
      {std::string(kTestXhrHeaderKey), std::string(kTestXhrHeaderValue)}};

  base::test::TestFuture<projector::mojom::XhrResponsePtr>
      send_xhr_request_future;
  page().page_handler()->SendXhr(
      GURL(kTestXhrUrl), kTestXhrMethod, kTestXhrRequestBody,
      /*use_credentials=*/true,
      /*use_api_key=*/false, headers,
      /*email=*/std::nullopt, send_xhr_request_future.GetCallback());
  mock_app_client().WaitForAccessRequest(kTestUserEmail);

  const auto& response = send_xhr_request_future.Get();
  EXPECT_EQ(response->response, kTestResponseBody);
  EXPECT_EQ(response->response_code,
            projector::mojom::XhrResponseCode::kSuccess);
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, SendXhrEmptyEmail) {
  mock_app_client().test_url_loader_factory().AddResponse(kTestXhrUrl,
                                                          kTestResponseBody);
  const base::flat_map<std::string, std::string> headers{
      {std::string(kTestXhrHeaderKey), std::string(kTestXhrHeaderValue)}};

  base::test::TestFuture<projector::mojom::XhrResponsePtr>
      send_xhr_request_future;
  page().page_handler()->SendXhr(
      GURL(kTestXhrUrl), kTestXhrMethod, kTestXhrRequestBody,
      /*use_credentials=*/true,
      /*use_api_key=*/false, headers,
      /*email=*/"", send_xhr_request_future.GetCallback());
  mock_app_client().WaitForAccessRequest(kTestUserEmail);

  const auto& response = send_xhr_request_future.Get();
  EXPECT_EQ(response->response, kTestResponseBody);
  EXPECT_EQ(response->response_code,
            projector::mojom::XhrResponseCode::kSuccess);
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, SendXhrWithEmail) {
  mock_app_client().test_url_loader_factory().AddResponse(kTestXhrUrl,
                                                          kTestResponseBody);
  const base::flat_map<std::string, std::string> headers{
      {std::string(kTestXhrHeaderKey), std::string(kTestXhrHeaderValue)}};
  base::test::TestFuture<projector::mojom::XhrResponsePtr>
      send_xhr_request_future;
  page().page_handler()->SendXhr(GURL(kTestXhrUrl), kTestXhrMethod,
                                 kTestXhrRequestBody,
                                 /*use_credentials=*/true,
                                 /*use_api_key=*/false, headers, kTestUserEmail,
                                 send_xhr_request_future.GetCallback());
  mock_app_client().WaitForAccessRequest(kTestUserEmail);

  const auto& response = send_xhr_request_future.Get();
  EXPECT_EQ(response->response, kTestResponseBody);
  EXPECT_EQ(response->response_code,
            projector::mojom::XhrResponseCode::kSuccess);
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, SendXhrFailed) {
  constexpr char kTestErrorResponseBody[] = "error";
  mock_app_client().test_url_loader_factory().AddResponse(
      /*url=*/kTestXhrUrl,
      /*content=*/kTestErrorResponseBody,
      /*status=*/net::HttpStatusCode::HTTP_NOT_FOUND);
  const base::flat_map<std::string, std::string> headers{
      {std::string(kTestXhrHeaderKey), std::string(kTestXhrHeaderValue)}};
  base::test::TestFuture<projector::mojom::XhrResponsePtr>
      send_xhr_request_future;
  page().page_handler()->SendXhr(GURL(kTestXhrUrl), kTestXhrMethod,
                                 kTestXhrRequestBody,
                                 /*use_credentials=*/true,
                                 /*use_api_key=*/false, headers, kTestUserEmail,
                                 send_xhr_request_future.GetCallback());

  mock_app_client().WaitForAccessRequest(kTestUserEmail);
  const auto& response = send_xhr_request_future.Get();
  EXPECT_EQ(response->response, kTestErrorResponseBody);
  EXPECT_EQ(response->response_code,
            projector::mojom::XhrResponseCode::kXhrFetchFailure);
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, SendXhrWithUnSupportedUrl) {
  auto crashing_lambda_test = [&]() {
    const base::flat_map<std::string, std::string> headers{
        {std::string(kTestXhrHeaderKey), std::string(kTestXhrHeaderValue)}};

    base::test::TestFuture<projector::mojom::XhrResponsePtr>
        send_xhr_request_future;

    page().page_handler()->SendXhr(
        GURL(kTestXhrUnsupportedUrl), kTestXhrMethod, kTestXhrRequestBody,
        /*use_credentials=*/true,
        /*use_api_key=*/false, headers, kTestUserEmail,
        send_xhr_request_future.GetCallback());

    const auto& response = send_xhr_request_future.Get();
    EXPECT_EQ(response->response_code,
              projector::mojom::XhrResponseCode::kUnsupportedURL);
  };

  EXPECT_DEATH_IF_SUPPORTED(crashing_lambda_test(), "");
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, GetAccounts) {
  base::test::TestFuture<std::vector<projector::mojom::AccountPtr>>
      get_accounts_future;
  page().page_handler()->GetAccounts(get_accounts_future.GetCallback());
  const auto& accounts = get_accounts_future.Get();
  EXPECT_EQ(accounts.size(), 1u);
  EXPECT_EQ(accounts[0]->email, kTestUserEmail);
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, GetVideo) {
  auto expected_video = projector::mojom::VideoInfo::New();
  expected_video->file_id = kVideoFileId;

  EXPECT_CALL(mock_app_client(),
              GetVideo(kVideoFileId, std::optional<std::string>(kResourceKey),
                       testing::_))
      .WillOnce([&expected_video](
                    const std::string& video_file_id,
                    const std::optional<std::string>& resource_key,
                    ProjectorAppClient::OnGetVideoCallback callback) {
        std::move(callback).Run(
            projector::mojom::GetVideoResult::NewVideo(expected_video.Clone()));
      });

  base::test::TestFuture<projector::mojom::GetVideoResultPtr> get_video_future;
  page().page_handler()->GetVideo(kVideoFileId, kResourceKey,
                                  get_video_future.GetCallback());

  const auto& result = get_video_future.Get<0>();
  EXPECT_FALSE(result->is_error_message());
  EXPECT_TRUE(result->is_video());
  EXPECT_EQ(result->get_video()->file_id, expected_video->file_id);
}

TEST_F(UntrustedProjectorPageHandlerImplUnitTest, GetVideoFail) {
  EXPECT_CALL(mock_app_client(), GetVideo(kVideoFileId, testing::_, testing::_))
      .WillOnce([](const std::string& video_file_id,
                   const std::optional<std::string>& resource_key,
                   ProjectorAppClient::OnGetVideoCallback callback) {
        EXPECT_FALSE(resource_key);
        std::move(callback).Run(
            projector::mojom::GetVideoResult::NewErrorMessage("error1"));
      });

  base::test::TestFuture<projector::mojom::GetVideoResultPtr> get_video_future;
  page().page_handler()->GetVideo(kVideoFileId, std::nullopt,
                                  get_video_future.GetCallback());

  const auto& result = get_video_future.Get<0>();
  EXPECT_TRUE(result->is_error_message());
  EXPECT_EQ(result->get_error_message(), "error1");
}

}  // namespace ash