chromium/ash/birch/birch_weather_provider_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 "ash/birch/birch_weather_provider.h"

#include <memory>
#include <utility>
#include <vector>

#include "ash/ambient/ambient_controller.h"
#include "ash/birch/birch_icon_cache.h"
#include "ash/birch/birch_item.h"
#include "ash/birch/birch_model.h"
#include "ash/birch/stub_birch_client.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/geolocation_access_level.h"
#include "ash/public/cpp/ambient/ambient_backend_controller.h"
#include "ash/public/cpp/ambient/fake_ambient_backend_controller_impl.h"
#include "ash/public/cpp/test/test_image_downloader.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time_override.h"
#include "chromeos/ash/components/geolocation/simple_geolocation_provider.h"
#include "components/prefs/pref_service.h"
#include "components/user_manager/user_names.h"

namespace ash {
namespace {

BirchWeatherProvider* GetWeatherProvider() {
  return static_cast<BirchWeatherProvider*>(
      Shell::Get()->birch_model()->GetWeatherProviderForTest());
}

class BirchWeatherProviderTest : public AshTestBase {
 public:
  BirchWeatherProviderTest() : clock_override_(&GetTestTime, nullptr, nullptr) {
    feature_list_.InitWithFeatures(
        {features::kForestFeature, features::kBirchWeather}, {});
    // Ensure the time is morning (7 AM) so weather will be fetched.
    SetTestTime(base::Time::Now().LocalMidnight() + base::Hours(7));
  }
  ~BirchWeatherProviderTest() override = default;

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();

    image_downloader_ = std::make_unique<ash::TestImageDownloader>();

    Shell::Get()->ambient_controller()->set_backend_controller_for_testing(
        nullptr);
    auto ambient_backend_controller =
        std::make_unique<FakeAmbientBackendControllerImpl>();
    ambient_backend_controller_ = ambient_backend_controller.get();
    Shell::Get()->ambient_controller()->set_backend_controller_for_testing(
        std::move(ambient_backend_controller));
  }
  void TearDown() override {
    ambient_backend_controller_ = nullptr;
    image_downloader_.reset();
    AshTestBase::TearDown();
  }

  static base::Time GetTestTime() { return test_time_; }

  static void SetTestTime(base::Time test_time) { test_time_ = test_time; }

  raw_ptr<FakeAmbientBackendControllerImpl> ambient_backend_controller_;
  std::unique_ptr<TestImageDownloader> image_downloader_;

 private:
  base::subtle::ScopedTimeClockOverrides clock_override_;
  static base::Time test_time_;
  base::test::ScopedFeatureList feature_list_;
};

// static
base::Time BirchWeatherProviderTest::test_time_;

TEST_F(BirchWeatherProviderTest, GetWeather) {
  auto* birch_model = Shell::Get()->birch_model();

  WeatherInfo info;
  info.condition_description = "Cloudy";
  info.condition_icon_url = "https://fake-icon-url";
  info.temp_f = 70.0f;
  ambient_backend_controller_->SetWeatherInfo(info);

  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  auto& weather_items = birch_model->GetWeatherForTest();
  ASSERT_EQ(1u, weather_items.size());
  EXPECT_EQ(u"Cloudy", weather_items[0].title());
  EXPECT_FLOAT_EQ(70.f, weather_items[0].temp_f());
  weather_items[0].LoadIcon(base::BindOnce(
      [](const ui::ImageModel& icon, SecondaryIconType secondary_icon_type) {
        EXPECT_FALSE(icon.IsEmpty());
        EXPECT_EQ(secondary_icon_type, SecondaryIconType::kNoIcon);
      }));
}

TEST_F(BirchWeatherProviderTest, GetWeatherUsesChromeOSWeatherClientId) {
  auto* birch_model = Shell::Get()->birch_model();

  // Set up fake weather.
  WeatherInfo info;
  info.condition_description = "Cloudy";
  info.condition_icon_url = "https://fake-icon-url";
  info.temp_f = 70.0f;
  ambient_backend_controller_->SetWeatherInfo(info);

  // Fetch birch data, which includes weather.
  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  // Verify the controller was called with the correct weather client ID.
  std::optional<std::string> weather_client_id =
      ambient_backend_controller_->weather_client_id();
  ASSERT_TRUE(weather_client_id.has_value());
  EXPECT_EQ(*weather_client_id, "chromeos-system-ui");
}

TEST_F(BirchWeatherProviderTest, GetWeatherWaitsForRefreshTokens) {
  auto* birch_model = Shell::Get()->birch_model();
  StubBirchClient birch_client;
  birch_model->SetClientAndInit(&birch_client);

  WeatherInfo info;
  info.condition_description = "Cloudy";
  info.condition_icon_url = "https://fake-icon-url";
  info.temp_f = 70.0f;
  ambient_backend_controller_->SetWeatherInfo(info);

  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  // The provider used the client to wait for refresh tokens.
  EXPECT_TRUE(birch_client.did_wait_for_refresh_tokens());

  // Weather data was fetched.
  auto& weather_items = birch_model->GetWeatherForTest();
  ASSERT_EQ(1u, weather_items.size());
  EXPECT_EQ(u"Cloudy", weather_items[0].title());
  EXPECT_FLOAT_EQ(70.f, weather_items[0].temp_f());
  weather_items[0].LoadIcon(base::BindOnce(
      [](const ui::ImageModel& icon, SecondaryIconType secondary_icon_type) {
        EXPECT_FALSE(icon.IsEmpty());
        EXPECT_EQ(secondary_icon_type, SecondaryIconType::kNoIcon);
      }));

  birch_model->SetClientAndInit(nullptr);
}

TEST_F(BirchWeatherProviderTest, WeatherNotFetchedWhenGeolocationDisabled) {
  auto* birch_model = Shell::Get()->birch_model();

  // Set up fake backend weather.
  WeatherInfo info;
  info.condition_description = "Cloudy";
  info.condition_icon_url = "https://fake-icon-url";
  info.temp_f = 70.0f;
  ambient_backend_controller_->SetWeatherInfo(info);

  // Disable geolocation.
  SimpleGeolocationProvider::GetInstance()->SetGeolocationAccessLevel(
      GeolocationAccessLevel::kDisallowed);

  // Fetch birch data.
  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  // Weather was not fetched.
  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
}

TEST_F(BirchWeatherProviderTest, WeatherNotFetchedForStubUser) {
  auto* birch_model = Shell::Get()->birch_model();

  // Set up fake backend weather.
  WeatherInfo info;
  info.condition_description = "Sunny";
  info.condition_icon_url = "https://fake-icon-url";
  info.temp_f = 72.0f;
  ambient_backend_controller_->SetWeatherInfo(info);

  // Simulate a stub user login.
  ClearLogin();
  SimulateUserLogin(user_manager::StubAccountId());

  // Fetch birch data.
  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  // The weather was not fetched.
  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
}

TEST_F(BirchWeatherProviderTest, WeatherNotFetchedInAfternoon) {
  auto* birch_model = Shell::Get()->birch_model();

  // Set up fake backend weather.
  WeatherInfo info;
  info.condition_description = "Sunny";
  info.condition_icon_url = "https://fake-icon-url";
  info.temp_f = 72.0f;
  ambient_backend_controller_->SetWeatherInfo(info);

  // Simulate afternoon (2 PM).
  SetTestTime(base::Time::Now().LocalMidnight() + base::Hours(14));

  // Fetch birch data.
  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  // The weather was not fetched.
  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
}

TEST_F(BirchWeatherProviderTest, NoWeatherInfo) {
  auto* birch_model = Shell::Get()->birch_model();

  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
}

TEST_F(BirchWeatherProviderTest, WeatherWithNoIcon) {
  auto* birch_model = Shell::Get()->birch_model();

  WeatherInfo info;
  info.condition_description = "Cloudy";
  info.show_celsius = false;
  info.temp_f = 70.0f;
  ambient_backend_controller_->SetWeatherInfo(std::move(info));

  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
}

TEST_F(BirchWeatherProviderTest, WeatherWithInvalidIcon) {
  auto* birch_model = Shell::Get()->birch_model();

  WeatherInfo info;
  info.condition_description = "Cloudy";
  info.condition_icon_url = "<invalid url>";
  info.show_celsius = false;
  info.temp_f = 70.0f;
  ambient_backend_controller_->SetWeatherInfo(std::move(info));

  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  // The item exists and will use the backup icon.
  EXPECT_FALSE(birch_model->GetWeatherForTest().empty());
}

TEST_F(BirchWeatherProviderTest, WeatherIconDownloadFailure) {
  auto* birch_model = Shell::Get()->birch_model();

  WeatherInfo info;
  info.condition_description = "Cloudy";
  info.condition_icon_url = "https://fake_icon_url";
  info.show_celsius = false;
  info.temp_f = 70.0f;
  ambient_backend_controller_->SetWeatherInfo(std::move(info));

  image_downloader_->set_should_fail(true);

  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  // The item exists and will use the backup icon.
  EXPECT_FALSE(birch_model->GetWeatherForTest().empty());
}

TEST_F(BirchWeatherProviderTest, WeatherWithNoTemperature) {
  auto* birch_model = Shell::Get()->birch_model();

  WeatherInfo info;
  info.condition_description = "Cloudy";
  info.condition_icon_url = "https://fake_icon_url";
  info.show_celsius = false;
  ambient_backend_controller_->SetWeatherInfo(std::move(info));

  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
}

TEST_F(BirchWeatherProviderTest, WeatherWithNoDecription) {
  auto* birch_model = Shell::Get()->birch_model();

  WeatherInfo info;
  info.condition_icon_url = "https://fake_icon_url";
  info.show_celsius = false;
  info.temp_f = 70.0f;
  ambient_backend_controller_->SetWeatherInfo(std::move(info));

  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
}

TEST_F(BirchWeatherProviderTest, RefetchWeather) {
  auto* birch_model = Shell::Get()->birch_model();

  WeatherInfo info1;
  info1.condition_description = "Cloudy";
  info1.condition_icon_url = "https://fake-icon-url";
  info1.show_celsius = false;
  info1.temp_f = 70.0f;
  ambient_backend_controller_->SetWeatherInfo(info1);

  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  auto& weather_items = birch_model->GetWeatherForTest();
  ASSERT_EQ(1u, weather_items.size());
  EXPECT_EQ(u"Cloudy", weather_items[0].title());
  EXPECT_FLOAT_EQ(70.f, weather_items[0].temp_f());
  weather_items[0].LoadIcon(base::BindOnce(
      [](const ui::ImageModel& icon, SecondaryIconType secondary_icon_type) {
        EXPECT_FALSE(icon.IsEmpty());
        EXPECT_EQ(secondary_icon_type, SecondaryIconType::kNoIcon);
      }));

  // Ensure the cache isn't used.
  GetWeatherProvider()->ResetCacheForTest();

  WeatherInfo info2;
  info2.condition_description = "Sunny";
  info2.condition_icon_url = "https://fake-icon-url";
  info2.show_celsius = false;
  info2.temp_f = 73.0f;
  ambient_backend_controller_->SetWeatherInfo(info2);

  base::RunLoop run_loop2;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop2.QuitClosure());
  run_loop2.Run();

  auto& updated_weather_items = birch_model->GetWeatherForTest();
  ASSERT_EQ(1u, updated_weather_items.size());
  EXPECT_EQ(u"Sunny", updated_weather_items[0].title());
  EXPECT_FLOAT_EQ(73.f, updated_weather_items[0].temp_f());
  weather_items[0].LoadIcon(base::BindOnce(
      [](const ui::ImageModel& icon, SecondaryIconType secondary_icon_type) {
        EXPECT_FALSE(icon.IsEmpty());
        EXPECT_EQ(secondary_icon_type, SecondaryIconType::kNoIcon);
      }));
}

TEST_F(BirchWeatherProviderTest, RefetchUsesCache) {
  auto* birch_model = Shell::Get()->birch_model();

  // Set up weather.
  WeatherInfo info1;
  info1.condition_description = "Cloudy";
  info1.condition_icon_url = "https://fake-icon-url";
  info1.show_celsius = false;
  info1.temp_f = 70.0f;
  ambient_backend_controller_->SetWeatherInfo(info1);

  // Make an initial fetch.
  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  // The weather from `info1` is fetched.
  auto& weather_items = birch_model->GetWeatherForTest();
  ASSERT_EQ(1u, weather_items.size());
  EXPECT_EQ(u"Cloudy", weather_items[0].title());
  EXPECT_FLOAT_EQ(70.f, weather_items[0].temp_f());

  // Set up different weather.
  WeatherInfo info2;
  info2.condition_description = "Sunny";
  info2.condition_icon_url = "https://fake-icon-url";
  info2.show_celsius = false;
  info2.temp_f = 73.0f;
  ambient_backend_controller_->SetWeatherInfo(info2);

  // Make another request. This will hit the cache and not fetch `info2`.
  base::RunLoop run_loop2;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop2.QuitClosure());
  run_loop2.Run();

  // The data is from `info1` because it came from the cache.
  auto& updated_weather_items = birch_model->GetWeatherForTest();
  ASSERT_EQ(1u, updated_weather_items.size());
  EXPECT_EQ(u"Cloudy", updated_weather_items[0].title());
  EXPECT_FLOAT_EQ(70.f, updated_weather_items[0].temp_f());
}

TEST_F(BirchWeatherProviderTest, RefetchInvalidWeather) {
  auto* birch_model = Shell::Get()->birch_model();

  WeatherInfo info1;
  info1.condition_description = "Cloudy";
  info1.condition_icon_url = "https://fake-icon-url";
  info1.show_celsius = false;
  info1.temp_f = 70.0f;
  ambient_backend_controller_->SetWeatherInfo(info1);

  base::RunLoop run_loop;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop.QuitClosure());
  run_loop.Run();

  auto& weather_items = birch_model->GetWeatherForTest();
  ASSERT_EQ(1u, weather_items.size());
  EXPECT_EQ(u"Cloudy", weather_items[0].title());
  EXPECT_FLOAT_EQ(70.f, weather_items[0].temp_f());
  weather_items[0].LoadIcon(base::BindOnce(
      [](const ui::ImageModel& icon, SecondaryIconType secondary_icon_type) {
        EXPECT_FALSE(icon.IsEmpty());
        EXPECT_EQ(secondary_icon_type, SecondaryIconType::kNoIcon);
      }));

  // Ensure the cache isn't used.
  GetWeatherProvider()->ResetCacheForTest();

  WeatherInfo info2;
  info2.show_celsius = false;
  ambient_backend_controller_->SetWeatherInfo(info2);

  base::RunLoop run_loop2;
  birch_model->RequestBirchDataFetch(/*is_post_login=*/false,
                                     run_loop2.QuitClosure());
  run_loop2.Run();

  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
}

TEST_F(BirchWeatherProviderTest, AllowOneFetchAtATime) {
  auto* birch_model = Shell::Get()->birch_model();
  BirchWeatherProvider provider(birch_model);

  // Set up the ambient controller so it pauses on FetchWeather().
  ambient_backend_controller_->set_run_fetch_weather_callback(false);
  ASSERT_EQ(ambient_backend_controller_->fetch_weather_count(), 0);

  // Make two concurrent weather requests.
  provider.RequestBirchDataFetch();
  provider.RequestBirchDataFetch();

  // The backend only received one request to fetch weather.
  EXPECT_EQ(ambient_backend_controller_->fetch_weather_count(), 1);
}

TEST_F(BirchWeatherProviderTest, DisabledByPolicy) {
  auto* birch_model = Shell::Get()->birch_model();
  BirchWeatherProvider provider(birch_model);

  // Disable weather integration by policy, no weather should be fetched.
  auto* pref_service =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  pref_service->SetList(prefs::kContextualGoogleIntegrationsConfiguration, {});

  provider.RequestBirchDataFetch();
  EXPECT_EQ(ambient_backend_controller_->fetch_weather_count(), 0);

  // Enable weather integration by policy, weather should be fetched.
  base::Value::List enabled_integrations;
  enabled_integrations.Append(prefs::kWeatherIntegrationName);
  pref_service->SetList(prefs::kContextualGoogleIntegrationsConfiguration,
                        std::move(enabled_integrations));

  provider.RequestBirchDataFetch();
  EXPECT_EQ(ambient_backend_controller_->fetch_weather_count(), 1);
}

}  // namespace
}  // namespace ash