chromium/chrome/browser/ash/crosapi/search_controller_ash_unittest.cc

// Copyright 2024 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/ash/crosapi/search_controller_ash.h"

#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/auto_reset.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chromeos/crosapi/mojom/launcher_search.mojom-forward.h"
#include "chromeos/crosapi/mojom/launcher_search.mojom.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace crosapi {
namespace {

using ::testing::ElementsAre;
using ::testing::Field;
using ::testing::InSequence;
using ::testing::IsEmpty;
using ::testing::Optional;
using ::testing::Pointee;

// TODO: b/326147929 - Share this code with `crosapi::SearchControllerAsh` unit
// tests (and possibly `app_list::OmniboxLacrosProvider` unit tests too).
class TestMojomSearchController : public mojom::SearchController {
 public:
  mojo::PendingRemote<mojom::SearchController> BindToRemote() {
    return receiver_.BindNewPipeAndPassRemote();
  }

  void RunUntilSearch() {
    base::test::TestFuture<void> future;
    base::AutoReset<base::RepeatingClosure> quit_loop(
        &search_callback_, future.GetRepeatingCallback());
    EXPECT_TRUE(future.Wait());
  }

  void ProduceResults(
      mojom::SearchStatus status,
      std::optional<std::vector<mojom::SearchResultPtr>> results) {
    publisher_->OnSearchResultsReceived(status, std::move(results));
  }

  const std::u16string& last_query() { return last_query_; }

 private:
  void Search(const std::u16string& query, SearchCallback callback) override {
    last_query_ = query;

    publisher_.reset();
    std::move(callback).Run(publisher_.BindNewEndpointAndPassReceiver());

    search_callback_.Run();
  }

  base::RepeatingClosure search_callback_ = base::DoNothing();

  mojo::Receiver<mojom::SearchController> receiver_{this};
  mojo::AssociatedRemote<mojom::SearchResultsPublisher> publisher_;
  std::u16string last_query_;
};

using SearchResultsTestFuture =
    ::base::test::TestFuture<std::vector<mojom::SearchResultPtr>>;
using DisconnectTestFuture =
    ::base::test::TestFuture<::base::WeakPtr<::crosapi::SearchControllerAsh>>;

using SearchControllerAshTest = ::testing::Test;

TEST_F(SearchControllerAshTest, CallbackNotCalledIfNotConnected) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchResultsTestFuture future;

  std::unique_ptr<SearchControllerAsh> controller;
  {
    TestMojomSearchController mojom_controller;
    controller =
        std::make_unique<SearchControllerAsh>(mojom_controller.BindToRemote());
  }
  {
    DisconnectTestFuture future1;
    controller->AddDisconnectHandler(future1.GetCallback());
    EXPECT_TRUE(future1.Wait());
  }
  controller->Search(u"cat", future.GetRepeatingCallback());

  EXPECT_FALSE(future.IsReady());
}

TEST_F(SearchControllerAshTest, CallbackNotCalledIfBackendUnavailable) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchResultsTestFuture future;
  TestMojomSearchController mojom_controller;

  SearchControllerAsh controller(mojom_controller.BindToRemote());
  controller.Search(u"cat", future.GetRepeatingCallback());
  mojom_controller.RunUntilSearch();
  mojom_controller.ProduceResults(mojom::SearchStatus::kBackendUnavailable,
                                  std::nullopt);
  // Run until `controller.OnSearchResultsReceived()` is called.
  // TODO: b/326147929 - Use a `QuitClosure` for this.
  base::RunLoop().RunUntilIdle();

  EXPECT_FALSE(future.IsReady());
}

TEST_F(SearchControllerAshTest, CallbackNotCalledIfCancelled) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchResultsTestFuture future;
  TestMojomSearchController mojom_controller;

  SearchControllerAsh controller(mojom_controller.BindToRemote());
  controller.Search(u"cat", future.GetRepeatingCallback());
  mojom_controller.RunUntilSearch();
  mojom_controller.ProduceResults(mojom::SearchStatus::kBackendUnavailable,
                                  std::nullopt);
  // Run until `controller.OnSearchResultsReceived()` is called.
  // TODO: b/326147929 - Use a `QuitClosure` for this.
  base::RunLoop().RunUntilIdle();

  EXPECT_FALSE(future.IsReady());
}

TEST_F(SearchControllerAshTest, CallbackCalledWithEmptyResults) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchResultsTestFuture future;
  TestMojomSearchController mojom_controller;

  SearchControllerAsh controller(mojom_controller.BindToRemote());
  controller.Search(u"cat", future.GetRepeatingCallback());
  mojom_controller.RunUntilSearch();
  mojom_controller.ProduceResults(mojom::SearchStatus::kDone,
                                  std::vector<mojom::SearchResultPtr>());

  std::vector<mojom::SearchResultPtr> returned_results = future.Take();
  EXPECT_THAT(returned_results, IsEmpty());
}

TEST_F(SearchControllerAshTest,
       CallbackCalledWithMultipleResultsSimultaneously) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchResultsTestFuture future;
  TestMojomSearchController mojom_controller;

  SearchControllerAsh controller(mojom_controller.BindToRemote());
  controller.Search(u"cat", future.GetRepeatingCallback());
  mojom_controller.RunUntilSearch();
  std::vector<mojom::SearchResultPtr> results;
  {
    mojom::SearchResultPtr result = mojom::SearchResult::New();
    result->destination_url = GURL("https://www.google.com/search?q=cat");
    results.push_back(std::move(result));
  }
  {
    mojom::SearchResultPtr result = mojom::SearchResult::New();
    result->destination_url =
        GURL("https://www.google.com/search?q=catalan+numbers");
    results.push_back(std::move(result));
  }
  mojom_controller.ProduceResults(mojom::SearchStatus::kDone,
                                  std::move(results));

  std::vector<mojom::SearchResultPtr> returned_results = future.Take();
  EXPECT_THAT(
      returned_results,
      ElementsAre(
          Pointee(Field("destination_url",
                        &mojom::SearchResult::destination_url,
                        Optional(GURL("https://www.google.com/search?q=cat")))),
          Pointee(Field(
              "destination_url", &mojom::SearchResult::destination_url,
              Optional(
                  GURL("https://www.google.com/search?q=catalan+numbers"))))));
}

TEST_F(SearchControllerAshTest, CallbackCalledWithMultipleResultsSeparately) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchResultsTestFuture future;
  TestMojomSearchController mojom_controller;

  SearchControllerAsh controller(mojom_controller.BindToRemote());
  controller.Search(u"cat", future.GetRepeatingCallback());
  mojom_controller.RunUntilSearch();

  {
    std::vector<mojom::SearchResultPtr> results;
    mojom::SearchResultPtr result = mojom::SearchResult::New();
    result->destination_url = GURL("https://www.google.com/search?q=cat");
    results.push_back(std::move(result));
    mojom_controller.ProduceResults(mojom::SearchStatus::kDone,
                                    std::move(results));

    std::vector<mojom::SearchResultPtr> returned_results = future.Take();
    EXPECT_THAT(returned_results,
                ElementsAre(Pointee(Field(
                    "destination_url", &mojom::SearchResult::destination_url,
                    Optional(GURL("https://www.google.com/search?q=cat"))))));
  }
  {
    std::vector<mojom::SearchResultPtr> results;
    mojom::SearchResultPtr result = mojom::SearchResult::New();
    result->destination_url =
        GURL("https://www.google.com/search?q=catalan+numbers");
    results.push_back(std::move(result));
    mojom_controller.ProduceResults(mojom::SearchStatus::kDone,
                                    std::move(results));

    std::vector<mojom::SearchResultPtr> returned_results = future.Take();
    EXPECT_THAT(returned_results,
                ElementsAre(Pointee(Field(
                    "destination_url", &mojom::SearchResult::destination_url,
                    Optional(GURL(
                        "https://www.google.com/search?q=catalan+numbers"))))));
  }
}

TEST_F(SearchControllerAshTest, CallbackIsNotCalledWithInProgressResults) {
  base::test::SingleThreadTaskEnvironment environment;
  SearchResultsTestFuture future;
  TestMojomSearchController mojom_controller;

  SearchControllerAsh controller(mojom_controller.BindToRemote());
  controller.Search(u"cat", future.GetRepeatingCallback());
  mojom_controller.RunUntilSearch();

  {
    std::vector<mojom::SearchResultPtr> results;
    mojom::SearchResultPtr result = mojom::SearchResult::New();
    result->destination_url = GURL("https://www.google.com/search?q=cat");
    results.push_back(std::move(result));
    mojom_controller.ProduceResults(mojom::SearchStatus::kInProgress,
                                    std::move(results));
    // Run until `controller.OnSearchResultsReceived()` is run.
    // TODO: b/326147929 - Use a `QuitClosure` for this.
    base::RunLoop().RunUntilIdle();

    EXPECT_FALSE(future.IsReady());
  }
  {
    std::vector<mojom::SearchResultPtr> results;
    mojom::SearchResultPtr result = mojom::SearchResult::New();
    result->destination_url =
        GURL("https://www.google.com/search?q=catalan+numbers");
    results.push_back(std::move(result));
    mojom_controller.ProduceResults(mojom::SearchStatus::kDone,
                                    std::move(results));

    std::vector<mojom::SearchResultPtr> returned_results = future.Take();
    EXPECT_THAT(returned_results,
                ElementsAre(Pointee(Field(
                    "destination_url", &mojom::SearchResult::destination_url,
                    Optional(GURL(
                        "https://www.google.com/search?q=catalan+numbers"))))));
  }
}

TEST_F(SearchControllerAshTest,
       DisconnectHandlerIsCalledOnDisconnectWithValidPointer) {
  base::test::SingleThreadTaskEnvironment environment;
  DisconnectTestFuture future;
  auto mojom_controller = std::make_unique<TestMojomSearchController>();
  SearchControllerAsh controller(mojom_controller->BindToRemote());

  controller.AddDisconnectHandler(future.GetCallback());
  mojom_controller.reset();
  ASSERT_TRUE(future.Wait()) << "Disconnect handler was never called";
  ASSERT_FALSE(controller.IsConnected());

  EXPECT_TRUE(future.IsReady());
  base::WeakPtr<SearchControllerAsh> weak_controller = future.Take();
  EXPECT_TRUE(weak_controller);
  EXPECT_EQ(weak_controller.get(), &controller);
}

TEST_F(SearchControllerAshTest,
       DisconnectHandlersAreCalledOnDisconnectWithValidPointers) {
  base::test::SingleThreadTaskEnvironment environment;
  DisconnectTestFuture future_1;
  DisconnectTestFuture future_2;
  auto mojom_controller = std::make_unique<TestMojomSearchController>();
  SearchControllerAsh controller(mojom_controller->BindToRemote());

  controller.AddDisconnectHandler(future_1.GetCallback());
  controller.AddDisconnectHandler(future_2.GetCallback());
  mojom_controller.reset();
  ASSERT_TRUE(future_2.Wait()) << "Disconnect handler was never called";
  ASSERT_FALSE(controller.IsConnected());

  base::WeakPtr<SearchControllerAsh> weak_controller_1 = future_1.Take();
  EXPECT_TRUE(weak_controller_1);
  EXPECT_EQ(weak_controller_1.get(), &controller);
  base::WeakPtr<SearchControllerAsh> weak_controller_2 = future_2.Take();
  EXPECT_TRUE(weak_controller_2);
  EXPECT_EQ(weak_controller_2.get(), &controller);
}

TEST_F(SearchControllerAshTest, DisconnectHandlersAreCalledInAdditionOrder) {
  base::test::SingleThreadTaskEnvironment environment;
  DisconnectTestFuture future_1;
  DisconnectTestFuture future_2;
  auto mojom_controller = std::make_unique<TestMojomSearchController>();
  SearchControllerAsh controller(mojom_controller->BindToRemote());

  controller.AddDisconnectHandler(
      future_1.GetCallback().Then(base::BindLambdaForTesting([&future_2]() {
        EXPECT_FALSE(future_2.IsReady())
            << "Second future called before first future";
      })));
  controller.AddDisconnectHandler(future_2.GetCallback());
  mojom_controller.reset();
  ASSERT_TRUE(future_2.Wait()) << "Disconnect handler was never called";
  ASSERT_FALSE(controller.IsConnected());

  // This also guarantees that the "first future called before second future"
  // `EXPECT` above is run.
  EXPECT_TRUE(future_1.IsReady()) << "First future not called";
  EXPECT_TRUE(future_2.IsReady()) << "Second future not called";
}

TEST_F(SearchControllerAshTest,
       DisconnectHandlerIsImmediatelyCalledIfAlreadyDisconnected) {
  base::test::SingleThreadTaskEnvironment environment;
  DisconnectTestFuture future;
  auto mojom_controller = std::make_unique<TestMojomSearchController>();
  SearchControllerAsh controller(mojom_controller->BindToRemote());
  mojom_controller.reset();
  {
    DisconnectTestFuture future1;
    controller.AddDisconnectHandler(future1.GetCallback());
    EXPECT_TRUE(future1.Wait());
  }
  ASSERT_FALSE(controller.IsConnected());

  controller.AddDisconnectHandler(future.GetCallback());

  EXPECT_TRUE(future.IsReady());
}

TEST_F(SearchControllerAshTest,
       DisconnectHandlerIsNotCalledIfNeverDisconnected) {
  base::test::SingleThreadTaskEnvironment environment;
  DisconnectTestFuture future;
  TestMojomSearchController mojom_controller;
  auto controller =
      std::make_unique<SearchControllerAsh>(mojom_controller.BindToRemote());

  controller->AddDisconnectHandler(future.GetCallback());
  controller.reset();

  EXPECT_FALSE(future.IsReady());
}

TEST_F(SearchControllerAshTest,
       DisconnectHandlerIsCalledWithNullptrIfDestructed) {
  base::test::SingleThreadTaskEnvironment environment;
  DisconnectTestFuture future;
  auto mojom_controller = std::make_unique<TestMojomSearchController>();
  auto controller =
      std::make_unique<SearchControllerAsh>(mojom_controller->BindToRemote());

  controller->AddDisconnectHandler(base::BindLambdaForTesting(
      [&controller](base::WeakPtr<SearchControllerAsh>) {
        controller.reset();
      }));
  controller->AddDisconnectHandler(future.GetCallback());
  mojom_controller.reset();

  base::WeakPtr<SearchControllerAsh> weak_controller = future.Take();
  EXPECT_FALSE(weak_controller);
}

}  // namespace
}  // namespace crosapi