// 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/usb/chrome_usb_delegate.h"

#include <optional>
#include <string_view>

#include "base/barrier_closure.h"
#include "base/strings/stringprintf.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/test_future.h"
#include "base/uuid.h"
#include "build/build_config.h"
#include "chrome/browser/usb/usb_chooser_context.h"
#include "chrome/browser/usb/usb_chooser_context_factory.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/embedded_worker_instance_test_harness.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "services/device/public/cpp/test/fake_usb_device_info.h"
#include "services/device/public/cpp/test/fake_usb_device_manager.h"
#include "services/device/public/cpp/test/scoped_usb_device_manager_overrider.h"
#include "services/device/public/mojom/usb_device.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/usb/web_usb_service.mojom.h"
#include "url/gurl.h"

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/usb/usb_connection_tracker.h"
#include "chrome/browser/usb/usb_connection_tracker_factory.h"
#endif  // !BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
#include "base/command_line.h"
#include "base/values.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)

namespace {

using ::base::test::RunClosure;
using ::base::test::RunOnceCallback;
using ::base::test::RunOnceClosure;
using ::base::test::TestFuture;
using ::testing::_;
using ::testing::NiceMock;

constexpr std::string_view kDefaultTestUrl{"https://www.google.com/"};
constexpr std::string_view kCrossOriginTestUrl{"https://www.chromium.org"};

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
constexpr std::string_view kExtensionId{"ckcendljdlmgnhghiaomidhiiclmapok"};
constexpr char kAllowlistedImprivataExtensionId[] =
    "dhodapiemamlmhlhblgcibabhdkohlen";
constexpr char kAllowlistedSmartCardExtensionId[] =
    "khpfeaanjngmcnplbdlpegiifgpfgdco";
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)

ACTION_P2(ExpectGuidAndThen, expected_guid, callback) {
  ASSERT_TRUE(arg0);
  EXPECT_EQ(expected_guid, arg0->guid);
  if (!callback.is_null())
    callback.Run();
}

// Calls GetDevices on `service` and blocks until the GetDevicesCallback is
// invoked. Fails the test if the guids of received UsbDeviceInfos do not
// exactly match `expected_guids`.
void GetDevicesBlocking(blink::mojom::WebUsbService* service,
                        const std::set<std::string>& expected_guids) {
  TestFuture<std::vector<device::mojom::UsbDeviceInfoPtr>> get_devices_future;
  service->GetDevices(get_devices_future.GetCallback());
  EXPECT_EQ(expected_guids.size(), get_devices_future.Get().size());
  std::set<std::string> actual_guids;
  for (const auto& device : get_devices_future.Get())
    actual_guids.insert(device->guid);
  EXPECT_EQ(expected_guids, actual_guids);
}

device::mojom::UsbOpenDeviceResultPtr NewUsbOpenDeviceSuccess() {
  return device::mojom::UsbOpenDeviceResult::NewSuccess(
      device::mojom::UsbOpenDeviceSuccess::OK);
}

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
// Creates a FakeUsbDeviceInfo with USB class code.
scoped_refptr<device::FakeUsbDeviceInfo> CreateFakeUsbDeviceInfo() {
  auto alternate_setting = device::mojom::UsbAlternateInterfaceInfo::New();
  alternate_setting->alternate_setting = 0;
  alternate_setting->class_code = device::mojom::kUsbHidClass;

  auto interface = device::mojom::UsbInterfaceInfo::New();
  interface->interface_number = 0;
  interface->alternates.push_back(std::move(alternate_setting));

  auto config = device::mojom::UsbConfigurationInfo::New();
  config->configuration_value = 1;
  config->interfaces.push_back(std::move(interface));

  std::vector<device::mojom::UsbConfigurationInfoPtr> configs;
  configs.push_back(std::move(config));

  return base::MakeRefCounted<device::FakeUsbDeviceInfo>(
      0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF", std::move(configs));
}

// Creates a FakeUsbDeviceInfo with Smart Card class code.
scoped_refptr<device::FakeUsbDeviceInfo> CreateFakeSmartCardDeviceInfo() {
  auto alternate_setting = device::mojom::UsbAlternateInterfaceInfo::New();
  alternate_setting->alternate_setting = 0;
  alternate_setting->class_code = device::mojom::kUsbSmartCardClass;

  auto interface = device::mojom::UsbInterfaceInfo::New();
  interface->interface_number = 0;
  interface->alternates.push_back(std::move(alternate_setting));

  auto config = device::mojom::UsbConfigurationInfo::New();
  config->configuration_value = 1;
  config->interfaces.push_back(std::move(interface));

  std::vector<device::mojom::UsbConfigurationInfoPtr> configs;
  configs.push_back(std::move(config));

  return base::MakeRefCounted<device::FakeUsbDeviceInfo>(
      0x4321, 0x8765, "ACME", "Frobinator", "ABCDEF", std::move(configs));
}
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)

// A mock UsbDeviceManagerClient implementation that can be used to listen for
// USB device connection events.
class MockDeviceManagerClient
    : public UsbChooserContext::UsbDeviceManagerClient {
 public:
  MockDeviceManagerClient() = default;
  ~MockDeviceManagerClient() override = default;

  mojo::PendingAssociatedRemote<UsbDeviceManagerClient>
  CreateInterfacePtrAndBind() {
    auto client = receiver_.BindNewEndpointAndPassRemote();
    receiver_.set_disconnect_handler(base::BindOnce(
        &MockDeviceManagerClient::OnConnectionError, base::Unretained(this)));
    return client;
  }

  MOCK_METHOD1(OnDeviceAdded, void(device::mojom::UsbDeviceInfoPtr));
  MOCK_METHOD1(OnDeviceRemoved, void(device::mojom::UsbDeviceInfoPtr));

  MOCK_METHOD0(ConnectionError, void());
  void OnConnectionError() {
    receiver_.reset();
    ConnectionError();
  }

 private:
  mojo::AssociatedReceiver<UsbDeviceManagerClient> receiver_{this};
};

// Android does not use UsbConnectionTracker.
#if !BUILDFLAG(IS_ANDROID)
class MockUsbConnectionTracker : public UsbConnectionTracker {
 public:
  explicit MockUsbConnectionTracker(Profile* profile)
      : UsbConnectionTracker(profile) {}
  ~MockUsbConnectionTracker() override = default;

  MOCK_METHOD(void, IncrementConnectionCount, (const url::Origin&), (override));
  MOCK_METHOD(void, DecrementConnectionCount, (const url::Origin&), (override));
};
#endif  // !BUILDFLAG(IS_ANDROID)

// Tests for embedder-specific behaviors of Chrome's blink::mojom::WebUsbService
// implementation.
class ChromeUsbTestHelper {
 public:
#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
  // Creates a fake extension with the specified `extension_id` so that it can
  // exercise behaviors that are only enabled for privileged extensions.
  std::optional<GURL> CreateExtensionWithId(std::string_view extension_id) {
    auto manifest = base::DictValue()
                        .Set("name", "Fake extension")
                        .Set("description", "For testing.")
                        .Set("version", "0.1")
                        .Set("manifest_version", 2)
                        .Set("web_accessible_resources",
                             base::ListValue().Append("index.html"));
    scoped_refptr<const extensions::Extension> extension =
        extensions::ExtensionBuilder()
            .SetManifest(std::move(manifest))
            .SetID(std::string(extension_id))
            .Build();
    if (!extension) {
      return std::nullopt;
    }
    extensions::TestExtensionSystem* extension_system =
        static_cast<extensions::TestExtensionSystem*>(
            extensions::ExtensionSystem::Get(profile_));
    extension_system->CreateExtensionService(
        base::CommandLine::ForCurrentProcess(), base::FilePath(), false);
    extensions::ExtensionRegistrar::Get(profile_)->AddExtension(extension);
    return extension->GetResourceURL("index.html");
  }
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)

  void SimulateDeviceServiceCrash() { device_manager()->CloseAllBindings(); }

  virtual void ConnectToService(
      mojo::PendingReceiver<blink::mojom::WebUsbService> receiver) = 0;

  virtual void SetUpOriginUrl() = 0;

  void SetUpWebPageOriginUrl() { origin_url_ = GURL(kDefaultTestUrl); }

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
  void SetUpExtensionOriginUrl(std::string_view extension_id) {
    auto extension_url = CreateExtensionWithId(extension_id);
    ASSERT_TRUE(extension_url);
    origin_url_ = *extension_url;
  }
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)

  void SetUpFakeDeviceManager() {
    if (!device_manager()->IsBound()) {
      mojo::PendingRemote<device::mojom::UsbDeviceManager>
          pending_device_manager;
      device_manager()->AddReceiver(
          pending_device_manager.InitWithNewPipeAndPassReceiver());
      GetChooserContext()->SetDeviceManagerForTesting(
          std::move(pending_device_manager));
    }
  }

  UsbChooserContext* GetChooserContext() {
    return UsbChooserContextFactory::GetForProfile(profile_);
  }

  device::FakeUsbDeviceManager* device_manager() {
    return usb_device_manager_overrider_.device_manager();
  }

#if !BUILDFLAG(IS_ANDROID)
  BrowserContextKeyedServiceFactory::TestingFactory
  GetUsbConnectionTrackerTestingFactory() {
    return base::BindRepeating([](content::BrowserContext* browser_context) {
      return static_cast<std::unique_ptr<KeyedService>>(
          std::make_unique<testing::NiceMock<MockUsbConnectionTracker>>(
              Profile::FromBrowserContext(browser_context)));
    });
  }

  void SetUpUsbConnectionTracker() {
    // Even MockUsbConnectionTracker can be lazily created in ChromeUsbDelegate,
    // we intentionally create it ahead of time so that we can test EXPECT_CALL
    // for invoking mock method for the first time.
    usb_connection_tracker_ = static_cast<MockUsbConnectionTracker*>(
        UsbConnectionTrackerFactory::GetForProfile(profile_, /*create=*/true));
  }

  MockUsbConnectionTracker& usb_connection_tracker() {
    return *usb_connection_tracker_;
  }
#endif  // !BUILDFLAG(IS_ANDROID)

  void ExpectIncrementConnectionCount(const url::Origin& origin) {
#if !BUILDFLAG(IS_ANDROID)
    if (supports_usb_connection_tracker_) {
      EXPECT_CALL(usb_connection_tracker(), IncrementConnectionCount(origin));
    }
#endif
  }

  void ExpectDecrementConnectionCount(const url::Origin& origin) {
#if !BUILDFLAG(IS_ANDROID)
    if (supports_usb_connection_tracker_) {
      EXPECT_CALL(usb_connection_tracker(), DecrementConnectionCount(origin));
    }
#endif
  }

  void TestNoPermissionDevice() {
    auto origin = url::Origin::Create(origin_url_);
    auto device1 = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
        0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
    auto device2 = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
        0x1234, 0x5679, "ACME", "Frobinator+", "GHIJKL");
    auto no_permission_device1 =
        base::MakeRefCounted<device::FakeUsbDeviceInfo>(
            0xffff, 0x567b, "ACME", "Frobinator II", "MNOPQR");
    auto no_permission_device2 =
        base::MakeRefCounted<device::FakeUsbDeviceInfo>(
            0xffff, 0x567c, "ACME", "Frobinator Xtreme", "STUVWX");

    // Connect two devices and grant permission for one.
    auto device_info_1 = device_manager()->AddDevice(device1);
    GetChooserContext()->GrantDevicePermission(origin, *device_info_1);
    device_manager()->AddDevice(no_permission_device1);

    // Create the WebUsbService and register a `mock_client` to receive
    // notifications on device connections and disconnections. GetDevices is
    // called to ensure the service is started and the client is set.
    mojo::Remote<blink::mojom::WebUsbService> web_usb_service;
    ConnectToService(web_usb_service.BindNewPipeAndPassReceiver());
    NiceMock<MockDeviceManagerClient> mock_client;
    web_usb_service->SetClient(mock_client.CreateInterfacePtrAndBind());
    GetDevicesBlocking(web_usb_service.get(), {device1->guid()});

    // Connect two more devices and grant permission for one.
    auto device_info_2 = device_manager()->AddDevice(device2);
    GetChooserContext()->GrantDevicePermission(origin, *device_info_2);
    device_manager()->AddDevice(no_permission_device2);

    // Disconnect all four devices. The `mock_client` should be notified only
    // for the devices it has permission to access.
    device_manager()->RemoveDevice(device1);
    device_manager()->RemoveDevice(device2);
    device_manager()->RemoveDevice(no_permission_device1);
    device_manager()->RemoveDevice(no_permission_device2);
    {
      base::RunLoop loop;
      base::RepeatingClosure barrier =
          base::BarrierClosure(2, loop.QuitClosure());
      testing::InSequence s;

      EXPECT_CALL(mock_client, OnDeviceRemoved)
          .WillOnce(ExpectGuidAndThen(device1->guid(), barrier))
          .WillOnce(ExpectGuidAndThen(device2->guid(), barrier));
      loop.Run();
    }

    // Reconnect all four devices. The `mock_client` should be notified only for
    // the devices it has permission to access.
    device_manager()->AddDevice(device1);
    device_manager()->AddDevice(device2);
    device_manager()->AddDevice(no_permission_device1);
    device_manager()->AddDevice(no_permission_device2);
    {
      base::RunLoop loop;
      base::RepeatingClosure barrier =
          base::BarrierClosure(2, loop.QuitClosure());
      testing::InSequence s;

      EXPECT_CALL(mock_client, OnDeviceAdded)
          .WillOnce(ExpectGuidAndThen(device1->guid(), barrier))
          .WillOnce(ExpectGuidAndThen(device2->guid(), barrier));
      loop.Run();
    }
  }

  void TestReconnectDeviceManager() {
    auto origin = url::Origin::Create(origin_url_);
    auto* context = GetChooserContext();
    auto device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
        0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
    auto ephemeral_device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
        0, 0, "ACME", "Frobinator II", "");

    // Connect two devices and grant permission for both. The first device is
    // eligible for persistent permissions and the second device is only
    // eligible for ephemeral permissions.
    auto device_info = device_manager()->AddDevice(device);
    auto ephemeral_device_info = device_manager()->AddDevice(ephemeral_device);
    mojo::Remote<blink::mojom::WebUsbService> web_usb_service;
    ConnectToService(web_usb_service.BindNewPipeAndPassReceiver());
    MockDeviceManagerClient mock_client;
    web_usb_service->SetClient(mock_client.CreateInterfacePtrAndBind());
    // GetDevices is called to ensure the service is started, the client is set,
    // and the callback from adding device to the device manager is settled.
    GetDevicesBlocking(web_usb_service.get(), {});

    context->GrantDevicePermission(origin, *ephemeral_device_info);
    context->GrantDevicePermission(origin, *device_info);
    GetDevicesBlocking(web_usb_service.get(),
                       {device->guid(), ephemeral_device->guid()});

    // Simulate a device service crash. The ephemeral permission should be
    // revoked.
    SimulateDeviceServiceCrash();
    base::RunLoop loop;
    EXPECT_CALL(mock_client, ConnectionError()).WillOnce([&]() {
      loop.Quit();
    });
    loop.Run();
    EXPECT_TRUE(context->HasDevicePermission(origin, *device_info));
    EXPECT_FALSE(context->HasDevicePermission(origin, *ephemeral_device_info));

    // Although a new device added, as the Device manager has been destroyed, no
    // event will be triggered.
    auto another_device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
        0x1234, 0x5679, "ACME", "Frobinator+", "GHIJKL");
    auto another_device_info = device_manager()->AddDevice(another_device);

    EXPECT_CALL(mock_client, OnDeviceAdded).Times(0);
    base::RunLoop().RunUntilIdle();

    // Grant permission to the new device when service is off.
    context->GrantDevicePermission(origin, *another_device_info);

    device_manager()->RemoveDevice(device);
    EXPECT_CALL(mock_client, OnDeviceRemoved).Times(0);
    base::RunLoop().RunUntilIdle();

    // Reconnect the service.
    web_usb_service.reset();
    base::RunLoop().RunUntilIdle();
    ConnectToService(web_usb_service.BindNewPipeAndPassReceiver());
    web_usb_service->SetClient(mock_client.CreateInterfacePtrAndBind());

    GetDevicesBlocking(web_usb_service.get(), {another_device->guid()});

    EXPECT_TRUE(context->HasDevicePermission(origin, *device_info));
    EXPECT_TRUE(context->HasDevicePermission(origin, *another_device_info));
    EXPECT_FALSE(context->HasDevicePermission(origin, *ephemeral_device_info));
  }

  void TestRevokeDevicePermission() {
    auto origin = url::Origin::Create(origin_url_);
    auto* context = GetChooserContext();

    // Connect a fake device.
    auto device_info = device_manager()->CreateAndAddDevice(
        0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");

    // Create the WebUsbService and call GetDevices to ensure it is started.
    mojo::Remote<blink::mojom::WebUsbService> web_usb_service;
    ConnectToService(web_usb_service.BindNewPipeAndPassReceiver());
    GetDevicesBlocking(web_usb_service.get(), {});

    // Grant permission to access the connected device.
    context->GrantDevicePermission(origin, *device_info);
    auto objects = context->GetGrantedObjects(origin);
    ASSERT_EQ(1u, objects.size());

    // Connect the UsbDevice.
    mojo::Remote<device::mojom::UsbDevice> device;
    web_usb_service->GetDevice(device_info->guid,
                               device.BindNewPipeAndPassReceiver());
    ASSERT_TRUE(device);

    // Revoke the permission. The UsbDevice should be disconnected.
    base::RunLoop disconnect_loop;
    device.set_disconnect_handler(disconnect_loop.QuitClosure());
    context->RevokeObjectPermission(origin, objects[0]->value);
    disconnect_loop.Run();
  }

  void TestOpenAndCloseDevice(content::WebContents* web_contents) {
    auto origin = url::Origin::Create(origin_url_);
    mojo::Remote<blink::mojom::WebUsbService> service;
    ConnectToService(service.BindNewPipeAndPassReceiver());

    // Connect a device and grant permission to access it.
    auto device_info = device_manager()->CreateAndAddDevice(
        0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
    GetChooserContext()->GrantDevicePermission(origin, *device_info);
    device::MockUsbMojoDevice mock_device;
    device_manager()->SetMockForDevice(device_info->guid, &mock_device);

    // Call GetDevices and expect the device to be returned.
    MockDeviceManagerClient mock_client;
    service->SetClient(mock_client.CreateInterfacePtrAndBind());
    GetDevicesBlocking(service.get(), {device_info->guid});

    // Call GetDevice to get the device. The WebContents should not indicate we
    // are connected to a device since the device is not opened.
    mojo::Remote<device::mojom::UsbDevice> device;
    service->GetDevice(device_info->guid, device.BindNewPipeAndPassReceiver());
    if (web_contents) {
      EXPECT_FALSE(web_contents->IsCapabilityActive(
          content::WebContentsCapabilityType::kUSB));
    }

    // Open the device. Now the WebContents should indicate we are connected to
    // a USB device.
    EXPECT_CALL(mock_device, Open)
        .WillOnce(RunOnceCallback<0>(NewUsbOpenDeviceSuccess()));
    TestFuture<device::mojom::UsbOpenDeviceResultPtr> open_future;
    ExpectIncrementConnectionCount(origin);
    device->Open(open_future.GetCallback());
    EXPECT_TRUE(open_future.Get()->is_success());
    if (web_contents) {
      EXPECT_TRUE(web_contents->IsCapabilityActive(
          content::WebContentsCapabilityType::kUSB));
    }

    // Close the device and check that the WebContents no longer indicates we
    // are connected.
    EXPECT_CALL(mock_device, Close).WillOnce(RunOnceClosure<0>());
    base::RunLoop loop;
    ExpectDecrementConnectionCount(origin);
    device->Close(loop.QuitClosure());
    loop.Run();
    if (web_contents) {
      EXPECT_FALSE(web_contents->IsCapabilityActive(
          content::WebContentsCapabilityType::kUSB));
    }
  }

  void TestOpenAndDisconnectDevice(content::WebContents* web_contents) {
    auto origin = url::Origin::Create(origin_url_);
    mojo::Remote<blink::mojom::WebUsbService> service;
    ConnectToService(service.BindNewPipeAndPassReceiver());

    // Connect a device and grant permission to access it.
    auto fake_device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
        0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
    auto device_info = device_manager()->AddDevice(fake_device);
    GetChooserContext()->GrantDevicePermission(origin, *device_info);
    device::MockUsbMojoDevice mock_device;
    device_manager()->SetMockForDevice(device_info->guid, &mock_device);

    // Call GetDevices and expect the device to be returned.
    MockDeviceManagerClient mock_client;
    service->SetClient(mock_client.CreateInterfacePtrAndBind());
    GetDevicesBlocking(service.get(), {device_info->guid});

    // Call GetDevice to get the device. The WebContents should not indicate we
    // are connected to a device since the device is not opened.
    mojo::Remote<device::mojom::UsbDevice> device;
    service->GetDevice(device_info->guid, device.BindNewPipeAndPassReceiver());
    if (web_contents) {
      EXPECT_FALSE(web_contents->IsCapabilityActive(
          content::WebContentsCapabilityType::kUSB));
    }

    // Open the device. Now the WebContents should indicate we are connected to
    // a USB device.
    EXPECT_CALL(mock_device, Open)
        .WillOnce(RunOnceCallback<0>(NewUsbOpenDeviceSuccess()));
    TestFuture<device::mojom::UsbOpenDeviceResultPtr> open_future;
    ExpectIncrementConnectionCount(origin);
    device->Open(open_future.GetCallback());
    EXPECT_TRUE(open_future.Get()->is_success());
    if (web_contents) {
      EXPECT_TRUE(web_contents->IsCapabilityActive(
          content::WebContentsCapabilityType::kUSB));
    }

    // Remove the device and check that the WebContents no longer indicates we
    // are connected.
    base::RunLoop decrement_connection_count_loop;
#if !BUILDFLAG(IS_ANDROID)
    if (supports_usb_connection_tracker_) {
      EXPECT_CALL(usb_connection_tracker(), DecrementConnectionCount(origin))
          .WillOnce(RunClosure(decrement_connection_count_loop.QuitClosure()));
    }
#endif
    EXPECT_CALL(mock_device, Close).WillOnce(RunOnceClosure<0>());
    device_manager()->RemoveDevice(fake_device);
#if !BUILDFLAG(IS_ANDROID)
    if (supports_usb_connection_tracker_) {
      decrement_connection_count_loop.Run();
    } else {
      base::RunLoop().RunUntilIdle();
    }
#else
    base::RunLoop().RunUntilIdle();
#endif
    if (web_contents) {
      EXPECT_FALSE(web_contents->IsCapabilityActive(
          content::WebContentsCapabilityType::kUSB));
    }
  }

  void TestWebUsbServiceNotConnected() {
    base::RunLoop run_loop;
    mojo::Remote<blink::mojom::WebUsbService> web_usb_service;
    ConnectToService(web_usb_service.BindNewPipeAndPassReceiver());
    web_usb_service.set_disconnect_handler(run_loop.QuitClosure());
    run_loop.Run();
    EXPECT_FALSE(web_usb_service.is_connected());
  }

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
  void TestAllowlistedImprivataExtension(content::WebContents* web_contents) {
    auto imprivata_origin = url::Origin::Create(origin_url_);
    auto* context = GetChooserContext();
    auto device_info = device_manager()->AddDevice(CreateFakeUsbDeviceInfo());
    context->GrantDevicePermission(imprivata_origin, *device_info);

    mojo::Remote<blink::mojom::WebUsbService> service;
    ConnectToService(service.BindNewPipeAndPassReceiver());
    MockDeviceManagerClient mock_client;
    service->SetClient(mock_client.CreateInterfacePtrAndBind());
    GetDevicesBlocking(service.get(), {device_info->guid});

    mojo::Remote<device::mojom::UsbDevice> device;
    service->GetDevice(device_info->guid, device.BindNewPipeAndPassReceiver());
    if (web_contents) {
      EXPECT_FALSE(web_contents->IsCapabilityActive(
          content::WebContentsCapabilityType::kUSB));
    }

    TestFuture<device::mojom::UsbOpenDeviceResultPtr> open_future;
    device->Open(open_future.GetCallback());
    EXPECT_TRUE(open_future.Get()->is_success());
    if (web_contents) {
      EXPECT_TRUE(web_contents->IsCapabilityActive(
          content::WebContentsCapabilityType::kUSB));
    }

    TestFuture<bool> set_configuration_future;
    device->SetConfiguration(1, set_configuration_future.GetCallback());
    EXPECT_TRUE(set_configuration_future.Get());

    TestFuture<device::mojom::UsbClaimInterfaceResult> claim_interface_future;
    device->ClaimInterface(0, claim_interface_future.GetCallback());

#if BUILDFLAG(IS_CHROMEOS)
    // The allowlist only allows the interface to be claimed on Chrome OS.
    EXPECT_EQ(claim_interface_future.Get(),
              device::mojom::UsbClaimInterfaceResult::kSuccess);
#else
    EXPECT_EQ(claim_interface_future.Get(),
              device::mojom::UsbClaimInterfaceResult::kProtectedClass);
#endif  // BUILDFLAG(IS_CHROMEOS)
  }

  void TestAllowlistedSmartCardConnectorExtension(
      content::WebContents* web_contents) {
    auto extension_origin = url::Origin::Create(origin_url_);
    // Add a smart card device. Also add an unrelated device, in order to test
    // that access is not automatically granted to it.
    auto ccid_device_info =
        device_manager()->AddDevice(CreateFakeSmartCardDeviceInfo());
    auto usb_device_info =
        device_manager()->AddDevice(CreateFakeUsbDeviceInfo());

    // No need to grant permission. It is granted automatically for smart
    // card device.

    mojo::Remote<blink::mojom::WebUsbService> service;
    ConnectToService(service.BindNewPipeAndPassReceiver());
    MockDeviceManagerClient mock_client;
    service->SetClient(mock_client.CreateInterfacePtrAndBind());

    // Check that the extensions is automatically granted access to the CCID
    // device and can claim its interfaces.
    {
      GetDevicesBlocking(service.get(), {ccid_device_info->guid});

      mojo::Remote<device::mojom::UsbDevice> device;
      service->GetDevice(ccid_device_info->guid,
                         device.BindNewPipeAndPassReceiver());
      if (web_contents) {
        EXPECT_FALSE(web_contents->IsCapabilityActive(
            content::WebContentsCapabilityType::kUSB));
      }

      TestFuture<device::mojom::UsbOpenDeviceResultPtr> open_future;
      device->Open(open_future.GetCallback());
      EXPECT_TRUE(open_future.Get()->is_success());
      if (web_contents) {
        EXPECT_TRUE(web_contents->IsCapabilityActive(
            content::WebContentsCapabilityType::kUSB));
      }

      TestFuture<bool> set_configuration_future;
      device->SetConfiguration(1, set_configuration_future.GetCallback());
      EXPECT_TRUE(set_configuration_future.Get());

      TestFuture<device::mojom::UsbClaimInterfaceResult> claim_interface_future;
      device->ClaimInterface(0, claim_interface_future.GetCallback());
      EXPECT_EQ(claim_interface_future.Get(),
                device::mojom::UsbClaimInterfaceResult::kSuccess);
    }

    // Check that the extension, if granted permission to a USB device can't
    // claim its interfaces.
    {
      GetChooserContext()->GrantDevicePermission(extension_origin,
                                                 *usb_device_info);
      GetDevicesBlocking(service.get(),
                         {ccid_device_info->guid, usb_device_info->guid});

      mojo::Remote<device::mojom::UsbDevice> device;
      service->GetDevice(usb_device_info->guid,
                         device.BindNewPipeAndPassReceiver());

      TestFuture<device::mojom::UsbOpenDeviceResultPtr> open_future;
      device->Open(open_future.GetCallback());
      EXPECT_TRUE(open_future.Get()->is_success());

      TestFuture<bool> set_configuration_future;
      device->SetConfiguration(1, set_configuration_future.GetCallback());
      EXPECT_TRUE(set_configuration_future.Get());

      TestFuture<device::mojom::UsbClaimInterfaceResult> claim_interface_future;
      device->ClaimInterface(0, claim_interface_future.GetCallback());
      EXPECT_EQ(claim_interface_future.Get(),
                device::mojom::UsbClaimInterfaceResult::kProtectedClass);
    }
  }
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)

 protected:
  raw_ptr<TestingProfile, DanglingUntriaged> profile_ = nullptr;
  GURL origin_url_;

#if !BUILDFLAG(IS_ANDROID)
  raw_ptr<MockUsbConnectionTracker, DanglingUntriaged> usb_connection_tracker_ =
      nullptr;
#endif

  // This flag is expected to be set to true only for the scenario of extension
  // origin.
  bool supports_usb_connection_tracker_ = false;

 private:
  device::ScopedUsbDeviceManagerOverrider usb_device_manager_overrider_;
};

class ChromeUsbDelegateRenderFrameTestBase
    : public ChromeRenderViewHostTestHarness,
      public ChromeUsbTestHelper {
 public:
  void SetUp() override {
    ChromeRenderViewHostTestHarness::SetUp();
    profile_ = profile();
    ASSERT_TRUE(profile_);
#if !BUILDFLAG(IS_ANDROID)
    UsbConnectionTrackerFactory::GetInstance()->SetTestingFactory(
        profile_, GetUsbConnectionTrackerTestingFactory());
    SetUpUsbConnectionTracker();
#endif
    SetUpOriginUrl();
    NavigateAndCommit(origin_url_);
  }

  // ChromeUsbTestHelper:
  void ConnectToService(
      mojo::PendingReceiver<blink::mojom::WebUsbService> receiver) override {
    SetUpFakeDeviceManager();
    content::RenderFrameHostTester::For(main_rfh())
        ->CreateWebUsbServiceForTesting(std::move(receiver));
  }

  void TestOpenAndNavigateCrossOrigin(content::WebContents* web_contents) {
    auto origin = url::Origin::Create(origin_url_);
    mojo::Remote<blink::mojom::WebUsbService> service;
    ConnectToService(service.BindNewPipeAndPassReceiver());

    // Connect a device and grant permission to access it.
    auto fake_device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
        0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
    auto device_info = device_manager()->AddDevice(fake_device);
    GetChooserContext()->GrantDevicePermission(origin, *device_info);
    device::MockUsbMojoDevice mock_device;
    device_manager()->SetMockForDevice(device_info->guid, &mock_device);

    // Call GetDevices and expect the device info to be returned.
    GetDevicesBlocking(service.get(), {device_info->guid});

    // Call GetDevice to get the device. The WebContents should not indicate we
    // are connected to a device since the device is not opened.
    mojo::Remote<device::mojom::UsbDevice> device;
    service->GetDevice(device_info->guid, device.BindNewPipeAndPassReceiver());
    if (web_contents) {
      EXPECT_FALSE(web_contents->IsCapabilityActive(
          content::WebContentsCapabilityType::kUSB));
    }

    // Open the device. Now the WebContents should indicate we are connected to
    // a USB device.
    EXPECT_CALL(mock_device, Open)
        .WillOnce(RunOnceCallback<0>(NewUsbOpenDeviceSuccess()));
    TestFuture<device::mojom::UsbOpenDeviceResultPtr> open_future;
    device->Open(open_future.GetCallback());
    EXPECT_TRUE(open_future.Get()->is_success());
    if (web_contents) {
      EXPECT_TRUE(web_contents->IsCapabilityActive(
          content::WebContentsCapabilityType::kUSB));
    }

    // Perform a cross-origin navigation. The WebContents should indicate we are
    // no longer connected.
    EXPECT_CALL(mock_device, Close).WillOnce(RunOnceClosure<0>());
    NavigateAndCommit(GURL(kCrossOriginTestUrl));
    base::RunLoop().RunUntilIdle();
    if (web_contents) {
      EXPECT_FALSE(web_contents->IsCapabilityActive(
          content::WebContentsCapabilityType::kUSB));
    }
  }
};

class ChromeUsbDelegateServiceWorkerTestBase
    : public content::EmbeddedWorkerInstanceTestHarness,
      public ChromeUsbTestHelper {
 public:
  void SetUp() override {
    content::EmbeddedWorkerInstanceTestHarness::SetUp();
    SetUpOriginUrl();
#if !BUILDFLAG(IS_ANDROID)
    SetUpUsbConnectionTracker();
#endif
    StartWorker();
  }

  void TearDown() override {
    StopWorker();
    content::EmbeddedWorkerInstanceTestHarness::TearDown();
  }

  // ChromeUsbTestHelper:
  void ConnectToService(
      mojo::PendingReceiver<blink::mojom::WebUsbService> receiver) override {
    SetUpFakeDeviceManager();
    BindUsbServiceToWorker(origin_url_, std::move(receiver));
  }

  // content::EmbeddedWorkerInstanceTestHarness
  std::unique_ptr<content::BrowserContext> CreateBrowserContext() override {
    auto builder = TestingProfile::Builder();
    auto testing_profile = builder.Build();
    profile_ = testing_profile.get();
#if !BUILDFLAG(IS_ANDROID)
    UsbConnectionTrackerFactory::GetInstance()->SetTestingFactory(
        profile_, GetUsbConnectionTrackerTestingFactory());
#endif
    return testing_profile;
  }

  void StartWorker() {
    auto worker_url =
        GURL(base::StringPrintf("%s/worker.js", origin_url_.spec().c_str()));
    CreateAndStartWorker(origin_url_, worker_url);

    // Wait until tasks triggered by ServiceWorkerUsbDelegateObserver settle.
    base::RunLoop().RunUntilIdle();
  }

  void StopWorker() { StopAndResetWorker(); }
};

class ChromeUsbDelegateRenderFrameTest
    : public ChromeUsbDelegateRenderFrameTestBase {
 public:
  void SetUpOriginUrl() override { SetUpWebPageOriginUrl(); }
};

class ChromeUsbDelegateServiceWorkerTest
    : public ChromeUsbDelegateServiceWorkerTestBase {
 public:
  void SetUpOriginUrl() override { SetUpWebPageOriginUrl(); }
};

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
class ChromeUsbDelegateExtensionRenderFrameTest
    : public ChromeUsbDelegateRenderFrameTestBase {
 public:
  ChromeUsbDelegateExtensionRenderFrameTest() {
    supports_usb_connection_tracker_ = true;
  }
  void SetUpOriginUrl() override { SetUpExtensionOriginUrl(kExtensionId); }
};

class ChromeUsbDelegateImprivataExtensionRenderFrameTest
    : public ChromeUsbDelegateRenderFrameTestBase {
 public:
  ChromeUsbDelegateImprivataExtensionRenderFrameTest() {
    supports_usb_connection_tracker_ = true;
  }
  void SetUpOriginUrl() override {
    SetUpExtensionOriginUrl(kAllowlistedImprivataExtensionId);
  }
};

class ChromeUsbDelegateSmartCardExtensionRenderFrameTest
    : public ChromeUsbDelegateRenderFrameTestBase {
 public:
  ChromeUsbDelegateSmartCardExtensionRenderFrameTest() {
    supports_usb_connection_tracker_ = true;
  }
  void SetUpOriginUrl() override {
    SetUpExtensionOriginUrl(kAllowlistedSmartCardExtensionId);
  }
};

class ChromeUsbDelegateExtensionServiceWorkerTest
    : public ChromeUsbDelegateServiceWorkerTestBase {
 public:
  ChromeUsbDelegateExtensionServiceWorkerTest() {
    supports_usb_connection_tracker_ = true;
  }
  void SetUpOriginUrl() override { SetUpExtensionOriginUrl(kExtensionId); }
};

class ChromeUsbDelegateImprivataExtensionServiceWorkerTest
    : public ChromeUsbDelegateServiceWorkerTestBase {
 public:
  ChromeUsbDelegateImprivataExtensionServiceWorkerTest() {
    supports_usb_connection_tracker_ = true;
  }
  void SetUpOriginUrl() override {
    SetUpExtensionOriginUrl(kAllowlistedImprivataExtensionId);
  }
};

class ChromeUsbDelegateSmartCardExtensionServiceWorkerTest
    : public ChromeUsbDelegateServiceWorkerTestBase {
 public:
  ChromeUsbDelegateSmartCardExtensionServiceWorkerTest() {
    supports_usb_connection_tracker_ = true;
  }
  void SetUpOriginUrl() override {
    SetUpExtensionOriginUrl(kAllowlistedSmartCardExtensionId);
  }
};

#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)

class ChromeUsbDelegateGuestTest : public ChromeRenderViewHostTestHarness {
 public:
  std::unique_ptr<content::WebContents> CreateGuestWebContents(
      const GURL& url) {
    const content::StoragePartitionConfig kGuestConfig =
        content::StoragePartitionConfig::Create(
            profile(), "test_partition", "guest_partition", /*in_memory=*/true);
    scoped_refptr<content::SiteInstance> guest_instance =
        content::SiteInstance::CreateForGuest(profile(), kGuestConfig);
    std::unique_ptr<content::WebContents> guest_contents =
        content::WebContentsTester::CreateTestWebContents(profile(),
                                                          guest_instance);
    content::WebContentsTester::For(guest_contents.get())
        ->NavigateAndCommit(url);
    return guest_contents;
  }
};

TEST_F(ChromeUsbDelegateGuestTest, BlocksGuestViews) {
  ChromeUsbDelegate usb_delegate;

  // 1. Test HTTPS Guest
  {
    std::unique_ptr<content::WebContents> guest =
        CreateGuestWebContents(GURL("https://example.com/"));
    EXPECT_FALSE(usb_delegate.PageMayUseUsb(guest->GetPrimaryPage()));
  }

  // 2. Test about:blank Guest
  {
    std::unique_ptr<content::WebContents> guest =
        CreateGuestWebContents(GURL("about:blank"));
    EXPECT_FALSE(usb_delegate.PageMayUseUsb(guest->GetPrimaryPage()));
  }
}

}  // namespace

TEST_F(ChromeUsbDelegateRenderFrameTest, NoPermissionDevice) {
  TestNoPermissionDevice();
}

TEST_F(ChromeUsbDelegateRenderFrameTest, ReconnectDeviceManager) {
  TestReconnectDeviceManager();
}

TEST_F(ChromeUsbDelegateRenderFrameTest, RevokeDevicePermission) {
  TestRevokeDevicePermission();
}

TEST_F(ChromeUsbDelegateRenderFrameTest, OpenAndCloseDevice) {
  TestOpenAndCloseDevice(web_contents());
}

TEST_F(ChromeUsbDelegateRenderFrameTest, OpenAndDisconnectDevice) {
  TestOpenAndDisconnectDevice(web_contents());
}

TEST_F(ChromeUsbDelegateRenderFrameTest, OpenAndNavigateCrossOrigin) {
  TestOpenAndNavigateCrossOrigin(web_contents());
}

TEST_F(ChromeUsbDelegateServiceWorkerTest, WebUsbServiceNotConnected) {
  TestWebUsbServiceNotConnected();
}

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
TEST_F(ChromeUsbDelegateExtensionRenderFrameTest, NoPermissionDevice) {
  TestNoPermissionDevice();
}

TEST_F(ChromeUsbDelegateExtensionRenderFrameTest, ReconnectDeviceManager) {
  TestReconnectDeviceManager();
}

TEST_F(ChromeUsbDelegateExtensionRenderFrameTest, RevokeDevicePermission) {
  TestRevokeDevicePermission();
}

TEST_F(ChromeUsbDelegateExtensionRenderFrameTest, OpenAndCloseDevice) {
  TestOpenAndCloseDevice(web_contents());
}

TEST_F(ChromeUsbDelegateExtensionRenderFrameTest, OpenAndDisconnectDevice) {
  TestOpenAndDisconnectDevice(web_contents());
}

TEST_F(ChromeUsbDelegateImprivataExtensionRenderFrameTest,
       AllowlistedImprivataExtension) {
  TestAllowlistedImprivataExtension(web_contents());
}

TEST_F(ChromeUsbDelegateSmartCardExtensionRenderFrameTest,
       AllowlistedSmartCardConnectorExtension) {
  TestAllowlistedSmartCardConnectorExtension(web_contents());
}

// Not supported on Android. See NOTREACHED in WebUsbServiceImpl constructor.
#if !BUILDFLAG(IS_ANDROID)
TEST_F(ChromeUsbDelegateImprivataExtensionServiceWorkerTest,
       AllowlistedImprivataExtension) {
  TestAllowlistedImprivataExtension(nullptr);
}

TEST_F(ChromeUsbDelegateSmartCardExtensionServiceWorkerTest,
       AllowlistedSmartCardConnectorExtension) {
  TestAllowlistedSmartCardConnectorExtension(nullptr);
}

TEST_F(ChromeUsbDelegateExtensionServiceWorkerTest, NoPermissionDevice) {
  TestNoPermissionDevice();
}

TEST_F(ChromeUsbDelegateExtensionServiceWorkerTest, ReconnectDeviceManager) {
  TestReconnectDeviceManager();
}

TEST_F(ChromeUsbDelegateExtensionServiceWorkerTest, RevokeDevicePermission) {
  TestRevokeDevicePermission();
}

TEST_F(ChromeUsbDelegateExtensionServiceWorkerTest, OpenAndCloseDevice) {
  TestOpenAndCloseDevice(/*web_contents=*/nullptr);
}

TEST_F(ChromeUsbDelegateExtensionServiceWorkerTest, OpenAndDisconnectDevice) {
  TestOpenAndDisconnectDevice(/*web_contents=*/nullptr);
}
#endif  // !BUILDFLAG(IS_ANDROID)

#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)

TEST(ChromeUsbDelegateBrowserContextTest, BrowserContextIsNull) {
  base::test::SingleThreadTaskEnvironment task_environment;
  ChromeUsbDelegate chrome_usb_delegate;
  url::Origin origin = url::Origin::Create(GURL(kDefaultTestUrl));
  EXPECT_FALSE(chrome_usb_delegate.CanRequestDevicePermission(
      /*browser_context=*/nullptr, origin));
  EXPECT_FALSE(chrome_usb_delegate.HasDevicePermission(
      /*browser_context=*/nullptr, /*frame=*/nullptr, origin,
      device::mojom::UsbDeviceInfo()));
  EXPECT_EQ(nullptr, chrome_usb_delegate.GetDeviceInfo(
                         /*browser_context=*/nullptr,
                         base::Uuid::GenerateRandomV4().AsLowercaseString()));

  TestFuture<std::vector<device::mojom::UsbDeviceInfoPtr>> get_devices_future;
  chrome_usb_delegate.GetDevices(/*browser_context=*/nullptr,
                                 get_devices_future.GetCallback());
  EXPECT_TRUE(get_devices_future.Get().empty());

  // TODO(crbug.com/40217296): Test GetDevice with null browser_context.
}
