// 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/chromeos/extensions/telemetry/api/routines/diagnostic_routine_manager.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/check_deref.h"
#include "base/containers/flat_map.h"
#include "base/files/file_path.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "base/types/expected.h"
#include "base/uuid.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/chromeos/extensions/telemetry/api/routines/diagnostic_routine.h"
#include "chrome/browser/chromeos/extensions/telemetry/api/routines/fake_diagnostic_routines_service.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chromeos/crosapi/mojom/telemetry_diagnostic_routine_service.mojom.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/ssl_status.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/unloaded_extension_reason.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_id.h"
#include "net/base/net_errors.h"
#include "net/cert/cert_status_flags.h"
#include "net/cert/x509_certificate.h"
#include "net/test/cert_test_util.h"
#include "net/test/test_data_directory.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/chromeos/extensions/telemetry/api/routines/fake_diagnostic_routines_service_factory.h"
#include "chromeos/ash/components/telemetry_extension/routines/telemetry_diagnostic_routine_service_ash.h"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/lacros/lacros_service.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
namespace chromeos {
namespace {
namespace crosapi = ::crosapi::mojom;
constexpr char kExtensionId1[] = "gogonhoemckpdpadfnjnpgbjpbjnodgc";
constexpr char kPwaPattern1[] = "*://googlechromelabs.github.io/*";
constexpr char kPwaUrl1[] = "https://googlechromelabs.github.io/";
constexpr char kExtensionId2[] = "alnedpmllcfpgldkagbfbjkloonjlfjb";
constexpr char kPwaPattern2[] = "https://hpcs-appschr.hpcloud.hp.com/*";
constexpr char kPwaUrl2[] = "https://hpcs-appschr.hpcloud.hp.com";
constexpr char kUnmappedUuid[] = "41976e88-b067-476f-9e1e-3ec47b6959af";
} // namespace
class TelemetryExtensionDiagnosticRoutinesManagerTest
: public BrowserWithTestWindowTest {
public:
void SetUp() override {
BrowserWithTestWindowTest::SetUp();
#if BUILDFLAG(IS_CHROMEOS_ASH)
fake_routines_service_impl_ = new FakeDiagnosticRoutinesService();
fake_routines_service_factory_.SetCreateInstanceResponse(
std::unique_ptr<FakeDiagnosticRoutinesService>(
fake_routines_service_impl_.get()));
ash::TelemetryDiagnosticsRoutineServiceAsh::Factory::SetForTesting(
&fake_routines_service_factory_);
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
fake_routines_service_impl_ =
std::make_unique<FakeDiagnosticRoutinesService>();
// Replace the production `TelemetryDiagnosticRoutinesService` with a fake
// for testing.
chromeos::LacrosService::Get()->InjectRemoteForTesting(
fake_routines_service_impl_->receiver().BindNewPipeAndPassRemote());
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
}
void TearDown() override {
#if BUILDFLAG(IS_CHROMEOS_ASH)
fake_routines_service_impl_ = nullptr;
#endif
BrowserWithTestWindowTest::TearDown();
}
protected:
void OpenAppUiUrlAndSetCertificateWithStatus(const GURL& url,
net::CertStatus cert_status) {
const base::FilePath certs_dir = net::GetTestCertsDirectory();
scoped_refptr<net::X509Certificate> test_cert(
net::ImportCertFromFile(certs_dir, "ok_cert.pem"));
ASSERT_TRUE(test_cert);
AddTab(browser(), url);
// AddTab() adds a new tab at index 0.
auto* web_contents = browser()->tab_strip_model()->GetWebContentsAt(0);
auto* entry = web_contents->GetController().GetVisibleEntry();
content::SSLStatus& ssl = entry->GetSSL();
ssl.certificate = test_cert;
ssl.cert_status = cert_status;
}
scoped_refptr<const extensions::Extension> CreateExtension(
const std::string& extension_id,
const std::vector<std::string> external_connectables) {
base::Value::List matches;
for (const auto& match : external_connectables) {
matches.Append(match);
}
auto extension =
extensions::ExtensionBuilder("Test ChromeOS System Extension")
.SetManifestVersion(3)
.SetManifestKey("chromeos_system_extension", base::Value::Dict())
.SetManifestKey(
"externally_connectable",
base::Value::Dict().Set("matches", std::move(matches)))
.SetID(extension_id)
.SetLocation(extensions::mojom::ManifestLocation::kInternal)
.Build();
extensions::ExtensionRegistry::Get(profile())->AddEnabled(extension);
return extension;
}
DiagnosticRoutineManager& routine_manager() {
return CHECK_DEREF(DiagnosticRoutineManager::Get(profile()));
}
FakeDiagnosticRoutinesService& fake_service() {
return CHECK_DEREF(fake_routines_service_impl_.get());
}
crosapi::TelemetryDiagnosticRoutineArgumentPtr GetMemoryArgument() {
auto memory_arg = crosapi::TelemetryDiagnosticMemoryRoutineArgument::New();
memory_arg->max_testing_mem_kib = 42;
return crosapi::TelemetryDiagnosticRoutineArgument::NewMemory(
std::move(memory_arg));
}
base::flat_map<extensions::ExtensionId, std::unique_ptr<AppUiObserver>>&
app_ui_observers() {
return routine_manager().app_ui_observers_;
}
bool IsUuidRegisteredForExtension(extensions::ExtensionId extension_id,
base::Uuid uuid) {
const auto& routines_per_extension =
routine_manager().routines_per_extension_;
auto it = routines_per_extension.find(extension_id);
if (it == routines_per_extension.end()) {
return false;
}
auto found_uuid =
std::find_if(it->second.begin(), it->second.end(),
[uuid](const std::unique_ptr<DiagnosticRoutine>& elem) {
return elem->info_.uuid == uuid;
});
return found_uuid != it->second.end();
}
private:
#if BUILDFLAG(IS_CHROMEOS_ASH)
raw_ptr<FakeDiagnosticRoutinesService> fake_routines_service_impl_;
FakeDiagnosticRoutinesServiceFactory fake_routines_service_factory_;
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
#if BUILDFLAG(IS_CHROMEOS_LACROS)
std::unique_ptr<FakeDiagnosticRoutinesService> fake_routines_service_impl_;
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
};
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest,
CreateRoutineNoExtension) {
EXPECT_EQ(
base::unexpected(DiagnosticRoutineManager::kExtensionUnloaded),
routine_manager().CreateRoutine(kExtensionId1, GetMemoryArgument()));
}
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest,
CreateRoutineAppUiClosed) {
CreateExtension(kExtensionId1, {kPwaPattern1});
EXPECT_EQ(
base::unexpected(DiagnosticRoutineManager::kAppUiClosed),
routine_manager().CreateRoutine(kExtensionId1, GetMemoryArgument()));
}
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest, CreateRoutineSuccess) {
CreateExtension(kExtensionId1, {kPwaPattern1});
OpenAppUiUrlAndSetCertificateWithStatus(GURL(kPwaUrl1),
/*cert_status=*/net::OK);
auto create_result =
routine_manager().CreateRoutine(kExtensionId1, GetMemoryArgument());
fake_service().FlushForTesting();
EXPECT_TRUE(create_result.has_value());
EXPECT_TRUE(app_ui_observers().contains(kExtensionId1));
auto* control = fake_service().GetCreatedRoutineControlForRoutineType(
crosapi::TelemetryDiagnosticRoutineArgument::Tag::kMemory);
ASSERT_TRUE(control);
EXPECT_TRUE(control->receiver().is_bound());
base::test::TestFuture<void> future;
control->receiver().set_disconnect_handler(future.GetCallback());
// Closing the tab cuts the observation.
browser()->tab_strip_model()->CloseWebContentsAt(0,
TabCloseTypes::CLOSE_NONE);
fake_service().FlushForTesting();
EXPECT_FALSE(app_ui_observers().contains(kExtensionId1));
EXPECT_FALSE(
IsUuidRegisteredForExtension(kExtensionId1, create_result.value()));
EXPECT_TRUE(future.Wait());
}
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest,
CreateRoutineTwoExtension) {
CreateExtension(kExtensionId1, {kPwaPattern1});
CreateExtension(kExtensionId2, {kPwaPattern2});
// Open app UI for extension 1.
OpenAppUiUrlAndSetCertificateWithStatus(GURL(kPwaUrl1),
/*cert_status=*/net::OK);
auto create_result =
routine_manager().CreateRoutine(kExtensionId1, GetMemoryArgument());
fake_service().FlushForTesting();
EXPECT_TRUE(create_result.has_value());
EXPECT_TRUE(app_ui_observers().contains(kExtensionId1));
EXPECT_EQ(
base::unexpected(DiagnosticRoutineManager::kAppUiClosed),
routine_manager().CreateRoutine(kExtensionId2, GetMemoryArgument()));
EXPECT_TRUE(app_ui_observers().contains(kExtensionId1));
EXPECT_FALSE(app_ui_observers().contains(kExtensionId2));
// Open app UI for extension 2.
OpenAppUiUrlAndSetCertificateWithStatus(GURL(kPwaUrl2),
/*cert_status=*/net::OK);
auto create_result_id2 =
routine_manager().CreateRoutine(kExtensionId2, GetMemoryArgument());
fake_service().FlushForTesting();
EXPECT_TRUE(create_result_id2.has_value());
EXPECT_TRUE(app_ui_observers().contains(kExtensionId1));
EXPECT_TRUE(app_ui_observers().contains(kExtensionId2));
// Close the app UI of extension 1.
browser()->tab_strip_model()->CloseWebContentsAt(1,
TabCloseTypes::CLOSE_NONE);
fake_service().FlushForTesting();
EXPECT_FALSE(app_ui_observers().contains(kExtensionId1));
EXPECT_TRUE(app_ui_observers().contains(kExtensionId2));
EXPECT_FALSE(
IsUuidRegisteredForExtension(kExtensionId1, create_result.value()));
// Close the app UI of extension 2.
browser()->tab_strip_model()->CloseWebContentsAt(0,
TabCloseTypes::CLOSE_NONE);
fake_service().FlushForTesting();
EXPECT_FALSE(app_ui_observers().contains(kExtensionId1));
EXPECT_FALSE(app_ui_observers().contains(kExtensionId2));
EXPECT_FALSE(
IsUuidRegisteredForExtension(kExtensionId2, create_result_id2.value()));
}
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest,
CreateRoutineMultipleTabsOpen) {
CreateExtension(kExtensionId1, {kPwaPattern1});
OpenAppUiUrlAndSetCertificateWithStatus(GURL(kPwaUrl1),
/*cert_status=*/net::OK);
auto create_result =
routine_manager().CreateRoutine(kExtensionId1, GetMemoryArgument());
fake_service().FlushForTesting();
EXPECT_TRUE(create_result.has_value());
EXPECT_TRUE(app_ui_observers().contains(kExtensionId1));
// Open second tab.
OpenAppUiUrlAndSetCertificateWithStatus(GURL(kPwaUrl1),
/*cert_status=*/net::OK);
EXPECT_TRUE(app_ui_observers().contains(kExtensionId1));
// Close the first tab (index 1). The observer shouldn't be cut.
browser()->tab_strip_model()->CloseWebContentsAt(1,
TabCloseTypes::CLOSE_NONE);
EXPECT_TRUE(app_ui_observers().contains(kExtensionId1));
// Closing the second tab (the last one) cuts the observation.
browser()->tab_strip_model()->CloseWebContentsAt(0,
TabCloseTypes::CLOSE_NONE);
EXPECT_FALSE(app_ui_observers().contains(kExtensionId1));
EXPECT_FALSE(
IsUuidRegisteredForExtension(kExtensionId1, create_result.value()));
}
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest,
UnloadExtensionSuccess) {
auto extension = CreateExtension(kExtensionId1, {kPwaPattern1});
OpenAppUiUrlAndSetCertificateWithStatus(GURL(kPwaUrl1),
/*cert_status=*/net::OK);
auto create_result =
routine_manager().CreateRoutine(kExtensionId1, GetMemoryArgument());
fake_service().FlushForTesting();
EXPECT_TRUE(create_result.has_value());
EXPECT_TRUE(app_ui_observers().contains(kExtensionId1));
auto* control = fake_service().GetCreatedRoutineControlForRoutineType(
crosapi::TelemetryDiagnosticRoutineArgument::Tag::kMemory);
EXPECT_TRUE(control);
base::test::TestFuture<void> future;
control->receiver().set_disconnect_handler(future.GetCallback());
ASSERT_TRUE(extensions::ExtensionRegistry::Get(profile())->RemoveEnabled(
kExtensionId1));
extensions::ExtensionRegistry::Get(profile())->TriggerOnUnloaded(
extension.get(), extensions::UnloadedExtensionReason::TERMINATE);
fake_service().FlushForTesting();
auto create_result_2 =
routine_manager().CreateRoutine(kExtensionId1, GetMemoryArgument());
EXPECT_EQ(
create_result_2,
base::unexpected(DiagnosticRoutineManager::Error::kExtensionUnloaded));
EXPECT_FALSE(app_ui_observers().contains(kExtensionId1));
EXPECT_TRUE(future.Wait());
}
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest,
StartRoutineNoExtension) {
EXPECT_FALSE(routine_manager().StartRoutineForExtension(
kExtensionId1, base::Uuid::ParseLowercase(kUnmappedUuid)));
}
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest, StartRoutineNoRoutine) {
CreateExtension(kExtensionId1, {kPwaPattern1});
EXPECT_FALSE(routine_manager().StartRoutineForExtension(
kExtensionId1, base::Uuid::ParseLowercase(kUnmappedUuid)));
}
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest, StartRoutineSuccess) {
CreateExtension(kExtensionId1, {kPwaPattern1});
OpenAppUiUrlAndSetCertificateWithStatus(GURL(kPwaUrl1),
/*cert_status=*/net::OK);
auto create_result =
routine_manager().CreateRoutine(kExtensionId1, GetMemoryArgument());
EXPECT_TRUE(create_result.has_value());
EXPECT_TRUE(routine_manager().StartRoutineForExtension(
kExtensionId1, create_result.value()));
}
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest, CancelRoutineSuccess) {
CreateExtension(kExtensionId1, {kPwaPattern1});
OpenAppUiUrlAndSetCertificateWithStatus(GURL(kPwaUrl1),
/*cert_status=*/net::OK);
auto create_result =
routine_manager().CreateRoutine(kExtensionId1, GetMemoryArgument());
EXPECT_TRUE(create_result.has_value());
EXPECT_TRUE(
IsUuidRegisteredForExtension(kExtensionId1, create_result.value()));
routine_manager().CancelRoutineForExtension(kExtensionId1,
create_result.value());
EXPECT_FALSE(
IsUuidRegisteredForExtension(kExtensionId1, create_result.value()));
}
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest,
ReplyToRoutineInquiryNoExtension) {
EXPECT_FALSE(routine_manager().ReplyToRoutineInquiryForExtension(
kExtensionId1, base::Uuid::ParseLowercase(kUnmappedUuid),
crosapi::TelemetryDiagnosticRoutineInquiryReply::NewUnrecognizedReply(
true)));
}
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest,
ReplyToRoutineInquiryNoRoutine) {
CreateExtension(kExtensionId1, {kPwaPattern1});
EXPECT_FALSE(routine_manager().ReplyToRoutineInquiryForExtension(
kExtensionId1, base::Uuid::ParseLowercase(kUnmappedUuid),
crosapi::TelemetryDiagnosticRoutineInquiryReply::NewUnrecognizedReply(
true)));
}
TEST_F(TelemetryExtensionDiagnosticRoutinesManagerTest,
ReplyToRoutineInquirySuccess) {
CreateExtension(kExtensionId1, {kPwaPattern1});
OpenAppUiUrlAndSetCertificateWithStatus(GURL(kPwaUrl1),
/*cert_status=*/net::OK);
auto create_result =
routine_manager().CreateRoutine(kExtensionId1, GetMemoryArgument());
EXPECT_TRUE(create_result.has_value());
EXPECT_TRUE(routine_manager().ReplyToRoutineInquiryForExtension(
kExtensionId1, create_result.value(),
crosapi::TelemetryDiagnosticRoutineInquiryReply::NewUnrecognizedReply(
true)));
}
} // namespace chromeos