chromium/ash/system/geolocation/geolocation_controller_unittest.cc

// Copyright 2021 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/system/geolocation/geolocation_controller.h"

#include <string_view>

#include "ash/constants/ash_pref_names.h"
#include "ash/constants/geolocation_access_level.h"
#include "ash/session/session_controller_impl.h"
#include "ash/session/test_session_controller_client.h"
#include "ash/shell.h"
#include "ash/system/geolocation/geolocation_controller_test_util.h"
#include "ash/system/geolocation/test_geolocation_url_loader_factory.h"
#include "ash/system/privacy_hub/privacy_hub_controller.h"
#include "ash/system/time/time_of_day.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/time_of_day_test_util.h"
#include "ash/test_shell_delegate.h"
#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/simple_test_clock.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "base/timer/mock_timer.h"
#include "chromeos/ash/components/geolocation/simple_geolocation_provider.h"
#include "chromeos/dbus/power/fake_power_manager_client.h"
#include "components/prefs/pref_service.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "third_party/icu/source/i18n/unicode/timezone.h"

namespace ash {

namespace {

constexpr char kUser1Email[] = "user1@geolocation";
constexpr char kUser2Email[] = "user2@geolocation";

// Sets of test longitudes/latitude and the corresponding sunrise/sunset times
// for testing. They all assume the clock's current time is `kTestNow`.
constexpr std::string_view kTestNow = "23 Dec 2021 12:00:00";

constexpr double kTestLatitude1 = 23.5;
constexpr double kTestLongitude1 = 35.88;
constexpr std::string_view kTestSunriseTime1 = "23 Dec 2021 04:14:36.626";
constexpr std::string_view kTestSunsetTime1 = "23 Dec 2021 14:59:58.459";

constexpr double kTestLatitude2 = 37.5;
constexpr double kTestLongitude2 = -100.5;
constexpr std::string_view kTestSunriseTime2 = "23 Dec 2021 13:55:13.306";
constexpr std::string_view kTestSunsetTime2 = "23 Dec 2021 23:33:46.855";

constexpr SimpleGeoposition kSanJoseGeoposition = {37.335480, -121.893028};

constexpr SimpleGeoposition kSanFranciscoGeoposition = {37.773972, -122.431297};

constexpr SimpleGeoposition kNewYorkGeoposition = {40.730610, -73.935242};

// Kiruna, Sweden
constexpr SimpleGeoposition kNoDarknessGeoposition = {67.855800, 20.225282};

// Belgrano II Base, Antarctica
constexpr SimpleGeoposition kNoDaylightGeoposition = {-77.87361, -34.62745};

constexpr char kNoDaylightDarknessTimestamp[] = "07 Jun 2023 20:30:00.000";

constexpr int kDefaultSunsetTimeOffsetMinutes = 18 * 60;
constexpr int kDefaultSunriseTimeOffsetMinutes = 6 * 60;

// Constructs a TimeZone object from the given `timezone_id`.
std::unique_ptr<icu::TimeZone> CreateTimezone(const char* timezone_id) {
  return base::WrapUnique(icu::TimeZone::createTimeZone(
      icu::UnicodeString(timezone_id, -1, US_INV)));
}

std::u16string GetTimezoneId(const icu::TimeZone& timezone) {
  return system::TimezoneSettings::GetTimezoneID(timezone);
}

base::Time ToUTCTime(std::string_view utc_time_str) {
  base::Time time;
  CHECK(base::Time::FromUTCString(std::string(utc_time_str).c_str(), &time))
      << "Invalid UTC time string specified: " << utc_time_str;
  return time;
}

class FakeGeolocationController : public GeolocationController {
 public:
  explicit FakeGeolocationController(
      SimpleGeolocationProvider* geolocation_provider)
      : GeolocationController(geolocation_provider) {}

  // Proxy method to call the `OnGeoposition()` callback directly, without
  // waiting for the server response. Need this to test scheduler behavior.
  void ImitateGeopositionReceived() {
    Geoposition fake_pos;
    fake_pos.latitude = kTestLatitude1;
    fake_pos.longitude = kTestLongitude1;
    fake_pos.status = Geoposition::STATUS_OK;
    fake_pos.accuracy = 10;
    fake_pos.timestamp = base::Time::Now();

    GeolocationController::OnGeoposition(fake_pos, false, base::Seconds(1));
  }

  // TODO(b/286233027): Override `RequestGeolocation()` to fake the server
  // communication.
};

// Base test fixture.
class GeolocationControllerTest : public AshTestBase {
 public:
  GeolocationControllerTest()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
  GeolocationControllerTest(const GeolocationControllerTest&) = delete;
  GeolocationControllerTest& operator=(const GeolocationControllerTest&) =
      delete;

  ~GeolocationControllerTest() override = default;

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();
    CreateTestUserSessions();
    // `SimpleGeolocationProvider` is initialized by `AshTestHelper`.
    controller_ = std::make_unique<FakeGeolocationController>(
        SimpleGeolocationProvider::GetInstance());

    test_clock_.SetNow(base::Time::Now());
    controller_->SetClockForTesting(&test_clock_);
    timer_ptr_ = controller_->GetTimerForTesting();

    // Prepare a valid geoposition.
    Geoposition position;
    position.latitude = 32.0;
    position.longitude = 31.0;
    position.status = Geoposition::STATUS_OK;
    position.accuracy = 10;
    position.timestamp = base::Time::Now();
    SetServerPosition(position);
  }

  // AshTestBase:
  void TearDown() override {
    controller_.reset();
    AshTestBase::TearDown();
  }

  FakeGeolocationController* controller() const { return controller_.get(); }
  base::SimpleTestClock* test_clock() { return &test_clock_; }
  base::OneShotTimer* timer_ptr() const { return timer_ptr_; }
  const Geoposition& position() const { return position_; }

  PrefService* user1_pref_service() {
    return Shell::Get()->session_controller()->GetUserPrefServiceForUser(
        AccountId::FromUserEmail(kUser1Email));
  }

  PrefService* user2_pref_service() {
    return Shell::Get()->session_controller()->GetUserPrefServiceForUser(
        AccountId::FromUserEmail(kUser2Email));
  }

  void CreateTestUserSessions() {
    GetSessionControllerClient()->Reset();
    GetSessionControllerClient()->AddUserSession(kUser1Email);
    GetSessionControllerClient()->AddUserSession(kUser2Email);
  }

  void SwitchActiveUser(const std::string& email) {
    GetSessionControllerClient()->SwitchActiveUser(
        AccountId::FromUserEmail(email));
  }

  // Fires the timer of the scheduler to request geoposition and wait for all
  // observers to receive the latest geoposition from the server.
  void FireTimerToFetchGeoposition() {
    GeopositionResponsesWaiter waiter(controller());
    EXPECT_TRUE(timer_ptr()->IsRunning());
    // Fast forward the scheduler to reach the time when the controller
    // requests for geoposition from the server in
    // `GeolocationController::RequestGeoposition`.
    timer_ptr_->FireNow();
    // Waits for the observers to receive the geoposition from the server.
    waiter.Wait();
  }

  // Sets the geoposition to be returned from the `factory_` upon the
  // `GeolocationController` request.
  void SetServerPosition(const Geoposition& position) {
    position_ = position;
    auto* factory = static_cast<TestGeolocationUrlLoaderFactory*>(
        SimpleGeolocationProvider::GetInstance()
            ->GetSharedURLLoaderFactoryForTesting());
    factory->ClearResponses();
    factory->set_position(position_);
  }

  void UpdateUserGeolocationPermission(GeolocationAccessLevel access_level) {
    SimpleGeolocationProvider::GetInstance()->SetGeolocationAccessLevel(
        access_level);
  }

 private:
  std::unique_ptr<FakeGeolocationController> controller_;
  base::SimpleTestClock test_clock_;
  raw_ptr<base::OneShotTimer, DanglingUntriaged> timer_ptr_;
  Geoposition position_;
};

// Tests adding and removing an observer should request and stop receiving
// a position update.
TEST_F(GeolocationControllerTest, Observer) {
  EXPECT_FALSE(timer_ptr()->IsRunning());

  // Add an observer should start the timer requesting the geoposition.
  GeolocationControllerObserver observer;
  controller()->AddObserver(&observer);
  FireTimerToFetchGeoposition();
  EXPECT_EQ(1, observer.position_received_num());

  // Check that the timer fires another schedule after a successful request.
  EXPECT_TRUE(timer_ptr()->IsRunning());

  // Removing an observer should stop the timer.
  controller()->RemoveObserver(&observer);
  EXPECT_FALSE(timer_ptr()->IsRunning());
  EXPECT_EQ(1, observer.position_received_num());
}

// Tests adding and removing observer and make sure that only observing ones
// receive the position updates.
TEST_F(GeolocationControllerTest, MultipleObservers) {
  EXPECT_FALSE(timer_ptr()->IsRunning());

  // Add an observer should start the timer requesting for the first
  // geoposition request.
  GeolocationControllerObserver observer1;
  controller()->AddObserver(&observer1);
  FireTimerToFetchGeoposition();
  EXPECT_EQ(1, observer1.position_received_num());
  EXPECT_TRUE(timer_ptr()->IsRunning());

  // Since `OnGeoposition()` handling a geoposition update always schedule
  // the next geoposition request, the timer should keep running and
  // update position periodically.
  FireTimerToFetchGeoposition();
  EXPECT_EQ(2, observer1.position_received_num());
  EXPECT_TRUE(timer_ptr()->IsRunning());

  // Adding `observer2` should not interrupt the request flow. Check that both
  // observers receive the new position.
  GeolocationControllerObserver observer2;
  controller()->AddObserver(&observer2);
  FireTimerToFetchGeoposition();
  EXPECT_EQ(3, observer1.position_received_num());
  EXPECT_EQ(1, observer2.position_received_num());
  EXPECT_TRUE(timer_ptr()->IsRunning());

  // Remove `observer1` and make sure that the timer is still running.
  // Only `observer2` should receive the new position.
  controller()->RemoveObserver(&observer1);
  FireTimerToFetchGeoposition();
  EXPECT_EQ(3, observer1.position_received_num());
  EXPECT_EQ(2, observer2.position_received_num());
  EXPECT_TRUE(timer_ptr()->IsRunning());

  // Removing `observer2` should stop the timer. The request count should
  // not change.
  controller()->RemoveObserver(&observer2);
  EXPECT_FALSE(timer_ptr()->IsRunning());
  EXPECT_EQ(3, observer1.position_received_num());
  EXPECT_EQ(2, observer2.position_received_num());
}

// Tests that controller only pushes valid positions.
TEST_F(GeolocationControllerTest, InvalidPositions) {
  GeolocationControllerObserver observer;
  // Update to an invalid position
  Geoposition invalid_position(position());
  invalid_position.error_code = 10;
  SetServerPosition(invalid_position);
  EXPECT_FALSE(timer_ptr()->IsRunning());
  controller()->AddObserver(&observer);

  // If the position is invalid, the controller won't push the geoposition
  // update to its observers.
  EXPECT_TRUE(timer_ptr()->IsRunning());
  timer_ptr()->FireNow();
  // Wait for the request and response to finish.
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(0, observer.position_received_num());
  // With error response, the server will retry with another timer which we
  // have no control over, so `mock_timer_ptr_` will not be running (refers to
  // `SimpleGeolocationRequest::Retry()` for more detail).
  EXPECT_FALSE(timer_ptr()->IsRunning());
}

// Tests that timezone changes result.
TEST_F(GeolocationControllerTest, TimezoneChanges) {
  EXPECT_FALSE(timer_ptr()->IsRunning());
  controller()->SetCurrentTimezoneIdForTesting(u"America/Los_Angeles");

  // Add an observer.
  GeolocationControllerObserver observer;
  controller()->AddObserver(&observer);
  EXPECT_EQ(0, observer.position_received_num());
  FireTimerToFetchGeoposition();
  EXPECT_EQ(1, observer.position_received_num());
  EXPECT_EQ(u"America/Los_Angeles", controller()->current_timezone_id());
  EXPECT_TRUE(timer_ptr()->IsRunning());

  // A new timezone results in new geoposition request.
  auto timezone = CreateTimezone("Asia/Tokyo");

  controller()->TimezoneChanged(*timezone);
  FireTimerToFetchGeoposition();
  EXPECT_EQ(2, observer.position_received_num());
  EXPECT_EQ(GetTimezoneId(*timezone), controller()->current_timezone_id());
  EXPECT_TRUE(timer_ptr()->IsRunning());
}

TEST_F(GeolocationControllerTest, SystemGeolocationPermissionChanges) {
  EXPECT_FALSE(timer_ptr()->IsRunning());

  GeolocationControllerObserver observer;
  controller()->AddObserver(&observer);
  EXPECT_EQ(0, observer.position_received_num());

  FireTimerToFetchGeoposition();
  EXPECT_EQ(1, observer.position_received_num());
  EXPECT_TRUE(timer_ptr()->IsRunning());

  // Block geolocation usage to apps only, shouldn't affect
  // `GeolocationController`.
  UpdateUserGeolocationPermission(
      GeolocationAccessLevel::kOnlyAllowedForSystem);
  EXPECT_TRUE(timer_ptr()->IsRunning());

  // Disable system geo permission. Scheduling should stop.
  UpdateUserGeolocationPermission(GeolocationAccessLevel::kDisallowed);
  EXPECT_FALSE(timer_ptr()->IsRunning());

  // Re-enabling the system geo permission, should resume scheduling.
  UpdateUserGeolocationPermission(GeolocationAccessLevel::kAllowed);
  EXPECT_TRUE(timer_ptr()->IsRunning());
}

TEST_F(GeolocationControllerTest, StopSchedulingWhileResponseIsComing) {
  EXPECT_FALSE(timer_ptr()->IsRunning());

  // This will start scheduling.
  GeolocationControllerObserver observer;
  controller()->AddObserver(&observer);
  EXPECT_TRUE(timer_ptr()->IsRunning());

  // Fire Geolocation request.
  timer_ptr()->FireNow();
  EXPECT_FALSE(timer_ptr()->IsRunning());

  // Disable user geolocation permission, this should stop scheduling.
  UpdateUserGeolocationPermission(GeolocationAccessLevel::kDisallowed);
  EXPECT_FALSE(timer_ptr()->IsRunning());

  // Simulate server response and check it didn't resume scheduling.
  controller()->ImitateGeopositionReceived();
  EXPECT_FALSE(timer_ptr()->IsRunning());

  // Re-enable user geolocation permission, this should resume scheduling.
  UpdateUserGeolocationPermission(GeolocationAccessLevel::kAllowed);
  EXPECT_TRUE(timer_ptr()->IsRunning());

  // Unsubscribe the observer before being destroyed.
  controller()->RemoveObserver(&observer);
}

TEST_F(GeolocationControllerTest, StopSchedulingWhenObserverListIsEmpty) {
  EXPECT_FALSE(timer_ptr()->IsRunning());

  // Add the first observer. This will kick off scheduling.
  GeolocationControllerObserver observer;
  controller()->AddObserver(&observer);

  // Fire Geolocation request.
  timer_ptr()->FireNow();

  // Unsubscribe the only observer.
  controller()->RemoveObserver(&observer);

  // Simulate the server response and check it didn't resume the scheduler.
  controller()->ImitateGeopositionReceived();
  EXPECT_FALSE(timer_ptr()->IsRunning());

  // Add the observer back, scheduling should resume.
  controller()->AddObserver(&observer);
  EXPECT_TRUE(timer_ptr()->IsRunning());

  // Unsubscribe the observer before being destroyed.
  controller()->RemoveObserver(&observer);
}

// Tests obtaining sunset/sunrise time when there is no valid geoposition, for
// example, due to lack of connectivity.
TEST_F(GeolocationControllerTest, SunsetSunriseDefault) {
  // If geoposition is unset, the controller should return the default sunset
  // and sunrise time .
  EXPECT_EQ(controller()->GetSunsetTime(),
            ToTimeToday(TimeOfDay(kDefaultSunsetTimeOffsetMinutes)));
  EXPECT_EQ(controller()->GetSunriseTime(),
            ToTimeToday(TimeOfDay(kDefaultSunriseTimeOffsetMinutes)));
}

// Tests the behavior when there is a valid geoposition, sunrise and sunset
// times are calculated correctly.
TEST_F(GeolocationControllerTest, GetSunRiseSet) {
  test_clock()->SetNow(ToUTCTime(kTestNow));

  // Add an observer and make sure that sunset and sunrise time are not
  // updated until the timer is fired.
  GeolocationControllerObserver observer1;
  controller()->AddObserver(&observer1);
  EXPECT_TRUE(timer_ptr()->IsRunning());
  EXPECT_NE(controller()->GetSunsetTime(), ToUTCTime(kTestSunsetTime1));
  EXPECT_NE(controller()->GetSunriseTime(), ToUTCTime(kTestSunriseTime1));
  EXPECT_EQ(0, observer1.position_received_num());

  // Prepare a valid geoposition.
  Geoposition position;
  position.latitude = kTestLatitude1;
  position.longitude = kTestLongitude1;
  position.status = Geoposition::STATUS_OK;
  position.accuracy = 10;
  position.timestamp = ToUTCTime(kTestNow);

  // Test that after sending the new position, sunrise and sunset time are
  // updated correctly.
  SetServerPosition(position);
  FireTimerToFetchGeoposition();
  EXPECT_EQ(1, observer1.position_received_num());
  EXPECT_EQ(controller()->GetSunsetTime(), ToUTCTime(kTestSunsetTime1));
  EXPECT_EQ(controller()->GetSunriseTime(), ToUTCTime(kTestSunriseTime1));
  EXPECT_TRUE(timer_ptr()->IsRunning());
}

// Tests that when there is a geoposition with 24 hours of daylight or darkness,
// sunrise and sunset times honor the API.
TEST_F(GeolocationControllerTest, GetSunRiseSetWithAllDaylightOrDarkness) {
  test_clock()->SetNow(ToUTCTime(kNoDaylightDarknessTimestamp));

  Geoposition position;
  position.latitude = kNoDarknessGeoposition.latitude;
  position.longitude = kNoDarknessGeoposition.longitude;
  position.status = Geoposition::STATUS_OK;
  position.accuracy = 10;
  position.timestamp = test_clock()->Now();

  // Test that after sending the new position, sunrise and sunset time are
  // updated correctly.
  SetServerPosition(position);
  FireTimerToFetchGeoposition();
  EXPECT_EQ(controller()->GetSunsetTime(),
            GeolocationController::kNoSunRiseSet);
  EXPECT_EQ(controller()->GetSunriseTime(),
            GeolocationController::kNoSunRiseSet);

  position.latitude = kNoDaylightGeoposition.latitude;
  position.longitude = kNoDaylightGeoposition.longitude;

  // Test that after sending the new position, sunrise and sunset time are
  // updated correctly.
  SetServerPosition(position);
  FireTimerToFetchGeoposition();
  EXPECT_EQ(controller()->GetSunsetTime(),
            GeolocationController::kNoSunRiseSet);
  EXPECT_EQ(controller()->GetSunriseTime(),
            GeolocationController::kNoSunRiseSet);
}

// Tests that if device sleeps more than a day, the geoposition is fetched
// instantly.
TEST_F(GeolocationControllerTest, RequestGeopositionAfterSuspend) {
  const base::TimeDelta zero_duration = base::Seconds(0);
  auto* power_manager_client = chromeos::FakePowerManagerClient::Get();
  const base::TimeDelta next_request_delay_after_success = base::Days(1);
  // Add an observer. Adding the first observer automatically requests a
  // geoposition instantly.
  GeolocationControllerObserver observer;
  controller()->AddObserver(&observer);
  EXPECT_EQ(0, observer.position_received_num());
  EXPECT_EQ(zero_duration, timer_ptr()->GetCurrentDelay());

  // Fetch that instant request to make the next request has a delay becomes
  // `kNextRequestDelayAfterSuccess`, i.e. `next_request_delay_after_success`,
  FireTimerToFetchGeoposition();
  EXPECT_EQ(next_request_delay_after_success, timer_ptr()->GetCurrentDelay());
  EXPECT_EQ(1, observer.position_received_num());

  // Suspend the device for a day and wake the device.
  power_manager_client->SendSuspendImminent(
      power_manager::SuspendImminent::Reason::SuspendImminent_Reason_IDLE);
  power_manager_client->SendSuspendDone(base::Days(1));
  // Test that after waking up from 1-day suspension, the controller request a
  // new geoposition instantly.
  EXPECT_EQ(zero_duration, timer_ptr()->GetCurrentDelay());
  FireTimerToFetchGeoposition();
  EXPECT_EQ(2, observer.position_received_num());
  EXPECT_EQ(next_request_delay_after_success, timer_ptr()->GetCurrentDelay());

  // Suspend the device for less than a day.
  power_manager_client->SendSuspendImminent(
      power_manager::SuspendImminent::Reason::SuspendImminent_Reason_IDLE);
  // Test that after waking up from 2-hr suspension, the controller continues
  // the old geoposition request with the same delay.
  power_manager_client->SendSuspendDone(base::Hours(2));
  EXPECT_EQ(next_request_delay_after_success, timer_ptr()->GetCurrentDelay());
}

// Tests the behavior when there is no valid geoposition for example due to lack
// of connectivity.
TEST_F(GeolocationControllerTest, AbsentValidGeoposition) {
  test_clock()->SetNow(ToUTCTime(kTestNow));

  // Initially, no values are stored in either of the two users' prefs.
  ASSERT_FALSE(user1_pref_service()->HasPrefPath(
      prefs::kDeviceGeolocationCachedLatitude));
  ASSERT_FALSE(user1_pref_service()->HasPrefPath(
      prefs::kDeviceGeolocationCachedLongitude));
  ASSERT_FALSE(user2_pref_service()->HasPrefPath(
      prefs::kDeviceGeolocationCachedLatitude));
  ASSERT_FALSE(user2_pref_service()->HasPrefPath(
      prefs::kDeviceGeolocationCachedLongitude));

  // Store fake geoposition in user 2's prefs.
  user2_pref_service()->SetDouble(prefs::kDeviceGeolocationCachedLatitude,
                                  kTestLatitude1);
  user2_pref_service()->SetDouble(prefs::kDeviceGeolocationCachedLongitude,
                                  kTestLongitude1);

  // Switch to user 2 and expect that geoposition is loaded from pref.
  SwitchActiveUser(kUser2Email);
  EXPECT_EQ(controller()->GetSunsetTime(), ToUTCTime(kTestSunsetTime1));
  EXPECT_EQ(controller()->GetSunriseTime(), ToUTCTime(kTestSunriseTime1));

  // Switching to user 1 should ignore the current geoposition since it's
  // a cached value from user 2's prefs rather than a newly-updated value.
  SwitchActiveUser(kUser1Email);
  EXPECT_EQ(
      controller()->GetSunsetTime(),
      ToTimeToday(
          TimeOfDay(kDefaultSunsetTimeOffsetMinutes).SetClock(test_clock())));
  EXPECT_EQ(
      controller()->GetSunriseTime(),
      ToTimeToday(
          TimeOfDay(kDefaultSunriseTimeOffsetMinutes).SetClock(test_clock())));

  // Now simulate receiving a live geoposition update.
  Geoposition position;
  position.latitude = kTestLatitude1;
  position.longitude = kTestLongitude1;
  position.status = Geoposition::STATUS_OK;
  position.accuracy = 10;
  position.timestamp = ToUTCTime(kTestNow);
  SetServerPosition(position);
  FireTimerToFetchGeoposition();
  EXPECT_EQ(controller()->GetSunsetTime(), ToUTCTime(kTestSunsetTime1));
  EXPECT_EQ(controller()->GetSunriseTime(), ToUTCTime(kTestSunriseTime1));

  // Update user 2's prefs with different geoposition.
  user2_pref_service()->SetDouble(prefs::kDeviceGeolocationCachedLatitude,
                                  kTestLatitude2);
  user2_pref_service()->SetDouble(prefs::kDeviceGeolocationCachedLongitude,
                                  kTestLongitude2);

  // Now switching to user 2 should completely ignore their cached geopsoition,
  // since from now on we have a valid newly-retrieved value.
  SwitchActiveUser(kUser2Email);
  EXPECT_EQ(controller()->GetSunsetTime(), ToUTCTime(kTestSunsetTime1));
  EXPECT_EQ(controller()->GetSunriseTime(), ToUTCTime(kTestSunriseTime1));

  // Clear all cached geoposition prefs for all users, just to make sure getting
  // a new geoposition will persist it for all users not just the active one.
  user1_pref_service()->ClearPref(prefs::kDeviceGeolocationCachedLatitude);
  user1_pref_service()->ClearPref(prefs::kDeviceGeolocationCachedLongitude);
  user2_pref_service()->ClearPref(prefs::kDeviceGeolocationCachedLatitude);
  user2_pref_service()->ClearPref(prefs::kDeviceGeolocationCachedLongitude);

  // Now simulate receiving another live geoposition update.
  position.latitude = kTestLatitude2;
  position.longitude = kTestLongitude2;
  SetServerPosition(position);
  FireTimerToFetchGeoposition();
  EXPECT_EQ(controller()->GetSunsetTime(), ToUTCTime(kTestSunsetTime2));
  EXPECT_EQ(controller()->GetSunriseTime(), ToUTCTime(kTestSunriseTime2));
  EXPECT_EQ(kTestLatitude2, user1_pref_service()->GetDouble(
                                prefs::kDeviceGeolocationCachedLatitude));
  EXPECT_EQ(kTestLongitude2, user1_pref_service()->GetDouble(
                                 prefs::kDeviceGeolocationCachedLongitude));
  EXPECT_EQ(kTestLatitude2, user2_pref_service()->GetDouble(
                                prefs::kDeviceGeolocationCachedLatitude));
  EXPECT_EQ(kTestLongitude2, user2_pref_service()->GetDouble(
                                 prefs::kDeviceGeolocationCachedLongitude));
}

// Tests that the `possible_change_in_timezone` is correct.
TEST_F(GeolocationControllerTest, ObserverPossibleChangeInTimezone) {
  test_clock()->SetNow(ToUTCTime(kTestNow));

  GeolocationControllerObserver observer;
  controller()->AddObserver(&observer);

  Geoposition position;
  position.latitude = kSanJoseGeoposition.latitude;
  position.longitude = kSanJoseGeoposition.longitude;
  position.status = Geoposition::STATUS_OK;
  position.accuracy = 10;
  position.timestamp = test_clock()->Now();
  SetServerPosition(position);
  FireTimerToFetchGeoposition();
  // First geoposition should always count as a possible change.
  ASSERT_EQ(observer.position_received_num(), 1);
  EXPECT_TRUE(observer.possible_change_in_timezone());

  position.latitude = kSanFranciscoGeoposition.latitude;
  position.longitude = kSanFranciscoGeoposition.longitude;
  SetServerPosition(position);
  FireTimerToFetchGeoposition();
  ASSERT_EQ(observer.position_received_num(), 2);
  EXPECT_FALSE(observer.possible_change_in_timezone());

  position.latitude = kNewYorkGeoposition.latitude;
  position.longitude = kNewYorkGeoposition.longitude;
  SetServerPosition(position);
  FireTimerToFetchGeoposition();
  ASSERT_EQ(observer.position_received_num(), 3);
  EXPECT_TRUE(observer.possible_change_in_timezone());

  controller()->RemoveObserver(&observer);
}

// Tests that the `possible_change_in_timezone` is correct when areas with no
// daylight/darkness are involved.
TEST_F(GeolocationControllerTest,
       ObserverPossibleChangeInTimezoneNoDaylightDarkness) {
  test_clock()->SetNow(ToUTCTime(kNoDaylightDarknessTimestamp));

  Geoposition position;
  position.status = Geoposition::STATUS_OK;
  position.accuracy = 10;
  position.timestamp = test_clock()->Now();

  GeolocationControllerObserver observer;
  controller()->AddObserver(&observer);
  int expected_position_received_num = 1;
  const auto test_new_geoposition = [this, &position,
                                     &expected_position_received_num,
                                     &observer](
                                        const SimpleGeoposition& new_lat_long) {
    position.latitude = new_lat_long.latitude;
    position.longitude = new_lat_long.longitude;
    SetServerPosition(position);
    FireTimerToFetchGeoposition();
    ASSERT_EQ(observer.position_received_num(), expected_position_received_num);
    EXPECT_TRUE(observer.possible_change_in_timezone());
    expected_position_received_num++;
  };

  test_new_geoposition(kSanJoseGeoposition);
  test_new_geoposition(kNoDarknessGeoposition);
  test_new_geoposition(kSanFranciscoGeoposition);
  test_new_geoposition(kNoDaylightGeoposition);
  test_new_geoposition(kNoDarknessGeoposition);

  controller()->RemoveObserver(&observer);
}

}  // namespace

}  // namespace ash