// 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