blob: 0e020936a790ad94eeb06fd1ae62246d0cccfc6a [file] [log] [blame]
// Copyright 2017 The Chromium 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 "net/reporting/reporting_endpoint_manager.h"
#include <string>
#include "base/strings/stringprintf.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/time/time.h"
#include "net/base/backoff_entry.h"
#include "net/base/network_isolation_key.h"
#include "net/reporting/reporting_cache.h"
#include "net/reporting/reporting_endpoint.h"
#include "net/reporting/reporting_policy.h"
#include "net/reporting/reporting_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace net {
namespace {
class TestReportingCache : public ReportingCache {
public:
class PersistentReportingStore;
// Tests using this class only use one origin/group.
TestReportingCache(const url::Origin& expected_origin,
const std::string& expected_group)
: expected_origin_(expected_origin), expected_group_(expected_group) {}
~TestReportingCache() override = default;
void SetEndpoint(const ReportingEndpoint& reporting_endpoint) {
reporting_endpoints_[reporting_endpoint.group_key.network_isolation_key]
.push_back(reporting_endpoint);
}
// ReportingCache implementation:
std::vector<ReportingEndpoint> GetCandidateEndpointsForDelivery(
const ReportingEndpointGroupKey& group_key) override {
EXPECT_EQ(expected_origin_, group_key.origin);
EXPECT_EQ(expected_group_, group_key.group_name);
return reporting_endpoints_[group_key.network_isolation_key];
}
// Everything below is NOTREACHED.
void AddReport(const NetworkIsolationKey& network_isolation_key,
const GURL& url,
const std::string& user_agent,
const std::string& group_name,
const std::string& type,
std::unique_ptr<const base::Value> body,
int depth,
base::TimeTicks queued,
int attempts) override {
NOTREACHED();
}
void GetReports(
std::vector<const ReportingReport*>* reports_out) const override {
NOTREACHED();
}
base::Value GetReportsAsValue() const override {
NOTREACHED();
return base::Value();
}
std::vector<const ReportingReport*> GetReportsToDeliver() override {
NOTREACHED();
return {};
}
void ClearReportsPending(
const std::vector<const ReportingReport*>& reports) override {
NOTREACHED();
}
void IncrementReportsAttempts(
const std::vector<const ReportingReport*>& reports) override {
NOTREACHED();
}
void IncrementEndpointDeliveries(const ReportingEndpointGroupKey& group_key,
const GURL& url,
int reports_delivered,
bool successful) override {
NOTREACHED();
}
void RemoveReports(const std::vector<const ReportingReport*>& reports,
ReportingReport::Outcome outcome) override {
NOTREACHED();
}
void RemoveAllReports(ReportingReport::Outcome outcome) override {
NOTREACHED();
}
size_t GetFullReportCountForTesting() const override {
NOTREACHED();
return 0;
}
bool IsReportPendingForTesting(const ReportingReport* report) const override {
NOTREACHED();
return false;
}
bool IsReportDoomedForTesting(const ReportingReport* report) const override {
NOTREACHED();
return false;
}
void OnParsedHeader(
const NetworkIsolationKey& network_isolation_key,
const url::Origin& origin,
std::vector<ReportingEndpointGroup> parsed_header) override {
NOTREACHED();
}
std::set<url::Origin> GetAllOrigins() const override {
NOTREACHED();
return std::set<url::Origin>();
}
void RemoveClient(const NetworkIsolationKey& network_isolation_key,
const url::Origin& origin) override {
NOTREACHED();
}
void RemoveClientsForOrigin(const url::Origin& origin) override {
NOTREACHED();
}
void RemoveAllClients() override { NOTREACHED(); }
void RemoveEndpointGroup(
const ReportingEndpointGroupKey& group_key) override {
NOTREACHED();
}
void RemoveEndpointsForUrl(const GURL& url) override { NOTREACHED(); }
void AddClientsLoadedFromStore(
std::vector<ReportingEndpoint> loaded_endpoints,
std::vector<CachedReportingEndpointGroup> loaded_endpoint_groups)
override {
NOTREACHED();
}
base::Value GetClientsAsValue() const override {
NOTREACHED();
return base::Value();
}
size_t GetEndpointCount() const override {
NOTREACHED();
return 0;
}
void Flush() override { NOTREACHED(); }
ReportingEndpoint GetEndpointForTesting(
const ReportingEndpointGroupKey& group_key,
const GURL& url) const override {
NOTREACHED();
return ReportingEndpoint();
}
bool EndpointGroupExistsForTesting(const ReportingEndpointGroupKey& group_key,
OriginSubdomains include_subdomains,
base::Time expires) const override {
NOTREACHED();
return false;
}
bool ClientExistsForTesting(const NetworkIsolationKey& network_isolation_key,
const url::Origin& origin) const override {
NOTREACHED();
return false;
}
size_t GetEndpointGroupCountForTesting() const override {
NOTREACHED();
return 0;
}
size_t GetClientCountForTesting() const override {
NOTREACHED();
return 0;
}
void SetEndpointForTesting(const ReportingEndpointGroupKey& group_key,
const GURL& url,
OriginSubdomains include_subdomains,
base::Time expires,
int priority,
int weight) override {
NOTREACHED();
}
private:
const url::Origin expected_origin_;
const std::string expected_group_;
std::map<NetworkIsolationKey, std::vector<ReportingEndpoint>>
reporting_endpoints_;
DISALLOW_COPY_AND_ASSIGN(TestReportingCache);
};
class ReportingEndpointManagerTest : public testing::Test {
public:
ReportingEndpointManagerTest() : cache_(kOrigin, kGroup) {
policy_.endpoint_backoff_policy.num_errors_to_ignore = 0;
policy_.endpoint_backoff_policy.initial_delay_ms = 60000;
policy_.endpoint_backoff_policy.multiply_factor = 2.0;
policy_.endpoint_backoff_policy.jitter_factor = 0.0;
policy_.endpoint_backoff_policy.maximum_backoff_ms = -1;
policy_.endpoint_backoff_policy.entry_lifetime_ms = 0;
policy_.endpoint_backoff_policy.always_use_initial_delay = false;
clock_.SetNowTicks(base::TimeTicks());
endpoint_manager_ = ReportingEndpointManager::Create(
&policy_, &clock_, &delegate_, &cache_, TestReportingRandIntCallback());
}
protected:
void SetEndpoint(
const GURL& endpoint,
int priority = ReportingEndpoint::EndpointInfo::kDefaultPriority,
int weight = ReportingEndpoint::EndpointInfo::kDefaultWeight,
const NetworkIsolationKey& network_isolation_key =
NetworkIsolationKey()) {
ReportingEndpointGroupKey group_key(kGroupKey);
group_key.network_isolation_key = network_isolation_key;
cache_.SetEndpoint(ReportingEndpoint(
group_key,
ReportingEndpoint::EndpointInfo{endpoint, priority, weight}));
}
const NetworkIsolationKey kNik;
const url::Origin kOrigin = url::Origin::Create(GURL("https://origin/"));
const std::string kGroup = "group";
const ReportingEndpointGroupKey kGroupKey =
ReportingEndpointGroupKey(kNik, kOrigin, kGroup);
const GURL kEndpoint = GURL("https://endpoint/");
ReportingPolicy policy_;
base::SimpleTestTickClock clock_;
TestReportingDelegate delegate_;
TestReportingCache cache_;
std::unique_ptr<ReportingEndpointManager> endpoint_manager_;
};
TEST_F(ReportingEndpointManagerTest, NoEndpoint) {
ReportingEndpoint endpoint =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
EXPECT_FALSE(endpoint);
}
TEST_F(ReportingEndpointManagerTest, Endpoint) {
SetEndpoint(kEndpoint);
ReportingEndpoint endpoint =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
ASSERT_TRUE(endpoint);
EXPECT_EQ(kEndpoint, endpoint.info.url);
}
TEST_F(ReportingEndpointManagerTest, BackedOffEndpoint) {
ASSERT_EQ(2.0, policy_.endpoint_backoff_policy.multiply_factor);
base::TimeDelta initial_delay = base::TimeDelta::FromMilliseconds(
policy_.endpoint_backoff_policy.initial_delay_ms);
SetEndpoint(kEndpoint);
endpoint_manager_->InformOfEndpointRequest(NetworkIsolationKey(), kEndpoint,
false);
// After one failure, endpoint is in exponential backoff.
ReportingEndpoint endpoint =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
EXPECT_FALSE(endpoint);
// After initial delay, endpoint is usable again.
clock_.Advance(initial_delay);
ReportingEndpoint endpoint2 =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
ASSERT_TRUE(endpoint2);
EXPECT_EQ(kEndpoint, endpoint2.info.url);
endpoint_manager_->InformOfEndpointRequest(NetworkIsolationKey(), kEndpoint,
false);
// After a second failure, endpoint is backed off again.
ReportingEndpoint endpoint3 =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
EXPECT_FALSE(endpoint3);
clock_.Advance(initial_delay);
// Next backoff is longer -- 2x the first -- so endpoint isn't usable yet.
ReportingEndpoint endpoint4 =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
EXPECT_FALSE(endpoint4);
clock_.Advance(initial_delay);
// After 2x the initial delay, the endpoint is usable again.
ReportingEndpoint endpoint5 =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
ASSERT_TRUE(endpoint5);
EXPECT_EQ(kEndpoint, endpoint5.info.url);
endpoint_manager_->InformOfEndpointRequest(NetworkIsolationKey(), kEndpoint,
true);
endpoint_manager_->InformOfEndpointRequest(NetworkIsolationKey(), kEndpoint,
true);
// Two more successful requests should reset the backoff to the initial delay
// again.
endpoint_manager_->InformOfEndpointRequest(NetworkIsolationKey(), kEndpoint,
false);
ReportingEndpoint endpoint6 =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
EXPECT_FALSE(endpoint6);
clock_.Advance(initial_delay);
ReportingEndpoint endpoint7 =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
EXPECT_TRUE(endpoint7);
}
// Make sure that multiple endpoints will all be returned at some point, to
// avoid accidentally or intentionally implementing any priority ordering.
TEST_F(ReportingEndpointManagerTest, RandomEndpoint) {
static const GURL kEndpoint1("https://endpoint1/");
static const GURL kEndpoint2("https://endpoint2/");
static const int kMaxAttempts = 20;
SetEndpoint(kEndpoint1);
SetEndpoint(kEndpoint2);
bool endpoint1_seen = false;
bool endpoint2_seen = false;
for (int i = 0; i < kMaxAttempts; ++i) {
ReportingEndpoint endpoint =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
ASSERT_TRUE(endpoint);
ASSERT_TRUE(endpoint.info.url == kEndpoint1 ||
endpoint.info.url == kEndpoint2);
if (endpoint.info.url == kEndpoint1)
endpoint1_seen = true;
else if (endpoint.info.url == kEndpoint2)
endpoint2_seen = true;
if (endpoint1_seen && endpoint2_seen)
break;
}
EXPECT_TRUE(endpoint1_seen);
EXPECT_TRUE(endpoint2_seen);
}
TEST_F(ReportingEndpointManagerTest, Priority) {
static const GURL kPrimaryEndpoint("https://endpoint1/");
static const GURL kBackupEndpoint("https://endpoint2/");
SetEndpoint(kPrimaryEndpoint, 10 /* priority */,
ReportingEndpoint::EndpointInfo::kDefaultWeight);
SetEndpoint(kBackupEndpoint, 20 /* priority */,
ReportingEndpoint::EndpointInfo::kDefaultWeight);
ReportingEndpoint endpoint =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
ASSERT_TRUE(endpoint);
EXPECT_EQ(kPrimaryEndpoint, endpoint.info.url);
// The backoff policy we set up in the constructor means that a single failed
// upload will take the primary endpoint out of contention. This should cause
// us to choose the backend endpoint.
endpoint_manager_->InformOfEndpointRequest(NetworkIsolationKey(),
kPrimaryEndpoint, false);
ReportingEndpoint endpoint2 =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
ASSERT_TRUE(endpoint2);
EXPECT_EQ(kBackupEndpoint, endpoint2.info.url);
// Advance the current time far enough to clear out the primary endpoint's
// backoff clock. This should bring the primary endpoint back into play.
clock_.Advance(base::TimeDelta::FromMinutes(2));
ReportingEndpoint endpoint3 =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
ASSERT_TRUE(endpoint3);
EXPECT_EQ(kPrimaryEndpoint, endpoint3.info.url);
}
// Note: This test depends on the deterministic mock RandIntCallback set up in
// TestReportingContext, which returns consecutive integers starting at 0
// (modulo the requested range, plus the requested minimum).
TEST_F(ReportingEndpointManagerTest, Weight) {
static const GURL kEndpoint1("https://endpoint1/");
static const GURL kEndpoint2("https://endpoint2/");
static const int kEndpoint1Weight = 5;
static const int kEndpoint2Weight = 2;
static const int kTotalEndpointWeight = kEndpoint1Weight + kEndpoint2Weight;
SetEndpoint(kEndpoint1, ReportingEndpoint::EndpointInfo::kDefaultPriority,
kEndpoint1Weight);
SetEndpoint(kEndpoint2, ReportingEndpoint::EndpointInfo::kDefaultPriority,
kEndpoint2Weight);
int endpoint1_count = 0;
int endpoint2_count = 0;
for (int i = 0; i < kTotalEndpointWeight; ++i) {
ReportingEndpoint endpoint =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
ASSERT_TRUE(endpoint);
ASSERT_TRUE(endpoint.info.url == kEndpoint1 ||
endpoint.info.url == kEndpoint2);
if (endpoint.info.url == kEndpoint1)
++endpoint1_count;
else if (endpoint.info.url == kEndpoint2)
++endpoint2_count;
}
EXPECT_EQ(kEndpoint1Weight, endpoint1_count);
EXPECT_EQ(kEndpoint2Weight, endpoint2_count);
}
TEST_F(ReportingEndpointManagerTest, ZeroWeights) {
static const GURL kEndpoint1("https://endpoint1/");
static const GURL kEndpoint2("https://endpoint2/");
SetEndpoint(kEndpoint1, ReportingEndpoint::EndpointInfo::kDefaultPriority,
0 /* weight */);
SetEndpoint(kEndpoint2, ReportingEndpoint::EndpointInfo::kDefaultPriority,
0 /* weight */);
int endpoint1_count = 0;
int endpoint2_count = 0;
for (int i = 0; i < 10; ++i) {
ReportingEndpoint endpoint =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
ASSERT_TRUE(endpoint);
ASSERT_TRUE(endpoint.info.url == kEndpoint1 ||
endpoint.info.url == kEndpoint2);
if (endpoint.info.url == kEndpoint1)
++endpoint1_count;
else if (endpoint.info.url == kEndpoint2)
++endpoint2_count;
}
EXPECT_EQ(5, endpoint1_count);
EXPECT_EQ(5, endpoint2_count);
}
// Check that ReportingEndpointManager distinguishes NetworkIsolationKeys.
TEST_F(ReportingEndpointManagerTest, NetworkIsolationKey) {
const url::Origin kOrigin2 = url::Origin::Create(GURL("https://origin2/"));
const NetworkIsolationKey kNetworkIsolationKey1(kOrigin, kOrigin);
const NetworkIsolationKey kNetworkIsolationKey2(kOrigin2, kOrigin2);
const ReportingEndpointGroupKey kGroupKey1(kNetworkIsolationKey1, kOrigin,
kGroup);
const ReportingEndpointGroupKey kGroupKey2(kNetworkIsolationKey2, kOrigin,
kGroup);
// An Endpoint set for kNetworkIsolationKey1 should not affect
// kNetworkIsolationKey2.
SetEndpoint(kEndpoint, ReportingEndpoint::EndpointInfo::kDefaultPriority,
0 /* weight */, kNetworkIsolationKey1);
ReportingEndpoint endpoint =
endpoint_manager_->FindEndpointForDelivery(kGroupKey1);
ASSERT_TRUE(endpoint);
EXPECT_EQ(kEndpoint, endpoint.info.url);
EXPECT_FALSE(endpoint_manager_->FindEndpointForDelivery(kGroupKey2));
EXPECT_FALSE(endpoint_manager_->FindEndpointForDelivery(kGroupKey));
// Set the same Endpoint for kNetworkIsolationKey2, so both should be
// reporting to the same URL.
SetEndpoint(kEndpoint, ReportingEndpoint::EndpointInfo::kDefaultPriority,
0 /* weight */, kNetworkIsolationKey2);
endpoint = endpoint_manager_->FindEndpointForDelivery(kGroupKey1);
ASSERT_TRUE(endpoint);
EXPECT_EQ(kEndpoint, endpoint.info.url);
endpoint = endpoint_manager_->FindEndpointForDelivery(kGroupKey2);
ASSERT_TRUE(endpoint);
EXPECT_EQ(kEndpoint, endpoint.info.url);
EXPECT_FALSE(endpoint_manager_->FindEndpointForDelivery(kGroupKey));
// An error reporting to that URL in the context of kNetworkIsolationKey1
// should only affect the Endpoint retrieved in the context of
// kNetworkIsolationKey1.
endpoint_manager_->InformOfEndpointRequest(kNetworkIsolationKey1, kEndpoint,
false);
EXPECT_FALSE(endpoint_manager_->FindEndpointForDelivery(kGroupKey1));
endpoint = endpoint_manager_->FindEndpointForDelivery(kGroupKey2);
ASSERT_TRUE(endpoint);
EXPECT_EQ(kEndpoint, endpoint.info.url);
EXPECT_FALSE(endpoint_manager_->FindEndpointForDelivery(kGroupKey));
}
TEST_F(ReportingEndpointManagerTest, NetworkIsolationKeyWithMultipleEndpoints) {
const url::Origin kOrigin2 = url::Origin::Create(GURL("https://origin2/"));
const NetworkIsolationKey kNetworkIsolationKey1(kOrigin, kOrigin);
const NetworkIsolationKey kNetworkIsolationKey2(kOrigin2, kOrigin2);
const ReportingEndpointGroupKey kGroupKey1(kNetworkIsolationKey1, kOrigin,
kGroup);
const ReportingEndpointGroupKey kGroupKey2(kNetworkIsolationKey2, kOrigin,
kGroup);
const GURL kEndpoint1("https://endpoint1/");
const GURL kEndpoint2("https://endpoint2/");
const GURL kEndpoint3("https://endpoint3/");
const int kMaxAttempts = 20;
// Add two Endpoints for kNetworkIsolationKey1, and a different one for
// kNetworkIsolationKey2.
SetEndpoint(kEndpoint1, ReportingEndpoint::EndpointInfo::kDefaultPriority,
ReportingEndpoint::EndpointInfo::kDefaultWeight,
kNetworkIsolationKey1);
SetEndpoint(kEndpoint2, ReportingEndpoint::EndpointInfo::kDefaultPriority,
ReportingEndpoint::EndpointInfo::kDefaultWeight,
kNetworkIsolationKey1);
SetEndpoint(kEndpoint3, ReportingEndpoint::EndpointInfo::kDefaultPriority,
ReportingEndpoint::EndpointInfo::kDefaultWeight,
kNetworkIsolationKey2);
bool endpoint1_seen = false;
bool endpoint2_seen = false;
// Make sure that calling FindEndpointForDelivery() with kNetworkIsolationKey1
// can return both of its endpoints, but not kNetworkIsolationKey2's endpoint.
for (int i = 0; i < kMaxAttempts; ++i) {
ReportingEndpoint endpoint =
endpoint_manager_->FindEndpointForDelivery(kGroupKey1);
ASSERT_TRUE(endpoint);
ASSERT_TRUE(endpoint.info.url == kEndpoint1 ||
endpoint.info.url == kEndpoint2);
if (endpoint.info.url == kEndpoint1) {
endpoint1_seen = true;
} else if (endpoint.info.url == kEndpoint2) {
endpoint2_seen = true;
}
}
EXPECT_TRUE(endpoint1_seen);
EXPECT_TRUE(endpoint2_seen);
ReportingEndpoint endpoint =
endpoint_manager_->FindEndpointForDelivery(kGroupKey2);
ASSERT_TRUE(endpoint);
EXPECT_EQ(kEndpoint3, endpoint.info.url);
}
TEST_F(ReportingEndpointManagerTest, CacheEviction) {
// Add |kMaxEndpointBackoffCacheSize| endpoints.
for (int i = 0; i < ReportingEndpointManager::kMaxEndpointBackoffCacheSize;
++i) {
SetEndpoint(GURL(base::StringPrintf("https://endpoint%i/", i)));
}
// Mark each endpoint as bad, one-at-a-time. Use FindEndpointForDelivery() to
// pick which one to mark as bad, both to exercise the code walking through
// all endpoints, and as a sanity check.
std::set<GURL> seen_endpoints;
for (int i = 0; i < ReportingEndpointManager::kMaxEndpointBackoffCacheSize;
++i) {
ReportingEndpoint endpoint =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
EXPECT_TRUE(endpoint);
EXPECT_FALSE(seen_endpoints.count(endpoint.info.url));
seen_endpoints.insert(endpoint.info.url);
endpoint_manager_->InformOfEndpointRequest(NetworkIsolationKey(),
endpoint.info.url, false);
}
// All endpoints should now be marked as bad.
EXPECT_FALSE(endpoint_manager_->FindEndpointForDelivery(kGroupKey));
// Add another endpoint with a different NetworkIsolationKey;
const NetworkIsolationKey kDifferentNetworkIsolationKey(kOrigin, kOrigin);
const ReportingEndpointGroupKey kDifferentGroupKey(
kDifferentNetworkIsolationKey, kOrigin, kGroup);
SetEndpoint(kEndpoint, ReportingEndpoint::EndpointInfo::kDefaultPriority,
ReportingEndpoint::EndpointInfo::kDefaultWeight,
kDifferentNetworkIsolationKey);
// All endpoints associated with the empty NetworkIsolationKey should still be
// marked as bad.
EXPECT_FALSE(endpoint_manager_->FindEndpointForDelivery(kGroupKey));
// Make the endpoint added for the kDifferentNetworkIsolationKey as bad.
endpoint_manager_->InformOfEndpointRequest(kDifferentNetworkIsolationKey,
kEndpoint, false);
// The only endpoint for kDifferentNetworkIsolationKey should still be marked
// as bad.
EXPECT_FALSE(endpoint_manager_->FindEndpointForDelivery(kDifferentGroupKey));
// One of the endpoints for the empty NetworkIsolationKey should no longer be
// marked as bad, due to eviction.
ReportingEndpoint endpoint =
endpoint_manager_->FindEndpointForDelivery(kGroupKey);
EXPECT_TRUE(endpoint);
// Reporting a success for the (only) good endpoint for the empty
// NetworkIsolationKey should evict the entry for kNetworkIsolationKey, since
// the most recent FindEndpointForDelivery() call visited all of the empty
// NetworkIsolationKey's cached bad entries.
endpoint_manager_->InformOfEndpointRequest(NetworkIsolationKey(),
endpoint.info.url, true);
EXPECT_TRUE(endpoint_manager_->FindEndpointForDelivery(kDifferentGroupKey));
}
} // namespace
} // namespace net