chromium/chrome/browser/ash/crosapi/parent_access_ash_unittest.cc

// Copyright 2022 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/crosapi/parent_access_ash.h"

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

#include "base/memory/raw_ptr.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chrome/browser/ui/webui/ash/parent_access/parent_access_dialog.h"
#include "extensions/browser/extension_util.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/permissions/permission_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "url/gurl.h"

using crosapi::mojom::ParentAccessResultPtr;

class FakeParentAccessDialogProvider : public ash::ParentAccessDialogProvider {
 public:
  ParentAccessDialogProvider::ShowError Show(
      parent_access_ui::mojom::ParentAccessParamsPtr params,
      ash::ParentAccessDialog::Callback callback) override {
    callback_ = std::move(callback);
    last_params_received_ = std::move(params);
    return next_show_error_;
  }

  void SetNextShowError(ash::ParentAccessDialogProvider::ShowError error) {
    next_show_error_ = error;
  }

  parent_access_ui::mojom::ParentAccessParamsPtr GetLastParamsReceived() {
    return std::move(last_params_received_);
  }

  void TriggerCallbackWithResult(
      std::unique_ptr<ash::ParentAccessDialog::Result> result) {
    std::move(callback_).Run(std::move(result));
  }

 private:
  ash::ParentAccessDialog::Callback callback_;
  parent_access_ui::mojom::ParentAccessParamsPtr last_params_received_;
  ash::ParentAccessDialogProvider::ShowError next_show_error_;
};

namespace {
constexpr char test_url[] = "http://example.com";
const std::u16string test_child_display_name = u"child display name";
const gfx::ImageSkia test_favicon = gfx::test::CreateImageSkia(1, 2);
const std::u16string test_extension_name = u"extension";
}  // namespace

class ParentAccessAshTest : public testing::Test {
 public:
  ParentAccessAshTest() = default;
  ~ParentAccessAshTest() override = default;

  // testing::Test:
  void SetUp() override {
    parent_access_ash_ = std::make_unique<crosapi::ParentAccessAsh>();
    parent_access_ash_->BindReceiver(
        parent_access_remote_.BindNewPipeAndPassReceiver());
    dialog_provider_ = static_cast<FakeParentAccessDialogProvider*>(
        parent_access_ash_->SetDialogProviderForTest(
            std::make_unique<FakeParentAccessDialogProvider>()));
  }

  void TearDown() override { parent_access_ash_.reset(); }

 protected:
  base::test::TaskEnvironment task_environment_;
  mojo::Remote<crosapi::mojom::ParentAccess> parent_access_remote_;
  std::unique_ptr<crosapi::ParentAccessAsh> parent_access_ash_;
  raw_ptr<FakeParentAccessDialogProvider, DanglingUntriaged> dialog_provider_;
};

// Tests that the correct parameters were passed through to the dialog for
// the GetWebsiteParentApproval method.
TEST_F(ParentAccessAshTest, GetWebsiteParentApprovalParams) {
  parent_access_ash_->GetWebsiteParentApproval(
      GURL(test_url), test_child_display_name, test_favicon, base::DoNothing());

  parent_access_ui::mojom::ParentAccessParamsPtr params =
      dialog_provider_->GetLastParamsReceived();

  // Check for the correct flow type.
  EXPECT_EQ(
      params->flow_type,
      parent_access_ui::mojom::ParentAccessParams::FlowType::kWebsiteAccess);
  // Check for the correct URL.
  EXPECT_EQ(params->flow_type_params->get_web_approvals_params()->url,
            GURL(test_url));
  // Check for the correct child display name.
  EXPECT_EQ(
      params->flow_type_params->get_web_approvals_params()->child_display_name,
      test_child_display_name);

  // Encode the test bitmap.
  std::vector<uint8_t> favicon_bitmap;
  gfx::PNGCodec::FastEncodeBGRASkBitmap(*test_favicon.bitmap(), false,
                                        &favicon_bitmap);
  // Make sure it's there.
  EXPECT_EQ(
      params->flow_type_params->get_web_approvals_params()->favicon_png_bytes,
      favicon_bitmap);
}

TEST_F(ParentAccessAshTest, GetExtensionParentApprovalParams) {
  parent_access_ash_->GetExtensionParentApproval(
      test_extension_name, test_child_display_name,
      extensions::util::GetDefaultExtensionIcon(), {}, false,
      base::DoNothing());

  parent_access_ui::mojom::ParentAccessParamsPtr params =
      dialog_provider_->GetLastParamsReceived();

  // Verify request params.
  EXPECT_EQ(
      params->flow_type,
      parent_access_ui::mojom::ParentAccessParams::FlowType::kExtensionAccess);
  EXPECT_EQ(params->is_disabled, false);
  EXPECT_EQ(params->flow_type_params->get_extension_approvals_params()
                ->child_display_name,
            test_child_display_name);
  EXPECT_EQ(params->flow_type_params->get_extension_approvals_params()
                ->extension_name,
            test_extension_name);
  std::vector<uint8_t> icon_bitmap;
  gfx::PNGCodec::FastEncodeBGRASkBitmap(
      *extensions::util::GetDefaultExtensionIcon().bitmap(), false,
      &icon_bitmap);
  EXPECT_EQ(params->flow_type_params->get_extension_approvals_params()
                ->icon_png_bytes,
            icon_bitmap);
}

TEST_F(ParentAccessAshTest, GetExtensionApprovalParamsForExtensionDisabled) {
  parent_access_ash_->GetExtensionParentApproval(
      test_extension_name, test_child_display_name,
      extensions::util::GetDefaultExtensionIcon(), {}, true, base::DoNothing());

  parent_access_ui::mojom::ParentAccessParamsPtr params =
      dialog_provider_->GetLastParamsReceived();

  // Verify request params.
  EXPECT_EQ(
      params->flow_type,
      parent_access_ui::mojom::ParentAccessParams::FlowType::kExtensionAccess);
  EXPECT_EQ(params->is_disabled, true);
}

// Makes sure the correct result is returned by the crosapi when the request is
// approved.
TEST_F(ParentAccessAshTest, GetWebsiteParentApproval_Approved) {
  base::test::TestFuture<ParentAccessResultPtr> future;
  parent_access_ash_->GetWebsiteParentApproval(
      GURL(test_url), test_child_display_name, test_favicon,
      future.GetCallback());

  auto dialog_result = std::make_unique<ash::ParentAccessDialog::Result>();
  dialog_result->status = ash::ParentAccessDialog::Result::Status::kApproved;
  dialog_result->parent_access_token = "ABC123";
  dialog_result->parent_access_token_expire_timestamp =
      base::Time::FromSecondsSinceUnixEpoch(123456UL);
  dialog_provider_->TriggerCallbackWithResult(std::move(dialog_result));

  const ParentAccessResultPtr result = future.Take();
  ASSERT_TRUE(result->is_approved());
  EXPECT_EQ(result->get_approved()->parent_access_token, "ABC123");
  EXPECT_EQ(result->get_approved()->parent_access_token_expire_timestamp,
            base::Time::FromSecondsSinceUnixEpoch(123456UL));
}

// Makes sure the correct result is returned by the crosapi when the request
// is declined.
TEST_F(ParentAccessAshTest, GetWebsiteParentApproval_Declined) {
  base::test::TestFuture<ParentAccessResultPtr> future;
  parent_access_ash_->GetWebsiteParentApproval(
      GURL(test_url), test_child_display_name, test_favicon,
      future.GetCallback());

  auto dialog_result = std::make_unique<ash::ParentAccessDialog::Result>();
  dialog_result->status = ash::ParentAccessDialog::Result::Status::kDeclined;
  dialog_provider_->TriggerCallbackWithResult(std::move(dialog_result));

  const ParentAccessResultPtr result = future.Take();
  EXPECT_TRUE(result->is_declined());
}

// Makes sure an cancel result is returned by the crosapi when the request
// is canceled.
TEST_F(ParentAccessAshTest, GetWebsiteParentApproval_Canceled) {
  base::test::TestFuture<ParentAccessResultPtr> future;
  parent_access_ash_->GetWebsiteParentApproval(
      GURL(test_url), test_child_display_name, test_favicon,
      future.GetCallback());

  auto dialog_result = std::make_unique<ash::ParentAccessDialog::Result>();
  dialog_result->status = ash::ParentAccessDialog::Result::Status::kCanceled;
  dialog_provider_->TriggerCallbackWithResult(std::move(dialog_result));

  const ParentAccessResultPtr result = future.Take();
  EXPECT_TRUE(result->is_canceled());
}

// Makes sure an error result is returned by the crosapi when the request
// had an error.
TEST_F(ParentAccessAshTest, GetWebsiteParentApproval_Error) {
  base::test::TestFuture<ParentAccessResultPtr> future;
  parent_access_ash_->GetWebsiteParentApproval(
      GURL(test_url), test_child_display_name, test_favicon,
      future.GetCallback());

  auto dialog_result = std::make_unique<ash::ParentAccessDialog::Result>();
  dialog_result->status = ash::ParentAccessDialog::Result::Status::kError;
  dialog_provider_->TriggerCallbackWithResult(std::move(dialog_result));

  const ParentAccessResultPtr result = future.Take();
  ASSERT_TRUE(result->is_error());
  EXPECT_EQ(result->get_error()->type,
            crosapi::mojom::ParentAccessErrorResult::Type::kUnknown);
}

// Makes sure only one ParentAccess request can exist at a time..
TEST_F(ParentAccessAshTest, GetWebsiteParentApproval_AlreadyVisible) {
  base::test::TestFuture<ParentAccessResultPtr> successful_show_future;
  parent_access_ash_->GetWebsiteParentApproval(
      GURL(test_url), test_child_display_name, test_favicon,
      successful_show_future.GetCallback());

  dialog_provider_->SetNextShowError(
      ash::ParentAccessDialogProvider::ShowError::kDialogAlreadyVisible);

  // Show dialog again, should be blocked because it is already visible.
  base::test::TestFuture<ParentAccessResultPtr> already_visible_future;
  parent_access_ash_->GetWebsiteParentApproval(
      GURL(test_url), test_child_display_name, test_favicon,
      already_visible_future.GetCallback());

  const ParentAccessResultPtr already_visible_result =
      already_visible_future.Take();
  ASSERT_TRUE(already_visible_result->is_error());
  // The error was that it is already visible.
  EXPECT_EQ(already_visible_result->get_error()->type,
            crosapi::mojom::ParentAccessErrorResult::Type::kAlreadyVisible);

  auto dialog_result = std::make_unique<ash::ParentAccessDialog::Result>();
  dialog_result->status = ash::ParentAccessDialog::Result::Status::kApproved;
  dialog_provider_->TriggerCallbackWithResult(std::move(dialog_result));

  const ParentAccessResultPtr show_result = successful_show_future.Take();
  EXPECT_TRUE(show_result->is_approved());
}

// Makes sure regular users can't request parent access.
TEST_F(ParentAccessAshTest, GetWebsiteParentApproval_NotAChildUser) {
  dialog_provider_->SetNextShowError(
      ash::ParentAccessDialogProvider::ShowError::kNotAChildUser);

  base::test::TestFuture<ParentAccessResultPtr> future;
  parent_access_ash_->GetWebsiteParentApproval(
      GURL(test_url), test_child_display_name, test_favicon,
      future.GetCallback());

  const ParentAccessResultPtr result = future.Take();
  ASSERT_TRUE(result->is_error());
  EXPECT_EQ(result->get_error()->type,
            crosapi::mojom::ParentAccessErrorResult::Type::kNotAChildUser);
}

TEST_F(ParentAccessAshTest, GetExtensionParentApproval_Canceled) {
  base::test::TestFuture<ParentAccessResultPtr> future;
  parent_access_ash_->GetExtensionParentApproval(
      test_extension_name, test_child_display_name,
      extensions::util::GetDefaultExtensionIcon(), {}, true,
      future.GetCallback());

  auto dialog_result = std::make_unique<ash::ParentAccessDialog::Result>();
  dialog_result->status = ash::ParentAccessDialog::Result::Status::kCanceled;
  dialog_provider_->TriggerCallbackWithResult(std::move(dialog_result));

  const ParentAccessResultPtr result = future.Take();
  EXPECT_TRUE(result->is_canceled());
}

TEST_F(ParentAccessAshTest, GetExtensionParentApproval_Error) {
  base::test::TestFuture<ParentAccessResultPtr> future;
  parent_access_ash_->GetExtensionParentApproval(
      test_extension_name, test_child_display_name,
      extensions::util::GetDefaultExtensionIcon(), {}, true,
      future.GetCallback());

  auto dialog_result = std::make_unique<ash::ParentAccessDialog::Result>();
  dialog_result->status = ash::ParentAccessDialog::Result::Status::kError;
  dialog_provider_->TriggerCallbackWithResult(std::move(dialog_result));

  const ParentAccessResultPtr result = future.Take();
  ASSERT_TRUE(result->is_error());
  EXPECT_EQ(result->get_error()->type,
            crosapi::mojom::ParentAccessErrorResult::Type::kUnknown);
}

TEST_F(ParentAccessAshTest, GetExtensionParentApproval_AlreadyVisible) {
  base::test::TestFuture<ParentAccessResultPtr> successful_show_future;
  parent_access_ash_->GetExtensionParentApproval(
      test_extension_name, test_child_display_name,
      extensions::util::GetDefaultExtensionIcon(), {}, true,
      successful_show_future.GetCallback());

  dialog_provider_->SetNextShowError(
      ash::ParentAccessDialogProvider::ShowError::kDialogAlreadyVisible);

  // Show dialog again, should be blocked because it is already visible.
  base::test::TestFuture<ParentAccessResultPtr> already_visible_future;
  parent_access_ash_->GetExtensionParentApproval(
      test_extension_name, test_child_display_name,
      extensions::util::GetDefaultExtensionIcon(), {}, true,
      already_visible_future.GetCallback());

  const ParentAccessResultPtr already_visible_result =
      already_visible_future.Take();
  ASSERT_TRUE(already_visible_result->is_error());
  EXPECT_EQ(already_visible_result->get_error()->type,
            crosapi::mojom::ParentAccessErrorResult::Type::kAlreadyVisible);

  auto dialog_result = std::make_unique<ash::ParentAccessDialog::Result>();
  dialog_result->status = ash::ParentAccessDialog::Result::Status::kApproved;
  dialog_provider_->TriggerCallbackWithResult(std::move(dialog_result));

  const ParentAccessResultPtr show_result = successful_show_future.Take();
  EXPECT_TRUE(show_result->is_approved());
}

TEST_F(ParentAccessAshTest, GetExtensionParentApproval_NotAChildUser) {
  dialog_provider_->SetNextShowError(
      ash::ParentAccessDialogProvider::ShowError::kNotAChildUser);

  base::test::TestFuture<ParentAccessResultPtr> future;
  parent_access_ash_->GetExtensionParentApproval(
      test_extension_name, test_child_display_name,
      extensions::util::GetDefaultExtensionIcon(), {}, true,
      future.GetCallback());

  const ParentAccessResultPtr result = future.Take();
  ASSERT_TRUE(result->is_error());
  EXPECT_EQ(result->get_error()->type,
            crosapi::mojom::ParentAccessErrorResult::Type::kNotAChildUser);
}