| // 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 "chromecast/net/connectivity_checker_impl.h" |
| |
| #include <memory> |
| |
| #include "base/memory/scoped_refptr.h" |
| #include "base/run_loop.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.h" |
| #include "chromecast/base/metrics/mock_cast_metrics_helper.h" |
| #include "chromecast/net/fake_shared_url_loader_factory.h" |
| #include "net/http/http_status_code.h" |
| #include "services/network/public/cpp/network_connection_tracker.h" |
| #include "services/network/public/cpp/url_loader_completion_status.h" |
| #include "services/network/public/mojom/network_change_manager.mojom-shared.h" |
| #include "services/network/public/mojom/url_response_head.mojom-forward.h" |
| #include "services/network/test/test_network_connection_tracker.h" |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "services/network/test/test_utils.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| namespace chromecast { |
| |
| using ::testing::InvokeWithoutArgs; |
| using ::testing::NiceMock; |
| |
| constexpr const char* kDefaultConnectivityCheckUrls[] = { |
| kDefaultConnectivityCheckUrl, |
| kHttpConnectivityCheckUrl, |
| }; |
| |
| // Number of consecutive connectivity check errors before status is changed |
| // to offline. |
| const unsigned int kNumErrorsToNotifyOffline = 3; |
| |
| // Most tests use the TestNetworkConnectionChecker from |
| // services/network/test/test_network_connection_tracker.h, but some tests |
| // require testing how many times GetConnectionType() is called by |
| // ConnectivityCheckerImpl. For these tests, we use a fake that records the |
| // number of invocations and otherwise has the default behavior. |
| class FakeNetworkConnectionTracker : public network::NetworkConnectionTracker { |
| public: |
| // Spoof a valid connection type. |
| bool GetConnectionType(network::mojom::ConnectionType* type, |
| ConnectionTypeCallback callback) override { |
| check_counter_++; |
| *type = network::mojom::ConnectionType::CONNECTION_UNKNOWN; |
| return true; |
| } |
| |
| void NotifyNetworkTypeChanged(network::mojom::ConnectionType type) { |
| OnNetworkChanged(type); |
| } |
| |
| unsigned int check_counter() const { return check_counter_; } |
| |
| private: |
| // To memorize how many times GetConnectionType() called by checker |
| unsigned int check_counter_ = 0; |
| }; |
| |
| class ConnectivityCheckPeriods { |
| public: |
| ConnectivityCheckPeriods(base::TimeDelta disconnected_check_period, |
| base::TimeDelta connected_check_period) |
| : disconnected_check_period_(disconnected_check_period), |
| connected_check_period_(connected_check_period) {} |
| |
| static const ConnectivityCheckPeriods Empty() { return empty_; } |
| |
| bool IsEmpty() { |
| return disconnected_check_period_ == empty_.disconnected_check_period_ && |
| connected_check_period_ == empty_.connected_check_period_; |
| } |
| |
| const base::TimeDelta disconnected_check_period_; |
| const base::TimeDelta connected_check_period_; |
| |
| private: |
| // empty object: use minimum and negative TimeDeltas for empty object. |
| static const ConnectivityCheckPeriods empty_; |
| }; |
| |
| const ConnectivityCheckPeriods ConnectivityCheckPeriods::empty_ = |
| ConnectivityCheckPeriods(base::TimeDelta::Min(), base::TimeDelta::Min()); |
| |
| std::ostream& operator<<(std::ostream& out, const ConnectivityCheckPeriods& x) { |
| return out << "{disconnected_check_period: " << x.disconnected_check_period_ |
| << ", connected_check_period: " << x.connected_check_period_ |
| << "}"; |
| } |
| |
| template <typename NetworkConnectivityCheckerT> |
| class ConnectivityCheckerImplBaseTest : public ::testing::Test { |
| public: |
| explicit ConnectivityCheckerImplBaseTest( |
| std::unique_ptr<NetworkConnectivityCheckerT> tracker, |
| ConnectivityCheckPeriods check_periods = |
| ConnectivityCheckPeriods::Empty(), |
| std::unique_ptr<FakePendingSharedURLLoaderFactory> pending_factory = |
| std::make_unique<FakePendingSharedURLLoaderFactory>()) |
| : tracker_(std::move(tracker)), |
| fake_shared_url_loader_factory_( |
| pending_factory->fake_shared_url_loader_factory()), |
| checker_(check_periods.IsEmpty() |
| ? ConnectivityCheckerImpl::Create( |
| task_environment_.GetMainThreadTaskRunner(), |
| std::move(pending_factory), |
| tracker_.get(), |
| /* time_sync_tracker */ nullptr) |
| : ConnectivityCheckerImpl::Create( |
| task_environment_.GetMainThreadTaskRunner(), |
| std::move(pending_factory), |
| tracker_.get(), |
| check_periods.disconnected_check_period_, |
| check_periods.connected_check_period_, |
| /* time_sync_tracker */ nullptr)) { |
| checker_->SetCastMetricsHelperForTesting(&cast_metrics_helper_); |
| } |
| |
| void SetUp() final { |
| // Run pending initialization tasks. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| void TearDown() final { test_url_loader_factory().ClearResponses(); } |
| |
| protected: |
| void SetResponsesWithStatusCode(net::HttpStatusCode status) { |
| for (const char* url : kDefaultConnectivityCheckUrls) { |
| test_url_loader_factory().AddResponse(url, /*content=*/"", status); |
| } |
| } |
| |
| void ConnectAndCheck() { |
| SetResponsesWithStatusCode(kConnectivitySuccessStatusCode); |
| checker_->Check(); |
| base::RunLoop().RunUntilIdle(); |
| test_url_loader_factory().ClearResponses(); |
| } |
| |
| void DisconnectAndCheck() { |
| SetResponsesWithStatusCode(net::HTTP_INTERNAL_SERVER_ERROR); |
| checker_->Check(); |
| base::RunLoop().RunUntilIdle(); |
| test_url_loader_factory().ClearResponses(); |
| } |
| |
| void CheckAndExpectRecordedError( |
| ConnectivityCheckerImpl::ErrorType error_type) { |
| base::RunLoop run_loop; |
| EXPECT_CALL(cast_metrics_helper_, |
| RecordEventWithValue("Network.ConnectivityChecking.ErrorType", |
| static_cast<int>(error_type))) |
| .WillOnce( |
| InvokeWithoutArgs([quit = run_loop.QuitClosure()] { quit.Run(); })); |
| checker_->Check(); |
| run_loop.Run(); |
| } |
| |
| network::TestURLLoaderFactory& test_url_loader_factory() { |
| return fake_shared_url_loader_factory_->test_url_loader_factory(); |
| } |
| |
| base::test::SingleThreadTaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME, |
| }; |
| std::unique_ptr<NetworkConnectivityCheckerT> tracker_; |
| scoped_refptr<FakeSharedURLLoaderFactory> fake_shared_url_loader_factory_; |
| NiceMock<metrics::MockCastMetricsHelper> cast_metrics_helper_; |
| scoped_refptr<ConnectivityCheckerImpl> checker_; |
| }; |
| |
| class ConnectivityCheckerImplTest : public ConnectivityCheckerImplBaseTest< |
| network::TestNetworkConnectionTracker> { |
| public: |
| ConnectivityCheckerImplTest() |
| : ConnectivityCheckerImplBaseTest<network::TestNetworkConnectionTracker>( |
| network::TestNetworkConnectionTracker::CreateInstance()) {} |
| }; |
| |
| TEST_F(ConnectivityCheckerImplTest, StartsDisconnected) { |
| EXPECT_FALSE(checker_->Connected()); |
| } |
| |
| TEST_F(ConnectivityCheckerImplTest, DetectsConnected) { |
| tracker_->SetConnectionType( |
| network::mojom::ConnectionType::CONNECTION_UNKNOWN); |
| ConnectAndCheck(); |
| EXPECT_TRUE(checker_->Connected()); |
| } |
| |
| class ConnectivityCheckerImplTestParameterized |
| : public ConnectivityCheckerImplTest, |
| public ::testing::WithParamInterface<net::HttpStatusCode> {}; |
| |
| TEST_P(ConnectivityCheckerImplTestParameterized, |
| RecordsDisconnectDueToBadHttpStatus) { |
| tracker_->SetConnectionType( |
| network::mojom::ConnectionType::CONNECTION_UNKNOWN); |
| ConnectAndCheck(); |
| SetResponsesWithStatusCode(GetParam()); |
| CheckAndExpectRecordedError( |
| ConnectivityCheckerImpl::ErrorType::BAD_HTTP_STATUS); |
| } |
| |
| // Test 3xx, 4xx, 5xx responses. |
| INSTANTIATE_TEST_SUITE_P(ConnectivityCheckerImplTestBadHttpStatus, |
| ConnectivityCheckerImplTestParameterized, |
| ::testing::Values(net::HTTP_TEMPORARY_REDIRECT, |
| net::HTTP_BAD_REQUEST, |
| net::HTTP_INTERNAL_SERVER_ERROR)); |
| |
| class ConnectivityCheckerImplTestPeriodParameterized |
| : public ConnectivityCheckerImplBaseTest< |
| network::TestNetworkConnectionTracker>, |
| // disconnected probe period |
| public ::testing::WithParamInterface<ConnectivityCheckPeriods> { |
| public: |
| ConnectivityCheckerImplTestPeriodParameterized() |
| : ConnectivityCheckerImplBaseTest<network::TestNetworkConnectionTracker>( |
| network::TestNetworkConnectionTracker::CreateInstance(), |
| GetParam()) {} |
| }; |
| |
| TEST_P(ConnectivityCheckerImplTestPeriodParameterized, |
| CheckWithCustomizedPeriodsConnected) { |
| const ConnectivityCheckPeriods periods = GetParam(); |
| const base::TimeDelta margin = base::Milliseconds(100); |
| tracker_->SetConnectionType( |
| network::mojom::ConnectionType::CONNECTION_UNKNOWN); |
| |
| // Initial: disconnected. First Check. |
| // Next check is scheduled in disconnected_check_period_. |
| DisconnectAndCheck(); |
| // Connect. |
| SetResponsesWithStatusCode(kConnectivitySuccessStatusCode); |
| |
| // Jump to right before the next Check. Result is still not connected. |
| task_environment_.FastForwardBy(periods.disconnected_check_period_ - margin); |
| EXPECT_FALSE(checker_->Connected()); |
| // After the Check --> connected. |
| // Next check is scheduled in connected_check_period_. |
| task_environment_.FastForwardBy(margin * 2); |
| EXPECT_TRUE(checker_->Connected()); |
| } |
| |
| TEST_P(ConnectivityCheckerImplTestPeriodParameterized, |
| CheckWithCustomizedPeriodsDisconnected) { |
| const ConnectivityCheckPeriods periods = GetParam(); |
| const base::TimeDelta margin = base::Milliseconds(100); |
| tracker_->SetConnectionType( |
| network::mojom::ConnectionType::CONNECTION_UNKNOWN); |
| |
| // Initial: connected. First Check. |
| // Next check is scheduled in disconnected_check_period_. |
| ConnectAndCheck(); |
| // Disconnect. |
| SetResponsesWithStatusCode(net::HTTP_INTERNAL_SERVER_ERROR); |
| |
| // Jump to right before the next Check. Result is still connected. |
| task_environment_.FastForwardBy(periods.connected_check_period_ - margin); |
| EXPECT_TRUE(checker_->Connected()); |
| |
| // After the Check, still connected. |
| // It retries kNumErrorsToNotifyOffline times to switch to disconnected. |
| task_environment_.FastForwardBy(margin * 2); |
| // Fast forward by kNumErrorsToNotifyOffline * connected_check_period_. |
| for (unsigned int i = 0; i < kNumErrorsToNotifyOffline; i++) { |
| EXPECT_TRUE(checker_->Connected()); |
| // Check again. |
| task_environment_.FastForwardBy(periods.disconnected_check_period_); |
| } |
| // After retries, the result becomes disconnected. |
| EXPECT_FALSE(checker_->Connected()); |
| } |
| |
| // Test various connected/disconnected check periods |
| INSTANTIATE_TEST_SUITE_P( |
| ConnectivityCheckerImplTestCheckPeriods, |
| ConnectivityCheckerImplTestPeriodParameterized, |
| ::testing::Values( |
| ConnectivityCheckPeriods(base::Seconds(1), base::Seconds(1)), |
| ConnectivityCheckPeriods(base::Seconds(1), base::Seconds(60)), |
| ConnectivityCheckPeriods(base::Seconds(60), base::Seconds(1)), |
| ConnectivityCheckPeriods(base::Seconds(10), base::Seconds(120)), |
| ConnectivityCheckPeriods(base::Seconds(50), base::Seconds(200)))); |
| |
| TEST_F(ConnectivityCheckerImplTest, RecordsDisconnectDueToRequestTimeout) { |
| tracker_->SetConnectionType( |
| network::mojom::ConnectionType::CONNECTION_UNKNOWN); |
| ConnectAndCheck(); |
| |
| // Don't send a response for the request. |
| test_url_loader_factory().ClearResponses(); |
| CheckAndExpectRecordedError( |
| ConnectivityCheckerImpl::ErrorType::REQUEST_TIMEOUT); |
| } |
| |
| TEST_F(ConnectivityCheckerImplTest, RecordsDisconnectDueToNetError) { |
| tracker_->SetConnectionType( |
| network::mojom::ConnectionType::CONNECTION_UNKNOWN); |
| ConnectAndCheck(); |
| |
| // Set up a generic failure |
| network::URLLoaderCompletionStatus status; |
| status.error_code = net::ERR_FAILED; |
| |
| // Simulate network responses using the configured network error. |
| for (const char* url : kDefaultConnectivityCheckUrls) { |
| test_url_loader_factory().AddResponse( |
| GURL(url), |
| network::CreateURLResponseHead(kConnectivitySuccessStatusCode), |
| /*content=*/"", status, /*redirects=*/{}, |
| network::TestURLLoaderFactory::kSendHeadersOnNetworkError); |
| } |
| |
| CheckAndExpectRecordedError(ConnectivityCheckerImpl::ErrorType::NET_ERROR); |
| } |
| |
| TEST_F(ConnectivityCheckerImplTest, InitialCheckIsNotDelayed) { |
| // Do not set an initial connection type. This causes the first check to be |
| // delayed. |
| EXPECT_FALSE(checker_->Connected()); |
| // Notify that a network connection is established after the checker is |
| // constructed. A check should be automatically started when the connection |
| // is established. |
| SetResponsesWithStatusCode(kConnectivitySuccessStatusCode); |
| tracker_->SetConnectionType( |
| network::mojom::ConnectionType::CONNECTION_UNKNOWN); |
| // Poke the task runner, but don't fast forward the clock, to prove that the |
| // check is not a delayed task. We don't want the first check to be delayed |
| // after the network is initially connected because a lot of initialization of |
| // the Cast shell is bottlenecked on establishing network connectivity, and |
| // any delay of the initial check shows up as a user-visible delay in the |
| // Cast receiver becoming discoverable after boot. |
| task_environment_.FastForwardBy(base::TimeDelta()); |
| EXPECT_TRUE(checker_->Connected()); |
| } |
| |
| TEST_F(ConnectivityCheckerImplTest, InitialCheck_NoNetwork) { |
| // Do not set an initial connection type. This causes the first check to be |
| // delayed. |
| EXPECT_FALSE(checker_->Connected()); |
| // Initialize the network tracker to indicate that there is no connection at |
| // first. |
| SetResponsesWithStatusCode(kConnectivitySuccessStatusCode); |
| tracker_->SetConnectionType(network::mojom::ConnectionType::CONNECTION_NONE); |
| // Network connection still isn't established after flushing tasks. |
| task_environment_.FastForwardBy(base::TimeDelta()); |
| EXPECT_FALSE(checker_->Connected()); |
| // Establish a connection. |
| SetResponsesWithStatusCode(kConnectivitySuccessStatusCode); |
| tracker_->SetConnectionType( |
| network::mojom::ConnectionType::CONNECTION_UNKNOWN); |
| // Subsequent network checks are delayed due to rate limiting, so the |
| // connectivity status doesn't update until delay elapses. |
| task_environment_.FastForwardBy(base::TimeDelta()); |
| EXPECT_FALSE(checker_->Connected()); |
| task_environment_.FastForwardBy(kNetworkChangedDelay); |
| EXPECT_TRUE(checker_->Connected()); |
| } |
| |
| class ConnectivityCheckerImplTestPeriodicCheck |
| : public ConnectivityCheckerImplBaseTest<FakeNetworkConnectionTracker>, |
| public ::testing::WithParamInterface<ConnectivityCheckPeriods> { |
| public: |
| ConnectivityCheckerImplTestPeriodicCheck() |
| : ConnectivityCheckerImplBaseTest<FakeNetworkConnectionTracker>( |
| std::make_unique<FakeNetworkConnectionTracker>(), |
| GetParam()) {} |
| }; |
| |
| TEST_P(ConnectivityCheckerImplTestPeriodicCheck, NoDuplicateConnectedCheck) { |
| const ConnectivityCheckPeriods periods = GetParam(); |
| constexpr base::TimeDelta kCheckRequestDelay = base::Milliseconds(100); |
| constexpr unsigned int kRounds = 10; |
| tracker_->NotifyNetworkTypeChanged( |
| network::mojom::ConnectionType::CONNECTION_UNKNOWN); |
| |
| // Initial: connected. First Check. |
| // A check is scheduled in connected_check_period_. |
| ConnectAndCheck(); |
| |
| // Add a delay to prevent the new check() from being ignored due to the |
| // duplicate url loader request |
| task_environment_.FastForwardBy(kCheckRequestDelay); |
| SetResponsesWithStatusCode(kConnectivitySuccessStatusCode); |
| tracker_->NotifyNetworkTypeChanged( |
| network::mojom::ConnectionType::CONNECTION_WIFI); |
| |
| // Wait for the internal network change delay. |
| // A check will be executed and the next check will be schedule in |
| // connected_check_period_. The old scheduled check should be removed. |
| task_environment_.FastForwardBy(kNetworkChangedDelay); |
| |
| // Fast forward and count the times of check() |
| unsigned int counter_start = tracker_->check_counter(); |
| task_environment_.FastForwardBy(periods.connected_check_period_ * kRounds); |
| |
| // The check_counter should increase by kRounds. |
| EXPECT_EQ(tracker_->check_counter() - counter_start, kRounds); |
| } |
| |
| TEST_P(ConnectivityCheckerImplTestPeriodicCheck, NoDuplicateDisconnectedCheck) { |
| const ConnectivityCheckPeriods periods = GetParam(); |
| constexpr base::TimeDelta kCheckRequestDelay = base::Milliseconds(100); |
| constexpr unsigned int kRounds = 10; |
| tracker_->NotifyNetworkTypeChanged( |
| network::mojom::ConnectionType::CONNECTION_UNKNOWN); |
| |
| // Initial: disconnected. First Check. |
| // A check is scheduled in disconnected_check_period_. |
| DisconnectAndCheck(); |
| |
| // Add a delay to prevent the new check() from being ignored due to the |
| // duplicate url loader request |
| task_environment_.FastForwardBy(kCheckRequestDelay); |
| SetResponsesWithStatusCode(net::HTTP_INTERNAL_SERVER_ERROR); |
| tracker_->NotifyNetworkTypeChanged( |
| network::mojom::ConnectionType::CONNECTION_WIFI); |
| |
| // Wait for the internal network change delay. |
| // A check will be executed and the next check will be schedule in |
| // disconnected_check_period_. The old scheduled check should be removed. |
| task_environment_.FastForwardBy(kNetworkChangedDelay); |
| |
| // Fast forward and count the times of check() |
| unsigned int counter_start = tracker_->check_counter(); |
| task_environment_.FastForwardBy(periods.disconnected_check_period_ * kRounds); |
| |
| // The check_counter should increase by kRounds. |
| EXPECT_EQ(tracker_->check_counter() - counter_start, kRounds); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| ConnectivityCheckerImplTestCheckPeriodicCheck, |
| ConnectivityCheckerImplTestPeriodicCheck, |
| ::testing::Values( |
| ConnectivityCheckPeriods(base::Seconds(1), base::Seconds(1)), |
| ConnectivityCheckPeriods(base::Seconds(10), base::Seconds(10)), |
| ConnectivityCheckPeriods(base::Seconds(1), base::Seconds(60)))); |
| |
| } // namespace chromecast |