chromium/chrome/browser/ash/policy/remote_commands/device_command_screenshot_job_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/ash/policy/remote_commands/device_command_screenshot_job.h"

#include <map>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/memory/raw_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/test_future.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/test/base/chrome_ash_test_base.h"
#include "components/policy/proto/device_management_backend.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/png_codec.h"

namespace policy {

namespace {

namespace em = ::enterprise_management;

// String constant identifying the result field in the result payload.
const char* const kResultFieldName = "result";

const char* const kMockUploadUrl = "http://example.com/upload";

const RemoteCommandJob::UniqueIDType kUniqueID = 123456789;

// String constant identifying the upload url field in the command payload.
const char* const kUploadUrlFieldName = "fileUploadUrl";

em::RemoteCommand GenerateScreenshotCommandProto(
    RemoteCommandJob::UniqueIDType unique_id,
    base::TimeDelta age_of_command,
    const std::string upload_url) {
  em::RemoteCommand command_proto;
  command_proto.set_type(em::RemoteCommand_Type_DEVICE_SCREENSHOT);
  command_proto.set_command_id(unique_id);
  command_proto.set_age_of_command(age_of_command.InMilliseconds());
  std::string payload;
  auto root_dict = base::Value::Dict().Set(kUploadUrlFieldName, upload_url);
  base::JSONWriter::Write(root_dict, &payload);
  command_proto.set_payload(payload);
  return command_proto;
}

class MockUploadJob : public UploadJob {
 public:
  // If |error_code| is a null pointer OnSuccess() will be invoked when the
  // Start() method is called, otherwise OnFailure() will be invoked with the
  // respective |error_code|.
  MockUploadJob(const GURL& upload_url,
                UploadJob::Delegate* delegate,
                std::unique_ptr<UploadJob::ErrorCode> error_code);
  ~MockUploadJob() override;

  // UploadJob:
  void AddDataSegment(const std::string& name,
                      const std::string& filename,
                      const std::map<std::string, std::string>& header_entries,
                      std::unique_ptr<std::string> data) override;
  void Start() override;

  const GURL& GetUploadUrl() const;

 protected:
  const GURL upload_url_;
  raw_ptr<UploadJob::Delegate> delegate_;
  std::unique_ptr<UploadJob::ErrorCode> error_code_;
  bool add_datasegment_succeeds_;
};

MockUploadJob::MockUploadJob(const GURL& upload_url,
                             UploadJob::Delegate* delegate,
                             std::unique_ptr<UploadJob::ErrorCode> error_code)
    : upload_url_(upload_url),
      delegate_(delegate),
      error_code_(std::move(error_code)) {}

MockUploadJob::~MockUploadJob() = default;

void MockUploadJob::AddDataSegment(
    const std::string& name,
    const std::string& filename,
    const std::map<std::string, std::string>& header_entries,
    std::unique_ptr<std::string> data) {}

void MockUploadJob::Start() {
  DCHECK(delegate_);
  EXPECT_EQ(kMockUploadUrl, upload_url_.spec());
  if (error_code_) {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&UploadJob::Delegate::OnFailure,
                                  base::Unretained(delegate_), *error_code_));
    return;
  }
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&UploadJob::Delegate::OnSuccess,
                                base::Unretained(delegate_)));
}

scoped_refptr<base::RefCountedBytes> GenerateTestPNG(const int& width,
                                                     const int& height) {
  const SkColor background_color = SK_ColorBLUE;
  SkBitmap bmp;
  bmp.allocN32Pixels(width, height);
  for (int y = 0; y < height; ++y) {
    for (int x = 0; x < width; ++x) {
      *bmp.getAddr32(x, y) = background_color;
    }
  }
  scoped_refptr<base::RefCountedBytes> png_bytes(new base::RefCountedBytes());
  gfx::PNGCodec::ColorFormat color_format = gfx::PNGCodec::FORMAT_RGBA;
  if (!gfx::PNGCodec::Encode(
          reinterpret_cast<const unsigned char*>(bmp.getPixels()), color_format,
          gfx::Size(bmp.width(), bmp.height()),
          static_cast<int>(bmp.rowBytes()), false,
          std::vector<gfx::PNGCodec::Comment>(), &png_bytes->as_vector())) {
    LOG(ERROR) << "Failed to encode image";
  }
  return png_bytes;
}

class FakeScreenshotDelegate : public DeviceCommandScreenshotJob::Delegate {
 public:
  FakeScreenshotDelegate(
      std::unique_ptr<UploadJob::ErrorCode> upload_job_error_code,
      bool screenshot_allowed);
  ~FakeScreenshotDelegate() override;

  bool IsScreenshotAllowed() override;
  void TakeSnapshot(gfx::NativeWindow window,
                    const gfx::Rect& source_rect,
                    OnScreenshotTakenCallback callback) override;
  std::unique_ptr<UploadJob> CreateUploadJob(const GURL&,
                                             UploadJob::Delegate*) override;

 private:
  std::unique_ptr<UploadJob::ErrorCode> upload_job_error_code_;
  bool screenshot_allowed_;
};

FakeScreenshotDelegate::FakeScreenshotDelegate(
    std::unique_ptr<UploadJob::ErrorCode> upload_job_error_code,
    bool screenshot_allowed)
    : upload_job_error_code_(std::move(upload_job_error_code)),
      screenshot_allowed_(screenshot_allowed) {}

FakeScreenshotDelegate::~FakeScreenshotDelegate() = default;

bool FakeScreenshotDelegate::IsScreenshotAllowed() {
  return screenshot_allowed_;
}

void FakeScreenshotDelegate::TakeSnapshot(gfx::NativeWindow window,
                                          const gfx::Rect& source_rect,
                                          OnScreenshotTakenCallback callback) {
  EXPECT_TRUE(screenshot_allowed_)
      << "Should not take a screenshot unless it is allowed";

  const int width = source_rect.width();
  const int height = source_rect.height();
  scoped_refptr<base::RefCountedBytes> test_png =
      GenerateTestPNG(width, height);
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), test_png));
}

std::unique_ptr<UploadJob> FakeScreenshotDelegate::CreateUploadJob(
    const GURL& upload_url,
    UploadJob::Delegate* delegate) {
  return std::make_unique<MockUploadJob>(upload_url, delegate,
                                         std::move(upload_job_error_code_));
}

}  // namespace

class DeviceCommandScreenshotTest : public ChromeAshTestBase {
 public:
  DeviceCommandScreenshotTest(const DeviceCommandScreenshotTest&) = delete;
  DeviceCommandScreenshotTest& operator=(const DeviceCommandScreenshotTest&) =
      delete;

  void VerifyResults(const RemoteCommandJob& job,
                     RemoteCommandJob::Status expected_status,
                     std::string expected_payload);

 protected:
  DeviceCommandScreenshotTest();

  // ChromeAshTestBase:
  void SetUp() override;

  void InitializeScreenshotJob(RemoteCommandJob* job,
                               RemoteCommandJob::UniqueIDType unique_id,
                               base::TimeTicks issued_time,
                               const std::string& upload_url);

  std::string CreatePayloadFromResultCode(
      DeviceCommandScreenshotJob::ResultCode result_code);

  base::TimeTicks test_start_time_;

 private:
  scoped_refptr<base::TestMockTimeTaskRunner> task_runner_;
};

DeviceCommandScreenshotTest::DeviceCommandScreenshotTest()
    : task_runner_(new base::TestMockTimeTaskRunner()) {}

void DeviceCommandScreenshotTest::SetUp() {
  ChromeAshTestBase::SetUp();
  test_start_time_ = base::TimeTicks::Now();
}

void DeviceCommandScreenshotTest::InitializeScreenshotJob(
    RemoteCommandJob* job,
    RemoteCommandJob::UniqueIDType unique_id,
    base::TimeTicks issued_time,
    const std::string& upload_url) {
  EXPECT_TRUE(job->Init(
      base::TimeTicks::Now(),
      GenerateScreenshotCommandProto(
          unique_id, base::TimeTicks::Now() - issued_time, upload_url),
      em::SignedData()));
  EXPECT_EQ(unique_id, job->unique_id());
  EXPECT_EQ(RemoteCommandJob::NOT_STARTED, job->status());
}

std::string DeviceCommandScreenshotTest::CreatePayloadFromResultCode(
    DeviceCommandScreenshotJob::ResultCode result_code) {
  std::string payload;
  base::Value::Dict root_dict;
  if (result_code != DeviceCommandScreenshotJob::SUCCESS) {
    root_dict.Set(kResultFieldName, result_code);
  }
  base::JSONWriter::Write(root_dict, &payload);
  return payload;
}

void DeviceCommandScreenshotTest::VerifyResults(
    const RemoteCommandJob& job,
    RemoteCommandJob::Status expected_status,
    std::string expected_payload) {
  EXPECT_EQ(expected_status, job.status());
  if (job.status() == RemoteCommandJob::SUCCEEDED) {
    std::unique_ptr<std::string> payload = job.GetResultPayload();
    EXPECT_TRUE(payload);
    EXPECT_EQ(expected_payload, *payload);
  }
}

TEST_F(DeviceCommandScreenshotTest, Success) {
  auto job = std::make_unique<DeviceCommandScreenshotJob>(
      std::make_unique<FakeScreenshotDelegate>(nullptr, true));
  InitializeScreenshotJob(job.get(), kUniqueID, test_start_time_,
                          kMockUploadUrl);
  base::test::TestFuture<void> job_finished_future;
  bool success = job->Run(base::Time::Now(), base::TimeTicks::Now(),
                          job_finished_future.GetCallback());
  EXPECT_TRUE(success);
  ASSERT_TRUE(job_finished_future.Wait()) << "Job did not finish.";
  VerifyResults(
      *job, RemoteCommandJob::SUCCEEDED,
      CreatePayloadFromResultCode(DeviceCommandScreenshotJob::SUCCESS));
}

TEST_F(DeviceCommandScreenshotTest, FailureUserInput) {
  auto job = std::make_unique<DeviceCommandScreenshotJob>(
      std::make_unique<FakeScreenshotDelegate>(nullptr, false));
  InitializeScreenshotJob(job.get(), kUniqueID, test_start_time_,
                          kMockUploadUrl);
  base::test::TestFuture<void> job_finished_future;
  bool success = job->Run(base::Time::Now(), base::TimeTicks::Now(),
                          job_finished_future.GetCallback());
  EXPECT_TRUE(success);
  ASSERT_TRUE(job_finished_future.Wait()) << "Job did not finish.";
  VerifyResults(*job, RemoteCommandJob::FAILED,
                CreatePayloadFromResultCode(
                    DeviceCommandScreenshotJob::FAILURE_USER_INPUT));
}

TEST_F(DeviceCommandScreenshotTest, Failure) {
  using ErrorCode = UploadJob::ErrorCode;
  auto job = std::make_unique<DeviceCommandScreenshotJob>(
      std::make_unique<FakeScreenshotDelegate>(
          std::make_unique<ErrorCode>(UploadJob::AUTHENTICATION_ERROR), true));
  InitializeScreenshotJob(job.get(), kUniqueID, test_start_time_,
                          kMockUploadUrl);
  base::test::TestFuture<void> job_finished_future;
  bool success = job->Run(base::Time::Now(), base::TimeTicks::Now(),
                          job_finished_future.GetCallback());
  EXPECT_TRUE(success);
  ASSERT_TRUE(job_finished_future.Wait()) << "Job did not finish.";
  VerifyResults(*job, RemoteCommandJob::FAILED,
                CreatePayloadFromResultCode(
                    DeviceCommandScreenshotJob::FAILURE_AUTHENTICATION));
}

}  // namespace policy