chromium/chrome/browser/apps/app_service/publishers/extension_apps_chromeos_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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include <memory>
#include <string_view>
#include <utility>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/stringprintf.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/intent_util.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "components/services/app_service/public/cpp/intent.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/manifest_handlers/file_handler_info.h"
#include "extensions/common/manifest_handlers/web_file_handlers_info.h"
#include "extensions/common/web_file_handler_constants.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#include "net/base/filename_util.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/browser/test/test_file_system_context.h"
#include "storage/common/file_system/file_system_types.h"
#include "ui/base/window_open_disposition.h"
#include "ui/display/types/display_constants.h"

namespace apps {

namespace {

// Write file to disk.
base::FilePath WriteFile(const base::FilePath& directory,
                         const std::string_view name,
                         const std::string_view content) {
  const base::FilePath path = directory.Append(std::string_view(name));
  base::ScopedAllowBlockingForTesting allow_blocking;
  base::WriteFile(path, content);
  return path;
}

}  // namespace

class ExtensionAppsChromeOsBrowserTest
    : public extensions::ExtensionBrowserTest {
 public:
  ExtensionAppsChromeOsBrowserTest() {
    feature_list_.InitAndEnableFeature(
        extensions_features::kExtensionWebFileHandlers);
  }

 protected:
  // Launch the extension from an intent and wait for a result from chrome.test.
  void LaunchExtensionAndCatchResult(const extensions::Extension& extension) {
    std::unique_ptr<Intent> intent = SetupLaunchAndGetIntent(extension);
    ASSERT_TRUE(intent);

    // Prepare to verify launch.
    extensions::ResultCatcher catcher;

    // Launch app with intent.
    Profile* const profile = browser()->profile();
    const int32_t event_flags =
        apps::GetEventFlags(WindowOpenDisposition::NEW_WINDOW,
                            /*prefer_container=*/true);
    apps::AppServiceProxyFactory::GetForProfile(profile)->LaunchAppWithIntent(
        extension.id(), event_flags, std::move(intent),
        apps::LaunchSource::kFromFileManager, nullptr, base::DoNothing());

    // Verify launch.
    ASSERT_TRUE(catcher.GetNextResult());
  }

  // Install extension as a default installed extension. The permission UI isn't
  // presented in these cases, and as such there is no need to fake clicks.
  const extensions::Extension* InstallDefaultInstalledExtension(
      base::FilePath file_path) {
    return InstallExtensionWithSourceAndFlags(
        file_path,
        /*expected_change=*/true,
        extensions::mojom::ManifestLocation::kInternal,
        extensions::Extension::WAS_INSTALLED_BY_DEFAULT);
  }

 private:
  apps::IntentPtr SetupLaunchAndGetIntent(
      const extensions::Extension& extension) {
    auto* file_handlers =
        extensions::WebFileHandlers::GetFileHandlers(extension);
    EXPECT_EQ(1u, file_handlers->size());

    // Create file(s).
    base::ScopedAllowBlockingForTesting allow_blocking;
    base::ScopedTempDir scoped_temp_dir;
    EXPECT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
    auto intent = std::make_unique<apps::Intent>(apps_util::kIntentActionView);
    intent->mime_type = "text/csv";
    intent->activity_name = "open-csv.html";
    const base::FilePath file_path =
        WriteFile(scoped_temp_dir.GetPath(), "a.csv", "1,2,3");

    // Add file(s) to intent.
    int64_t file_size = 0;
    base::GetFileSize(file_path, &file_size);

    // Create a virtual file in the file system, as required for AppService.
    scoped_refptr<storage::FileSystemContext> file_system_context =
        storage::CreateFileSystemContextForTesting(
            /*quota_manager_proxy=*/nullptr, base::FilePath());
    auto file_system_url = file_system_context->CreateCrackedFileSystemURL(
        blink::StorageKey::CreateFromStringForTesting("chrome://file-manager"),
        storage::kFileSystemTypeTest, file_path);

    // Update the intent with the file.
    auto file = std::make_unique<apps::IntentFile>(file_system_url.ToGURL());
    file->file_name = base::SafeBaseName::Create("a.csv");
    file->file_size = file_size;
    file->mime_type = "text/csv";
    intent->files.push_back(std::move(file));
    return intent;
  }

  base::test::ScopedFeatureList feature_list_;
};

// Open the extension action url when opening a matching file type.
IN_PROC_BROWSER_TEST_F(ExtensionAppsChromeOsBrowserTest, LaunchWithFileIntent) {
  // Load extension.
  static constexpr char kManifest[] = R"({
    "name": "Test",
    "version": "0.0.1",
    "manifest_version": 3,
    "file_handlers": [
      {
        "name": "Comma separated values",
        "action": "/open-csv.html",
        "accept": {"text/csv": [".csv"]}
      }
    ]
  })";
  extensions::TestExtensionDir extension_dir;
  extension_dir.WriteManifest(kManifest);
  extension_dir.WriteFile("open-csv.js", "chrome.test.succeed();");
  extension_dir.WriteFile("open-csv.html",
                          R"(<script src="/open-csv.js"></script>)");
  const extensions::Extension* extension =
      InstallDefaultInstalledExtension(extension_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  ASSERT_TRUE(extensions::WebFileHandlers::SupportsWebFileHandlers(*extension));
  LaunchExtensionAndCatchResult(*extension);
}

// Test WFH (Web File Handlers) without a key and again with a QuickOffice key.
// WFH (Web File Handlers) are an MV3 concept. QO (QuickOffice) should use WFH
// to open files for Extensions, instead of ChromeApps.
// Verify window.launchQueue presence.
IN_PROC_BROWSER_TEST_F(ExtensionAppsChromeOsBrowserTest, SetConsumerCalled) {
  struct {
    const char* title;
    const std::string manifest_part;
  } test_cases[] = {
      {"Default", ""},
      {"QuickOffice",
       base::StringPrintf(R"(, "key": "%s")",
                          extensions::web_file_handlers::kQuickOfficeKey)},
  };

  for (const auto& test_case : test_cases) {
    SCOPED_TRACE(test_case.title);
    const std::string manifest =
        base::StringPrintf(R"({
      "name": "Test",
      "version": "0.0.1",
      "manifest_version": 3,
      "file_handlers": [
        {
          "name": "Comma separated values",
          "action": "/open-csv.html",
          "accept": {"text/csv": [".csv"]}
        }
      ]
      %s
    })",
                           test_case.manifest_part.c_str());

    // Load extension.
    extensions::TestExtensionDir extension_dir;
    extension_dir.WriteManifest(manifest);
    extension_dir.WriteFile("open-csv.js", R"(
      launchQueue.setConsumer((launchParams) => {
        chrome.test.assertTrue('launchQueue' in window);
        chrome.test.succeed();
      });
    )");
    extension_dir.WriteFile("open-csv.html",
                            R"(<script src="/open-csv.js"></script>"
                              "<body>Test</body>)");
    const extensions::Extension* extension =
        InstallDefaultInstalledExtension(extension_dir.UnpackedPath());
    ASSERT_TRUE(extension);

    // TODO(crbug.com/40169582): setConsumer is called, but launchParams is
    // empty in the test. However, it is populated when run manually. Find a
    // better way to automate launchParams testing such that it's populated in
    // the test, like it is when executed manually.
    LaunchExtensionAndCatchResult(*extension);
  }
}

// Verify that a new tab prefers opening in an existing window.
IN_PROC_BROWSER_TEST_F(ExtensionAppsChromeOsBrowserTest, NavigateExisting) {
  // Create an extension.
  static constexpr char const kManifest[] = R"({
    "name": "Test",
    "version": "0.0.1",
    "manifest_version": 3,
    "file_handlers": [
      {
        "name": "Comma separated values",
        "action": "/open-csv.html",
        "accept": {"text/csv": [".csv"]}
      }
    ]
  })";

  // Load extension.
  extensions::TestExtensionDir extension_dir;
  extension_dir.WriteManifest(kManifest);
  extension_dir.WriteFile("open-csv.js", R"(
    chrome.test.assertTrue('launchQueue' in window);
    launchQueue.setConsumer((launchParams) => {
      chrome.test.assertEq(1, launchParams.files.length);
      chrome.test.assertEq("a.csv", launchParams.files[0].name);
      chrome.test.assertEq("file", launchParams.files[0].kind);
      chrome.test.succeed();
    });
  )");
  extension_dir.WriteFile("open-csv.html",
                          R"(<script src="/open-csv.js"></script>"
                            "<body>Test</body>)");
  const extensions::Extension* extension =
      InstallDefaultInstalledExtension(extension_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // Open a file twice by launching the file handler each time.
  content::WebContents* web_contents[2];
  for (unsigned short i = 0; i < 2; i++) {
    LaunchExtensionAndCatchResult(*extension);
    web_contents[i] = browser()->tab_strip_model()->GetActiveWebContents();
  }

  // GetWindowIdOfTab() returns -1 for SessionID::InvalidValue().
  ASSERT_NE(extensions::ExtensionTabUtil::GetWindowIdOfTab(web_contents[0]),
            -1);

  // The Window ID should match for both launches.
  ASSERT_EQ(extensions::ExtensionTabUtil::GetWindowIdOfTab(web_contents[0]),
            extensions::ExtensionTabUtil::GetWindowIdOfTab(web_contents[1]));

  // Tab ids should differ.
  ASSERT_NE(extensions::ExtensionTabUtil::GetTabId(web_contents[0]),
            extensions::ExtensionTabUtil::GetTabId(web_contents[1]));
}

}  // namespace apps