chromium/chrome/browser/lacros/lacros_apps_publisher_browsertest.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 "chrome/browser/lacros/lacros_apps_publisher.h"

#include <optional>

#include "chrome/browser/apps/app_service/extension_apps_utils.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_keeplist_chromeos.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/media/webrtc/media_stream_capture_indicator.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/common/extensions/manifest_handlers/app_launch_info.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/crosapi/mojom/app_service_types.mojom.h"
#include "components/app_constants/constants.h"
#include "components/services/app_service/public/cpp/app_capability_access_cache.h"
#include "content/public/test/browser_test.h"
#include "extensions/common/constants.h"
#include "third_party/blink/public/common/mediastream/media_stream_request.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"

namespace {

using Accesses = std::vector<apps::CapabilityAccessPtr>;

// This fake intercepts and tracks all calls to Publish().
class LacrosAppsPublisherFake : public LacrosAppsPublisher {
 public:
  LacrosAppsPublisherFake() : LacrosAppsPublisher() {
    // Since LacrosAppsPublisherTest run without Ash, Lacros won't get
    // the Ash extension keeplist data from Ash (passed via crosapi). Therefore,
    // set empty ash keeplist for test.
    extensions::SetEmptyAshKeeplistForTest();
    apps::EnableHostedAppsInLacrosForTesting();
  }
  ~LacrosAppsPublisherFake() override = default;

  LacrosAppsPublisherFake(const LacrosAppsPublisherFake&) = delete;
  LacrosAppsPublisherFake& operator=(const LacrosAppsPublisherFake&) = delete;

  void VerifyAppCapabilityAccess(const std::string& app_id,
                                 size_t count,
                                 std::optional<bool> accessing_camera,
                                 std::optional<bool> accessing_microphone) {
    ASSERT_EQ(count, accesses_history().size());
    Accesses& accesses = accesses_history().back();
    ASSERT_EQ(1u, accesses.size());
    EXPECT_EQ(app_id, accesses[0]->app_id);

    if (accessing_camera.has_value()) {
      ASSERT_TRUE(accesses[0]->camera.has_value());
      EXPECT_EQ(accessing_camera.value(), accesses[0]->camera.value());
    } else {
      ASSERT_FALSE(accesses[0]->camera.has_value());
    }

    if (accessing_microphone.has_value()) {
      ASSERT_TRUE(accesses[0]->microphone.has_value());
      EXPECT_EQ(accessing_microphone.value(), accesses[0]->microphone.value());
    } else {
      ASSERT_FALSE(accesses[0]->microphone.has_value());
    }
  }

  std::vector<Accesses>& accesses_history() { return accesses_history_; }

 private:
  // Override to intercept calls to PublishCapabilityAccesses().
  void PublishCapabilityAccesses(Accesses accesses) override {
    accesses_history_.push_back(std::move(accesses));
  }

  // Override to pretend that crosapi is initialized.
  bool InitializeCrosapi() override { return true; }

  // Holds the contents of all calls to PublishCapabilityAccesses() in
  // chronological order.
  std::vector<Accesses> accesses_history_;
};

// Adds a fake media device with the specified `stream_type` and starts
// capturing. Returns a closure to stop the capturing.
base::OnceClosure StartMediaCapture(content::WebContents* web_contents,
                                    blink::mojom::MediaStreamType stream_type) {
  blink::mojom::StreamDevices fake_devices;
  blink::MediaStreamDevice device(stream_type, "fake_device", "fake_device");

  if (blink::IsAudioInputMediaType(stream_type)) {
    fake_devices.audio_device = device;
  } else {
    fake_devices.video_device = device;
  }

  std::unique_ptr<content::MediaStreamUI> ui =
      MediaCaptureDevicesDispatcher::GetInstance()
          ->GetMediaStreamCaptureIndicator()
          ->RegisterMediaStream(web_contents, fake_devices);

  ui->OnStarted(base::RepeatingClosure(),
                content::MediaStreamUI::SourceCallback(),
                /*label=*/std::string(), /*screen_capture_ids=*/{},
                content::MediaStreamUI::StateChangeCallback());

  return base::BindOnce(
      [](std::unique_ptr<content::MediaStreamUI> ui) { ui.reset(); },
      std::move(ui));
}

using LacrosAppsPublisherTest = extensions::ExtensionBrowserTest;

// Verify AppCapabilityAccess is modified for browser tabs.
IN_PROC_BROWSER_TEST_F(LacrosAppsPublisherTest, RequestAccessingForTab) {
  std::unique_ptr<LacrosAppsPublisherFake> publisher =
      std::make_unique<LacrosAppsPublisherFake>();
  publisher->Initialize();

  ASSERT_TRUE(embedded_test_server()->Start());
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(),
      embedded_test_server()->GetURL("app.com", "/ssl/google.html")));

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();

  // Request accessing the camera for `web_contents`.
  base::OnceClosure video_closure = StartMediaCapture(
      web_contents, blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE);
  publisher->VerifyAppCapabilityAccess(app_constants::kLacrosAppId, 1u, true,
                                       std::nullopt);

  // Request accessing the microphone for `web_contents`.
  base::OnceClosure audio_closure = StartMediaCapture(
      web_contents, blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE);
  publisher->VerifyAppCapabilityAccess(app_constants::kLacrosAppId, 2u,
                                       std::nullopt, true);

  // Stop accessing the camera for `web_contents`.
  std::move(video_closure).Run();
  publisher->VerifyAppCapabilityAccess(app_constants::kLacrosAppId, 3u, false,
                                       std::nullopt);

  // Stop accessing the microphone for `web_contents`.
  std::move(audio_closure).Run();
  publisher->VerifyAppCapabilityAccess(app_constants::kLacrosAppId, 4u,
                                       std::nullopt, false);
}

// Verify AppCapabilityAccess for Chrome apps is not handled by
// LacrosAppsPublisher.
IN_PROC_BROWSER_TEST_F(LacrosAppsPublisherTest, NoRequestAccessingForHostApp) {
  std::unique_ptr<LacrosAppsPublisherFake> publisher =
      std::make_unique<LacrosAppsPublisherFake>();
  publisher->Initialize();

  ASSERT_TRUE(embedded_test_server()->Start());
  const extensions::Extension* extension =
      LoadExtension(test_data_dir_.AppendASCII("app1"));
  ASSERT_TRUE(extension);

  // Navigate to the app's launch URL.
  auto url = extensions::AppLaunchInfo::GetLaunchWebURL(extension);
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  content::WebContents* web_content =
      browser()->tab_strip_model()->GetActiveWebContents();
  ASSERT_TRUE(web_content);

  // Request accessing the camera and microphone for `web_contents`.
  base::OnceClosure video_closure1 = StartMediaCapture(
      web_content, blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE);
  base::OnceClosure audio_closure1 = StartMediaCapture(
      web_content, blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE);

  // Verify the publisher does not handle the access for the Chrome app.
  ASSERT_TRUE(publisher->accesses_history().empty());
}

// Verify AppCapabilityAccess for web apps is not handled by
// LacrosAppsPublisher.
IN_PROC_BROWSER_TEST_F(LacrosAppsPublisherTest, NoRequestAccessingForWebApp) {
  std::unique_ptr<LacrosAppsPublisherFake> publisher =
      std::make_unique<LacrosAppsPublisherFake>();
  publisher->Initialize();

  ASSERT_TRUE(embedded_test_server()->Start());
  GURL url = embedded_test_server()->GetURL("app.com", "/ssl/google.html");
  auto web_app_info =
      web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(url);
  web_app_info->scope = url;
  auto app_id = web_app::test::InstallWebApp(browser()->profile(),
                                             std::move(web_app_info));

  // Launch |app_id| for the web app.
  web_app::LaunchWebAppBrowser(browser()->profile(), app_id);
  web_app::NavigateViaLinkClickToURLAndWait(browser(), url);
  content::WebContents* web_content =
      browser()->tab_strip_model()->GetActiveWebContents();
  ASSERT_TRUE(web_content);

  // Request accessing the camera and microphone for `web_contents`.
  base::OnceClosure video_closure1 = StartMediaCapture(
      web_content, blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE);
  base::OnceClosure audio_closure1 = StartMediaCapture(
      web_content, blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE);

  // Verify the publisher does not handle the access for the web app.
  ASSERT_TRUE(publisher->accesses_history().empty());
}

}  // namespace