// Copyright 2018 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/usb/usb_policy_allowed_devices.h"

#include <memory>
#include <string>
#include <utility>

#include "base/test/values_test_util.h"
#include "chrome/test/base/testing_profile.h"
#include "components/content_settings/core/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_task_environment.h"
#include "services/device/public/cpp/test/fake_usb_device_manager.h"
#include "services/device/public/mojom/usb_device.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace {

using ::base::test::ParseJson;
using ::testing::UnorderedElementsAre;

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

  void SetWebUsbAllowDevicesForUrlsPrefValue(const base::Value& value) {
    profile_.GetPrefs()->Set(prefs::kManagedWebUsbAllowDevicesForUrls, value);
  }

 protected:
  Profile* profile() { return &profile_; }

  std::unique_ptr<UsbPolicyAllowedDevices> CreateUsbPolicyAllowedDevices() {
    return std::make_unique<UsbPolicyAllowedDevices>(profile()->GetPrefs());
  }

  device::FakeUsbDeviceManager device_manager_;

  const url::Origin kGoogleOrigin =
      url::Origin::Create(GURL("https://google.com"));
  const url::Origin kCrbugOrigin =
      url::Origin::Create(GURL("https://crbug.com"));
  const url::Origin kYoutubeOrigin =
      url::Origin::Create(GURL("https://www.youtube.com"));

 private:
  content::BrowserTaskEnvironment task_environment_;
  TestingProfile profile_;
};

}  // namespace

TEST_F(UsbPolicyAllowedDevicesTest, InitializeWithMissingPrefValue) {
  auto usb_policy_allowed_devices = CreateUsbPolicyAllowedDevices();

  EXPECT_TRUE(usb_policy_allowed_devices->map().empty());
}

TEST_F(UsbPolicyAllowedDevicesTest, InitializeWithExistingEmptyPrefValue) {
  base::Value pref_value(base::Value::Type::LIST);

  SetWebUsbAllowDevicesForUrlsPrefValue(pref_value);

  auto usb_policy_allowed_devices = CreateUsbPolicyAllowedDevices();

  EXPECT_TRUE(usb_policy_allowed_devices->map().empty());
}

namespace {

constexpr char kPolicySetting[] = R"(
    [
      {
        "devices": [
          { "vendor_id": 1234, "product_id": 5678 },
          { "vendor_id": 4321 }
        ],
        "urls": [
          "https://google.com,https://google.com",
          "https://crbug.com"
        ]
      }, {
        "devices": [{}],
        "urls": ["https://www.youtube.com"]
      }
    ])";

}  // namespace

TEST_F(UsbPolicyAllowedDevicesTest, InitializeWithExistingPrefValue) {
  SetWebUsbAllowDevicesForUrlsPrefValue(ParseJson(kPolicySetting));

  auto usb_policy_allowed_devices = CreateUsbPolicyAllowedDevices();

  const auto& map = usb_policy_allowed_devices->map();
  EXPECT_EQ(map.size(), 3ul);

  auto device_key = std::make_pair(1234, 5678);
  ASSERT_TRUE(map.contains(device_key));

  const auto& first = map.at(device_key);
  EXPECT_THAT(first, UnorderedElementsAre(kGoogleOrigin, kCrbugOrigin));

  device_key = std::make_pair(4321, -1);
  ASSERT_TRUE(map.contains(device_key));

  const auto& second = map.at(device_key);
  EXPECT_THAT(second, UnorderedElementsAre(kGoogleOrigin, kCrbugOrigin));

  device_key = std::make_pair(-1, -1);
  ASSERT_TRUE(map.contains(device_key));

  const auto& third = map.at(device_key);
  EXPECT_THAT(third, UnorderedElementsAre(kYoutubeOrigin));
}

TEST_F(UsbPolicyAllowedDevicesTest,
       InitializeWithMissingPolicyThenUpdatePolicy) {
  auto usb_policy_allowed_devices = CreateUsbPolicyAllowedDevices();
  EXPECT_TRUE(usb_policy_allowed_devices->map().empty());

  // Ensure that the allowed devices can be dynamically updated.
  SetWebUsbAllowDevicesForUrlsPrefValue(ParseJson(kPolicySetting));

  const auto& map = usb_policy_allowed_devices->map();
  EXPECT_EQ(map.size(), 3ul);

  auto device_key = std::make_pair(1234, 5678);
  ASSERT_TRUE(map.contains(device_key));
  EXPECT_THAT(map.at(device_key),
              UnorderedElementsAre(kGoogleOrigin, kCrbugOrigin));

  device_key = std::make_pair(4321, -1);
  ASSERT_TRUE(map.contains(device_key));
  EXPECT_THAT(map.at(device_key),
              UnorderedElementsAre(kGoogleOrigin, kCrbugOrigin));

  device_key = std::make_pair(-1, -1);
  ASSERT_TRUE(map.contains(device_key));
  EXPECT_THAT(map.at(device_key), UnorderedElementsAre(kYoutubeOrigin));
}

TEST_F(UsbPolicyAllowedDevicesTest,
       InitializeWithExistingPolicyThenRemovePolicy) {
  SetWebUsbAllowDevicesForUrlsPrefValue(ParseJson(kPolicySetting));

  auto usb_policy_allowed_devices = CreateUsbPolicyAllowedDevices();

  const auto& map = usb_policy_allowed_devices->map();
  EXPECT_EQ(map.size(), 3ul);

  auto device_key = std::make_pair(1234, 5678);
  ASSERT_TRUE(map.contains(device_key));

  const auto& first = map.at(device_key);
  EXPECT_THAT(first, UnorderedElementsAre(kGoogleOrigin, kCrbugOrigin));

  device_key = std::make_pair(4321, -1);
  ASSERT_TRUE(map.contains(device_key));

  const auto& second = map.at(device_key);
  EXPECT_THAT(second, UnorderedElementsAre(kGoogleOrigin, kCrbugOrigin));

  device_key = std::make_pair(-1, -1);
  ASSERT_TRUE(map.contains(device_key));

  const auto& third = map.at(device_key);
  EXPECT_THAT(third, UnorderedElementsAre(kYoutubeOrigin));

  // Ensure that the allowed devices can be removed dynamically.
  SetWebUsbAllowDevicesForUrlsPrefValue(base::Value(base::Value::Type::LIST));

  EXPECT_TRUE(usb_policy_allowed_devices->map().empty());
}

namespace {

constexpr char kPolicySettingWithEntriesContainingDuplicateDevices[] = R"(
    [
      {
        "devices": [{ "vendor_id": 1234, "product_id": 5678 }],
        "urls": [
          "https://google.com",
          "https://crbug.com"
        ]
      }, {
        "devices": [{ "vendor_id": 1234, "product_id": 5678 }],
        "urls": ["https://www.youtube.com"]
      }
    ])";

}  // namespace

TEST_F(UsbPolicyAllowedDevicesTest,
       InitializeWithExistingPrefValueContainingDuplicateDevices) {
  SetWebUsbAllowDevicesForUrlsPrefValue(
      ParseJson(kPolicySettingWithEntriesContainingDuplicateDevices));

  auto usb_policy_allowed_devices = CreateUsbPolicyAllowedDevices();

  const auto& map = usb_policy_allowed_devices->map();
  ASSERT_EQ(map.size(), 1ul);

  auto device_key = std::make_pair(1234, 5678);
  ASSERT_TRUE(map.contains(device_key));

  // Ensure a device has all of the URL patterns allowed to access it.
  const auto& policy = map.at(device_key);
  EXPECT_THAT(policy, UnorderedElementsAre(kGoogleOrigin, kCrbugOrigin,
                                           kYoutubeOrigin));
}

namespace {

constexpr char kPolicySettingWithEntriesMatchingMultipleDevices[] = R"(
    [
      {
        "devices": [{ "vendor_id": 1234, "product_id": 5678 }],
        "urls": ["https://google.com"]
      }, {
        "devices": [{ "vendor_id": 1234 }],
        "urls": ["https://www.youtube.com"]
      }, {
        "devices": [{}],
        "urls": ["https://chromium.org"]
      }
    ])";

}  // namespace

TEST_F(UsbPolicyAllowedDevicesTest, IsDeviceAllowed) {
  SetWebUsbAllowDevicesForUrlsPrefValue(
      ParseJson(kPolicySettingWithEntriesMatchingMultipleDevices));

  auto usb_policy_allowed_devices = CreateUsbPolicyAllowedDevices();

  const auto kGoogleOrigin = url::Origin::Create(GURL("https://google.com"));
  const auto kYoutubeOrigin =
      url::Origin::Create(GURL("https://www.youtube.com"));
  const auto kChromiumOrigin =
      url::Origin::Create(GURL("https://chromium.org"));

  auto specific_device_info = device_manager_.CreateAndAddDevice(
      1234, 5678, "Google", "Gizmo", "123ABC");
  auto vendor_device_info = device_manager_.CreateAndAddDevice(
      1234, 8765, "Google", "Gizmo", "ABC123");
  auto unrelated_device_info = device_manager_.CreateAndAddDevice(
      4321, 8765, "Chrome", "Gizmo", "987ZYX");

  // Check that the specific device is allowed for https://google.com but not
  // any other device.
  EXPECT_TRUE(usb_policy_allowed_devices->IsDeviceAllowed(
      kGoogleOrigin, *specific_device_info));
  EXPECT_FALSE(usb_policy_allowed_devices->IsDeviceAllowed(
      kGoogleOrigin, *vendor_device_info));
  EXPECT_FALSE(usb_policy_allowed_devices->IsDeviceAllowed(
      kGoogleOrigin, *unrelated_device_info));

  // Check that devices with a vendor ID of 1234 are allowed for
  // https://www.youtube.com, but not an unrelated device.
  EXPECT_TRUE(usb_policy_allowed_devices->IsDeviceAllowed(
      kYoutubeOrigin, *specific_device_info));
  EXPECT_TRUE(usb_policy_allowed_devices->IsDeviceAllowed(kYoutubeOrigin,
                                                          *vendor_device_info));
  EXPECT_FALSE(usb_policy_allowed_devices->IsDeviceAllowed(
      kYoutubeOrigin, *unrelated_device_info));

  // Check that any device is allowed for https://chromium.org.
  EXPECT_TRUE(usb_policy_allowed_devices->IsDeviceAllowed(
      kChromiumOrigin, *specific_device_info));
  EXPECT_TRUE(usb_policy_allowed_devices->IsDeviceAllowed(kChromiumOrigin,
                                                          *vendor_device_info));
  EXPECT_TRUE(usb_policy_allowed_devices->IsDeviceAllowed(
      kChromiumOrigin, *unrelated_device_info));
}

TEST_F(UsbPolicyAllowedDevicesTest, IsDeviceAllowedForUrlsNotInPref) {
  SetWebUsbAllowDevicesForUrlsPrefValue(
      ParseJson(kPolicySettingWithEntriesMatchingMultipleDevices));

  auto usb_policy_allowed_devices = CreateUsbPolicyAllowedDevices();

  const url::Origin origins[] = {
      url::Origin::Create(GURL("https://evil.com")),
      url::Origin::Create(GURL("https://very.evil.com")),
      url::Origin::Create(GURL("https://chromium.deceptive.org"))};

  auto device_info = device_manager_.CreateAndAddDevice(1234, 5678, "Google",
                                                        "Gizmo", "123ABC");
  for (const url::Origin& origin : origins) {
    EXPECT_FALSE(
        usb_policy_allowed_devices->IsDeviceAllowed(origin, *device_info));
  }
}

TEST_F(UsbPolicyAllowedDevicesTest, IsDeviceAllowedForDeviceNotInPref) {
  SetWebUsbAllowDevicesForUrlsPrefValue(
      ParseJson(kPolicySettingWithEntriesMatchingMultipleDevices));

  auto usb_policy_allowed_devices = CreateUsbPolicyAllowedDevices();

  const url::Origin origins[] = {
      url::Origin::Create(GURL("https://google.com")),
      url::Origin::Create(GURL("https://www.youtube.com"))};

  auto device_info = device_manager_.CreateAndAddDevice(4321, 8765, "Google",
                                                        "Gizmo", "123ABC");
  for (const url::Origin& origin : origins) {
    EXPECT_FALSE(
        usb_policy_allowed_devices->IsDeviceAllowed(origin, *device_info));
  }
}

namespace {

constexpr char kPolicySettingWithUrlContainingEmbeddingOrigin[] = R"(
    [
      {
        "devices": [{ "vendor_id": 1234, "product_id": 5678 }],
        "urls": [
          "https://requesting.com,https://embedding.com"
        ]
      }
    ])";

}  // namespace

TEST_F(UsbPolicyAllowedDevicesTest,
       IsDeviceAllowedForUrlContainingEmbeddingOrigin) {
  SetWebUsbAllowDevicesForUrlsPrefValue(
      ParseJson(kPolicySettingWithUrlContainingEmbeddingOrigin));

  auto usb_policy_allowed_devices = CreateUsbPolicyAllowedDevices();

  const auto requesting_origin =
      url::Origin::Create(GURL("https://requesting.com"));
  const auto embedding_origin =
      url::Origin::Create(GURL("https://embedding.com"));

  auto device_info = device_manager_.CreateAndAddDevice(1234, 5678, "Google",
                                                        "Gizmo", "123ABC");
  EXPECT_TRUE(usb_policy_allowed_devices->IsDeviceAllowed(embedding_origin,
                                                          *device_info));
  EXPECT_FALSE(usb_policy_allowed_devices->IsDeviceAllowed(requesting_origin,
                                                           *device_info));
}
