blob: 2e152a0842a1ef1284be69aafee436c450517582 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "shill/portal_detector.h"
#include <memory>
#include <string>
#include <vector>
#include <base/bind.h>
#include <brillo/http/http_request.h>
#include <brillo/http/mock_connection.h>
#include <brillo/http/mock_transport.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "shill/mock_connection.h"
#include "shill/mock_control.h"
#include "shill/mock_device_info.h"
#include "shill/mock_event_dispatcher.h"
#include "shill/mock_http_request.h"
#include "shill/mock_manager.h"
#include "shill/mock_metrics.h"
#include "shill/net/mock_time.h"
using base::Bind;
using base::Callback;
using base::Unretained;
using std::string;
using std::vector;
using testing::_;
using testing::Mock;
using testing::NiceMock;
using testing::Return;
using testing::ReturnRef;
using testing::StrictMock;
using testing::Test;
namespace shill {
namespace {
const char kBadURL[] = "badurl";
const char kInterfaceName[] = "int0";
const char kHttpUrl[] = "http://www.chromium.org";
const char kHttpsUrl[] = "https://www.google.com";
const vector<string> kFallbackHttpUrls{
"http://www.google.com/gen_204",
"http://play.googleapis.com/generate_204",
};
const char kDNSServer0[] = "8.8.8.8";
const char kDNSServer1[] = "8.8.4.4";
const char* const kDNSServers[] = {kDNSServer0, kDNSServer1};
} // namespace
MATCHER_P(IsResult, result, "") {
return (result.phase == arg.phase && result.status == arg.status &&
result.redirect_url_string == arg.redirect_url_string &&
result.probe_url_string == arg.probe_url_string);
}
class PortalDetectorTest : public Test {
public:
PortalDetectorTest()
: manager_(&control_, &dispatcher_, nullptr),
device_info_(new NiceMock<MockDeviceInfo>(&manager_)),
connection_(new StrictMock<MockConnection>(device_info_.get())),
transport_(std::make_shared<brillo::http::MockTransport>()),
brillo_connection_(
std::make_shared<brillo::http::MockConnection>(transport_)),
portal_detector_(
new PortalDetector(connection_.get(),
&dispatcher_,
&metrics_,
callback_target_.result_callback())),
interface_name_(kInterfaceName),
dns_servers_(kDNSServers, kDNSServers + 2),
http_request_(nullptr),
https_request_(nullptr) {
current_time_.tv_sec = current_time_.tv_usec = 0;
}
void SetUp() override {
EXPECT_CALL(*connection_, IsIPv6()).WillRepeatedly(Return(false));
EXPECT_CALL(*connection_, interface_name())
.WillRepeatedly(ReturnRef(interface_name_));
portal_detector_->time_ = &time_;
EXPECT_CALL(time_, GetTimeMonotonic(_))
.WillRepeatedly(Invoke(this, &PortalDetectorTest::GetTimeMonotonic));
EXPECT_CALL(*connection_, dns_servers())
.WillRepeatedly(ReturnRef(dns_servers_));
EXPECT_EQ(nullptr, portal_detector_->http_request_);
}
void TearDown() override {
Mock::VerifyAndClearExpectations(&http_request_);
if (portal_detector()->http_request_) {
EXPECT_CALL(*http_request(), Stop());
EXPECT_CALL(*https_request(), Stop());
// Delete the portal detector while expectations still exist.
portal_detector_.reset();
}
testing::Mock::VerifyAndClearExpectations(brillo_connection_.get());
brillo_connection_.reset();
testing::Mock::VerifyAndClearExpectations(transport_.get());
transport_.reset();
}
protected:
static const int kNumAttempts;
class CallbackTarget {
public:
CallbackTarget()
: result_callback_(
Bind(&CallbackTarget::ResultCallback, Unretained(this))) {}
MOCK_METHOD(void,
ResultCallback,
(const PortalDetector::Result&, const PortalDetector::Result&));
Callback<void(const PortalDetector::Result&,
const PortalDetector::Result&)>&
result_callback() {
return result_callback_;
}
private:
Callback<void(const PortalDetector::Result&, const PortalDetector::Result&)>
result_callback_;
};
void AssignHttpRequest() {
http_request_ = new StrictMock<MockHttpRequest>();
https_request_ = new StrictMock<MockHttpRequest>();
portal_detector_->http_request_.reset(http_request_);
portal_detector_->https_request_.reset(
https_request_); // Passes ownership.
}
bool StartPortalRequest(const PortalDetector::Properties& props, int delay) {
bool ret = portal_detector_->StartAfterDelay(props, delay);
if (ret) {
AssignHttpRequest();
}
return ret;
}
void StartTrialTask() {
EXPECT_CALL(*http_request(), Start(_, _, _, _))
.WillOnce(Return(HttpRequest::kResultInProgress));
EXPECT_CALL(*https_request(), Start(_, _, _, _))
.WillOnce(Return(HttpRequest::kResultInProgress));
portal_detector()->StartTrialTask();
}
MockHttpRequest* http_request() { return http_request_; }
MockHttpRequest* https_request() { return https_request_; }
PortalDetector* portal_detector() { return portal_detector_.get(); }
MockEventDispatcher& dispatcher() { return dispatcher_; }
CallbackTarget& callback_target() { return callback_target_; }
MockMetrics& metrics() { return metrics_; }
brillo::http::MockConnection* brillo_connection() {
return brillo_connection_.get();
}
void ExpectReset() {
EXPECT_FALSE(portal_detector_->attempt_count_);
EXPECT_TRUE(callback_target_.result_callback() ==
portal_detector_->portal_result_callback_);
EXPECT_EQ(nullptr, portal_detector_->http_request_);
EXPECT_EQ(nullptr, portal_detector_->https_request_);
}
void AdvanceTime(int milliseconds) {
struct timeval tv = {milliseconds / 1000, (milliseconds % 1000) * 1000};
timeradd(&current_time_, &tv, &current_time_);
}
void StartAttempt() {
EXPECT_CALL(dispatcher(), PostDelayedTask(_, _, 0));
PortalDetector::Properties props =
PortalDetector::Properties(kHttpUrl, kHttpsUrl, kFallbackHttpUrls);
EXPECT_TRUE(StartPortalRequest(props, 0));
StartTrialTask();
}
void ExpectRequestSuccessWithStatus(int status_code, bool is_http) {
EXPECT_CALL(*brillo_connection_, GetResponseStatusCode())
.WillOnce(Return(status_code));
auto response =
std::make_shared<brillo::http::Response>(brillo_connection_);
if (is_http)
portal_detector_->HttpRequestSuccessCallback(response);
else
portal_detector_->HttpsRequestSuccessCallback(response);
}
private:
int GetTimeMonotonic(struct timeval* tv) {
*tv = current_time_;
return 0;
}
StrictMock<MockEventDispatcher> dispatcher_;
MockControl control_;
MockManager manager_;
std::unique_ptr<MockDeviceInfo> device_info_;
scoped_refptr<MockConnection> connection_;
std::shared_ptr<brillo::http::MockTransport> transport_;
NiceMock<MockMetrics> metrics_;
std::shared_ptr<brillo::http::MockConnection> brillo_connection_;
CallbackTarget callback_target_;
std::unique_ptr<PortalDetector> portal_detector_;
StrictMock<MockTime> time_;
struct timeval current_time_;
const string interface_name_;
vector<string> dns_servers_;
MockHttpRequest* http_request_;
MockHttpRequest* https_request_;
};
// static
const int PortalDetectorTest::kNumAttempts = 0;
TEST_F(PortalDetectorTest, Constructor) {
ExpectReset();
}
TEST_F(PortalDetectorTest, InvalidURL) {
EXPECT_FALSE(portal_detector()->IsActive());
EXPECT_CALL(dispatcher(), PostDelayedTask(_, _, 0)).Times(0);
PortalDetector::Properties props =
PortalDetector::Properties(kBadURL, kHttpsUrl, kFallbackHttpUrls);
EXPECT_FALSE(StartPortalRequest(props, 0));
ExpectReset();
EXPECT_FALSE(portal_detector()->IsActive());
}
TEST_F(PortalDetectorTest, IsActive) {
// Before the trial is started, should not be active.
EXPECT_FALSE(portal_detector()->IsActive());
// Once the trial is started, IsActive should return true.
EXPECT_CALL(dispatcher(), PostDelayedTask(_, _, 0));
PortalDetector::Properties props =
PortalDetector::Properties(kHttpUrl, kHttpsUrl, kFallbackHttpUrls);
EXPECT_TRUE(StartPortalRequest(props, 0));
StartTrialTask();
EXPECT_TRUE(portal_detector()->IsActive());
// Finish the trial, IsActive should return false.
EXPECT_CALL(*http_request(), Stop()).Times(1);
EXPECT_CALL(*https_request(), Stop()).Times(1);
portal_detector()->CompleteTrial(
PortalDetector::Result(PortalDetector::Phase::kContent,
PortalDetector::Status::kFailure),
PortalDetector::Result(PortalDetector::Phase::kContent,
PortalDetector::Status::kSuccess));
EXPECT_FALSE(portal_detector()->IsActive());
}
TEST_F(PortalDetectorTest, StartAttemptFailed) {
EXPECT_CALL(dispatcher(), PostDelayedTask(_, _, 0));
PortalDetector::Properties props =
PortalDetector::Properties(kHttpUrl, kHttpsUrl, kFallbackHttpUrls);
EXPECT_TRUE(StartPortalRequest(props, 0));
// Expect that the request will be started -- return failure.
EXPECT_CALL(*http_request(), Start(_, _, _, _))
.WillOnce(Return(HttpRequest::kResultDNSFailure));
EXPECT_CALL(dispatcher(), PostDelayedTask(_, _, 0)).Times(0);
EXPECT_CALL(*http_request(), Stop()).Times(1);
EXPECT_CALL(*https_request(), Stop()).Times(1);
// Expect a non-final failure to be relayed to the caller.
EXPECT_CALL(
callback_target(),
ResultCallback(
IsResult(PortalDetector::Result(PortalDetector::Phase::kDNS,
PortalDetector::Status::kFailure,
kNumAttempts)),
IsResult(PortalDetector::Result(PortalDetector::Phase::kContent,
PortalDetector::Status::kFailure))));
portal_detector()->StartTrialTask();
}
TEST_F(PortalDetectorTest, AdjustStartDelayImmediate) {
PortalDetector::Properties props =
PortalDetector::Properties(kHttpUrl, kHttpsUrl, kFallbackHttpUrls);
EXPECT_CALL(dispatcher(), PostDelayedTask(_, _, 0));
EXPECT_TRUE(StartPortalRequest(props, 0));
EXPECT_EQ(portal_detector()->AdjustStartDelay(1), 1);
}
TEST_F(PortalDetectorTest, AdjustStartDelayAfterDelay) {
const int kDelaySeconds = 123;
// The first attempt should be delayed by kDelaySeconds.
PortalDetector::Properties props =
PortalDetector::Properties(kHttpUrl, kHttpsUrl, kFallbackHttpUrls);
EXPECT_CALL(dispatcher(), PostDelayedTask(_, _, kDelaySeconds * 1000));
EXPECT_TRUE(StartPortalRequest(props, kDelaySeconds));
AdvanceTime(kDelaySeconds * 1000);
EXPECT_EQ(portal_detector()->AdjustStartDelay(1), 1);
}
TEST_F(PortalDetectorTest, StartRepeated) {
EXPECT_CALL(dispatcher(), PostDelayedTask(_, _, 0)).Times(1);
PortalDetector::Properties props =
PortalDetector::Properties(kHttpUrl, kHttpsUrl, kFallbackHttpUrls);
EXPECT_TRUE(StartPortalRequest(props, 0));
// A second should cancel the existing trial and set up the new one.
EXPECT_CALL(*http_request(), Stop());
EXPECT_CALL(*https_request(), Stop());
EXPECT_CALL(dispatcher(), PostDelayedTask(_, _, 10 * 1000)).Times(1);
EXPECT_TRUE(portal_detector()->StartAfterDelay(props, 10));
}
TEST_F(PortalDetectorTest, AttemptCount) {
EXPECT_FALSE(portal_detector()->IsInProgress());
// Expect the PortalDetector to immediately post a task for the each attempt.
EXPECT_CALL(dispatcher(), PostDelayedTask(_, _, _)).Times(4);
PortalDetector::Properties props =
PortalDetector::Properties(kHttpUrl, kHttpsUrl, kFallbackHttpUrls);
EXPECT_TRUE(StartPortalRequest(props, 0));
EXPECT_EQ(portal_detector()->http_url_string_, kHttpUrl);
EXPECT_CALL(
callback_target(),
ResultCallback(
IsResult(PortalDetector::Result(PortalDetector::Phase::kDNS,
PortalDetector::Status::kFailure,
kNumAttempts)),
IsResult(PortalDetector::Result(PortalDetector::Phase::kContent,
PortalDetector::Status::kFailure))))
.Times(3);
// Expect the PortalDetector to stop the trial after
// the final attempt.
EXPECT_CALL(*http_request(), Stop()).Times(7);
EXPECT_CALL(*https_request(), Stop()).Times(7);
int init_delay = 3;
for (int i = 0; i < 3; i++) {
int delay = portal_detector()->AdjustStartDelay(init_delay);
EXPECT_EQ(delay, init_delay);
portal_detector()->StartAfterDelay(props, delay);
EXPECT_NE(portal_detector()->http_url_string_, kHttpUrl);
AdvanceTime(delay * 1000);
PortalDetector::Result r = PortalDetector::GetPortalResultForRequestResult(
HttpRequest::kResultDNSFailure);
PortalDetector::Result https_result = PortalDetector::Result(
PortalDetector::Phase::kContent, PortalDetector::Status::kFailure);
portal_detector()->CompleteAttempt(r, https_result);
init_delay *= 2;
}
portal_detector()->Stop();
ExpectReset();
}
TEST_F(PortalDetectorTest, RequestSuccess) {
StartAttempt();
// HTTPS probe does not trigger anything (for now)
PortalDetector::Result success_result =
PortalDetector::Result(PortalDetector::Phase::kContent,
PortalDetector::Status::kSuccess, kNumAttempts);
EXPECT_CALL(callback_target(), ResultCallback(IsResult(success_result),
IsResult(success_result)))
.Times(0);
EXPECT_CALL(*http_request(), Stop()).Times(0);
EXPECT_CALL(*https_request(), Stop()).Times(0);
ExpectRequestSuccessWithStatus(204, false);
EXPECT_CALL(callback_target(), ResultCallback(IsResult(success_result),
IsResult(success_result)));
EXPECT_CALL(*http_request(), Stop()).Times(1);
EXPECT_CALL(*https_request(), Stop()).Times(1);
EXPECT_CALL(metrics(), NotifyPortalDetectionMultiProbeResult(_, _));
ExpectRequestSuccessWithStatus(204, true);
}
TEST_F(PortalDetectorTest, RequestHTTPFailureHTTPSSuccess) {
StartAttempt();
// HTTPS probe does not trigger anything (for now)
PortalDetector::Result failure_result =
PortalDetector::Result(PortalDetector::Phase::kContent,
PortalDetector::Status::kFailure, kNumAttempts);
PortalDetector::Result success_result =
PortalDetector::Result(PortalDetector::Phase::kContent,
PortalDetector::Status::kSuccess, kNumAttempts);
EXPECT_CALL(callback_target(), ResultCallback(IsResult(failure_result),
IsResult(success_result)))
.Times(0);
EXPECT_CALL(*http_request(), Stop()).Times(0);
EXPECT_CALL(*https_request(), Stop()).Times(0);
ExpectRequestSuccessWithStatus(123, true);
EXPECT_CALL(callback_target(), ResultCallback(IsResult(failure_result),
IsResult(success_result)));
EXPECT_CALL(*http_request(), Stop()).Times(1);
EXPECT_CALL(*https_request(), Stop()).Times(1);
EXPECT_CALL(metrics(), NotifyPortalDetectionMultiProbeResult(_, _));
ExpectRequestSuccessWithStatus(204, false);
}
TEST_F(PortalDetectorTest, RequestFail) {
StartAttempt();
// HTTPS probe does not trigger anything (for now)
PortalDetector::Result failure_result =
PortalDetector::Result(PortalDetector::Phase::kContent,
PortalDetector::Status::kFailure, kNumAttempts);
EXPECT_CALL(callback_target(), ResultCallback(IsResult(failure_result),
IsResult(failure_result)))
.Times(0);
EXPECT_CALL(*http_request(), Stop()).Times(0);
EXPECT_CALL(*https_request(), Stop()).Times(0);
ExpectRequestSuccessWithStatus(123, false);
EXPECT_CALL(callback_target(), ResultCallback(IsResult(failure_result),
IsResult(failure_result)));
EXPECT_CALL(*http_request(), Stop()).Times(1);
EXPECT_CALL(*https_request(), Stop()).Times(1);
EXPECT_CALL(metrics(), NotifyPortalDetectionMultiProbeResult(_, _));
ExpectRequestSuccessWithStatus(123, true);
}
TEST_F(PortalDetectorTest, RequestRedirect) {
StartAttempt();
PortalDetector::Result redirect_result = PortalDetector::Result(
PortalDetector::Phase::kContent, PortalDetector::Status::kRedirect);
redirect_result.redirect_url_string = kHttpUrl;
redirect_result.probe_url_string = kHttpUrl;
PortalDetector::Result failure_result = PortalDetector::Result(
PortalDetector::Phase::kContent, PortalDetector::Status::kFailure);
EXPECT_CALL(callback_target(), ResultCallback(IsResult(redirect_result),
IsResult(failure_result)))
.Times(0);
EXPECT_CALL(*http_request(), Stop()).Times(0);
EXPECT_CALL(*https_request(), Stop()).Times(0);
ExpectRequestSuccessWithStatus(123, false);
EXPECT_CALL(callback_target(), ResultCallback(IsResult(redirect_result),
IsResult(failure_result)));
EXPECT_CALL(*http_request(), Stop()).Times(1);
EXPECT_CALL(*https_request(), Stop()).Times(1);
EXPECT_CALL(*brillo_connection(), GetResponseHeader("Location"))
.WillOnce(Return(kHttpUrl));
EXPECT_CALL(metrics(), NotifyPortalDetectionMultiProbeResult(_, _));
ExpectRequestSuccessWithStatus(302, true);
}
struct ResultMapping {
ResultMapping() : http_result(HttpRequest::kResultUnknown), portal_result() {}
ResultMapping(HttpRequest::Result in_http_result,
const PortalDetector::Result& in_portal_result)
: http_result(in_http_result), portal_result(in_portal_result) {}
HttpRequest::Result http_result;
PortalDetector::Result portal_result;
};
class PortalDetectorResultMappingTest
: public testing::TestWithParam<ResultMapping> {};
TEST_P(PortalDetectorResultMappingTest, MapResult) {
PortalDetector::Result trial_result =
PortalDetector::GetPortalResultForRequestResult(GetParam().http_result);
EXPECT_EQ(trial_result.phase, GetParam().portal_result.phase);
EXPECT_EQ(trial_result.status, GetParam().portal_result.status);
}
INSTANTIATE_TEST_SUITE_P(
TrialResultMappingTest,
PortalDetectorResultMappingTest,
::testing::Values(
ResultMapping(HttpRequest::kResultUnknown,
PortalDetector::Result(PortalDetector::Phase::kUnknown,
PortalDetector::Status::kFailure)),
ResultMapping(HttpRequest::kResultInvalidInput,
PortalDetector::Result(PortalDetector::Phase::kUnknown,
PortalDetector::Status::kFailure)),
ResultMapping(HttpRequest::kResultInProgress,
PortalDetector::Result(PortalDetector::Phase::kUnknown,
PortalDetector::Status::kFailure)),
ResultMapping(HttpRequest::kResultDNSFailure,
PortalDetector::Result(PortalDetector::Phase::kDNS,
PortalDetector::Status::kFailure)),
ResultMapping(HttpRequest::kResultDNSTimeout,
PortalDetector::Result(PortalDetector::Phase::kDNS,
PortalDetector::Status::kTimeout)),
ResultMapping(HttpRequest::kResultConnectionFailure,
PortalDetector::Result(PortalDetector::Phase::kConnection,
PortalDetector::Status::kFailure)),
ResultMapping(HttpRequest::kResultHTTPFailure,
PortalDetector::Result(PortalDetector::Phase::kHTTP,
PortalDetector::Status::kFailure)),
ResultMapping(HttpRequest::kResultHTTPTimeout,
PortalDetector::Result(PortalDetector::Phase::kHTTP,
PortalDetector::Status::kTimeout)),
ResultMapping(
HttpRequest::kResultSuccess,
PortalDetector::Result(PortalDetector::Phase::kContent,
PortalDetector::Status::kFailure))));
} // namespace shill