blob: 101dc273b73987c23824976ce4a73054e0cd89fa [file] [log] [blame]
// Copyright 2021 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 "content/browser/conversions/rate_limit_table.h"
#include <memory>
#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/simple_test_clock.h"
#include "base/time/time.h"
#include "content/browser/conversions/conversion_report.h"
#include "content/browser/conversions/conversion_test_utils.h"
#include "sql/database.h"
#include "sql/test/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace content {
class RateLimitTableTest : public testing::Test {
public:
RateLimitTableTest() = default;
void SetUp() override {
ASSERT_TRUE(temp_directory_.CreateUniqueTempDir());
delegate_ = std::make_unique<ConfigurableStorageDelegate>();
table_ = std::make_unique<RateLimitTable>(delegate_.get(), &clock_);
}
ConversionReport NewConversionReport(const url::Origin& impression_origin,
const url::Origin& conversion_origin,
int64_t impression_id = 0) {
return ConversionReport(ImpressionBuilder(clock()->Now())
.SetImpressionOrigin(impression_origin)
.SetConversionOrigin(conversion_origin)
.SetImpressionId(impression_id)
.Build(),
/*conversion_data=*/"",
/*conversion_time=*/clock()->Now(),
/*report_time=*/clock()->Now(),
/*conversion_id=*/base::nullopt);
}
size_t GetRateLimitRows(sql::Database* db) {
size_t rows = 0;
EXPECT_TRUE(sql::test::CountTableRows(db, "rate_limits", &rows));
return rows;
}
base::FilePath db_path() {
return temp_directory_.GetPath().Append(FILE_PATH_LITERAL("Conversions"));
}
base::SimpleTestClock* clock() { return &clock_; }
RateLimitTable* table() { return table_.get(); }
ConfigurableStorageDelegate* delegate() { return delegate_.get(); }
protected:
base::ScopedTempDir temp_directory_;
private:
std::unique_ptr<ConfigurableStorageDelegate> delegate_;
base::SimpleTestClock clock_;
std::unique_ptr<RateLimitTable> table_;
};
TEST_F(RateLimitTableTest, TableCreated_TableAndIndicesInitialized) {
sql::Database db;
EXPECT_TRUE(db.Open(db_path()));
EXPECT_FALSE(db.DoesTableExist("rate_limits"));
EXPECT_FALSE(db.DoesIndexExist("rate_limit_impression_site_type_idx"));
EXPECT_FALSE(db.DoesIndexExist("rate_limit_conversion_time_idx"));
EXPECT_TRUE(table()->CreateTable(&db));
EXPECT_TRUE(db.DoesTableExist("rate_limits"));
EXPECT_TRUE(db.DoesIndexExist("rate_limit_impression_site_type_idx"));
EXPECT_TRUE(db.DoesIndexExist("rate_limit_conversion_time_idx"));
EXPECT_EQ(0u, GetRateLimitRows(&db));
}
TEST_F(RateLimitTableTest, AddRateLimit) {
sql::Database db;
EXPECT_TRUE(db.Open(db_path()));
EXPECT_TRUE(table()->CreateTable(&db));
delegate()->set_rate_limits({
.time_window = base::TimeDelta::FromDays(3),
.max_attributions_per_window = INT_MAX,
});
EXPECT_TRUE(table()->AddRateLimit(
&db,
NewConversionReport(url::Origin::Create(GURL("https://a.example/")),
url::Origin::Create(GURL("https://b.example/")))));
EXPECT_EQ(1u, GetRateLimitRows(&db));
// The above report should be deleted, as it expires after the clock is
// advanced.
clock()->Advance(base::TimeDelta::FromDays(3));
EXPECT_TRUE(table()->AddRateLimit(
&db,
NewConversionReport(url::Origin::Create(GURL("https://c.example/")),
url::Origin::Create(GURL("https://d.example/")))));
EXPECT_EQ(1u, GetRateLimitRows(&db));
}
TEST_F(RateLimitTableTest, IsAttributionAllowed) {
sql::Database db;
EXPECT_TRUE(db.Open(db_path()));
EXPECT_TRUE(table()->CreateTable(&db));
delegate()->set_rate_limits({
// Set this to >9d so |AddRateLimit|'s calls to |DeleteExpiredRateLimits|
// don't delete any of the rows we're adding.
.time_window = base::TimeDelta::FromDays(10),
.max_attributions_per_window = 2,
});
const url::Origin example_a = url::Origin::Create(GURL("https://a.example/"));
const url::Origin example_b = url::Origin::Create(GURL("https://b.example/"));
const url::Origin example_c = url::Origin::Create(GURL("https://c.example/"));
const url::Origin example_d = url::Origin::Create(GURL("https://d.example/"));
// We will expire this row by advancing the clock to +10d below.
EXPECT_TRUE(
table()->AddRateLimit(&db, NewConversionReport(example_a, example_c)));
clock()->Advance(base::TimeDelta::FromDays(3));
EXPECT_TRUE(
table()->AddRateLimit(&db, NewConversionReport(example_a, example_d)));
clock()->Advance(base::TimeDelta::FromDays(3));
EXPECT_TRUE(
table()->AddRateLimit(&db, NewConversionReport(example_a, example_c)));
EXPECT_EQ(3u, GetRateLimitRows(&db));
// impression and conversion match
const auto report_a_c = NewConversionReport(example_a, example_c);
const auto report_a_d = NewConversionReport(example_a, example_d);
// impression doesn't match
const auto report_b_c = NewConversionReport(example_b, example_c);
// conversion doesn't match
const auto report_a_b = NewConversionReport(example_a, example_b);
// neither impression and conversion match
const auto report_b_d = NewConversionReport(example_a, example_b);
base::Time now = clock()->Now();
EXPECT_FALSE(table()->IsAttributionAllowed(&db, report_a_c, now));
EXPECT_TRUE(table()->IsAttributionAllowed(&db, report_a_d, now));
EXPECT_TRUE(table()->IsAttributionAllowed(&db, report_b_c, now));
EXPECT_TRUE(table()->IsAttributionAllowed(&db, report_a_b, now));
EXPECT_TRUE(table()->IsAttributionAllowed(&db, report_b_d, now));
// Expire the first row above by advancing to +10d.
clock()->Advance(base::TimeDelta::FromDays(4));
now = clock()->Now();
EXPECT_TRUE(table()->IsAttributionAllowed(&db, report_a_c, now));
EXPECT_TRUE(table()->IsAttributionAllowed(&db, report_a_d, now));
EXPECT_TRUE(table()->IsAttributionAllowed(&db, report_b_c, now));
EXPECT_TRUE(table()->IsAttributionAllowed(&db, report_a_b, now));
EXPECT_TRUE(table()->IsAttributionAllowed(&db, report_b_d, now));
EXPECT_EQ(3u, GetRateLimitRows(&db));
}
TEST_F(RateLimitTableTest,
IsAttributionAllowed_ConversionDestinationSubdomains) {
sql::Database db;
EXPECT_TRUE(db.Open(db_path()));
EXPECT_TRUE(table()->CreateTable(&db));
delegate()->set_rate_limits({
.time_window = base::TimeDelta::FromDays(4),
.max_attributions_per_window = 2,
});
const url::Origin example_a = url::Origin::Create(GURL("https://a.example/"));
const url::Origin example_c = url::Origin::Create(GURL("https://c.example/"));
const url::Origin example_c_sub_a =
url::Origin::Create(GURL("https://a.c.example/"));
const url::Origin example_c_sub_b =
url::Origin::Create(GURL("https://b.c.example/"));
// Add distinct subdomains on the same origin to ensure correct use of
// ConversionDestination.
EXPECT_TRUE(table()->AddRateLimit(
&db, NewConversionReport(example_a, example_c_sub_a)));
clock()->Advance(base::TimeDelta::FromDays(3));
EXPECT_TRUE(table()->AddRateLimit(
&db, NewConversionReport(example_a, example_c_sub_b)));
base::Time now = clock()->Now();
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_a, example_c_sub_a), now));
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_a, example_c_sub_b), now));
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_a, example_c), now));
}
TEST_F(RateLimitTableTest, IsAttributionAllowed_ImpressionSiteSubdomains) {
sql::Database db;
EXPECT_TRUE(db.Open(db_path()));
EXPECT_TRUE(table()->CreateTable(&db));
delegate()->set_rate_limits({
.time_window = base::TimeDelta::FromDays(4),
.max_attributions_per_window = 2,
});
const url::Origin example_a = url::Origin::Create(GURL("https://a.example/"));
const url::Origin example_c = url::Origin::Create(GURL("https://c.example/"));
const url::Origin example_c_sub_a =
url::Origin::Create(GURL("https://a.c.example/"));
const url::Origin example_c_sub_b =
url::Origin::Create(GURL("https://b.c.example/"));
// Add distinct subdomains on the same origin to ensure correct use of
// impression_site.
EXPECT_TRUE(table()->AddRateLimit(
&db, NewConversionReport(example_c_sub_a, example_a)));
clock()->Advance(base::TimeDelta::FromDays(3));
EXPECT_TRUE(table()->AddRateLimit(
&db, NewConversionReport(example_c_sub_b, example_a)));
base::Time now = clock()->Now();
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_c_sub_a, example_a), now));
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_c_sub_b, example_a), now));
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_c, example_a), now));
}
TEST_F(RateLimitTableTest, ClearAllDataAllTime) {
sql::Database db;
EXPECT_TRUE(db.Open(db_path()));
EXPECT_TRUE(table()->CreateTable(&db));
const url::Origin example_a = url::Origin::Create(GURL("https://a.example/"));
const url::Origin example_b = url::Origin::Create(GURL("https://b.example/"));
EXPECT_TRUE(
table()->AddRateLimit(&db, NewConversionReport(example_a, example_b)));
EXPECT_TRUE(
table()->AddRateLimit(&db, NewConversionReport(example_b, example_a)));
EXPECT_EQ(2u, GetRateLimitRows(&db));
EXPECT_TRUE(table()->ClearAllDataAllTime(&db));
EXPECT_EQ(0u, GetRateLimitRows(&db));
}
TEST_F(RateLimitTableTest, ClearAllDataInRange) {
sql::Database db;
EXPECT_TRUE(db.Open(db_path()));
EXPECT_TRUE(table()->CreateTable(&db));
delegate()->set_rate_limits({
.time_window = base::TimeDelta::Max(),
.max_attributions_per_window = 2,
});
const url::Origin example_a = url::Origin::Create(GURL("https://a.example/"));
const url::Origin example_b = url::Origin::Create(GURL("https://b.example/"));
const url::Origin example_c = url::Origin::Create(GURL("https://c.example/"));
EXPECT_TRUE(
table()->AddRateLimit(&db, NewConversionReport(example_a, example_b)));
clock()->Advance(base::TimeDelta::FromDays(2));
EXPECT_TRUE(
table()->AddRateLimit(&db, NewConversionReport(example_a, example_b)));
clock()->Advance(base::TimeDelta::FromDays(2));
EXPECT_TRUE(
table()->AddRateLimit(&db, NewConversionReport(example_b, example_c)));
clock()->Advance(base::TimeDelta::FromDays(2));
EXPECT_TRUE(
table()->AddRateLimit(&db, NewConversionReport(example_b, example_c)));
EXPECT_EQ(4u, GetRateLimitRows(&db));
base::Time now = clock()->Now();
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_a, example_b), now));
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_b, example_c), now));
// Delete the first row: attribution should now be allowed for the site,
// but the other rows should not be deleted.
EXPECT_TRUE(table()->ClearAllDataInRange(&db,
now - base::TimeDelta::FromDays(7),
now - base::TimeDelta::FromDays(6)));
EXPECT_EQ(3u, GetRateLimitRows(&db));
EXPECT_TRUE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_a, example_b), now));
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_b, example_c), now));
}
TEST_F(RateLimitTableTest, ClearDataForOriginsInRange) {
sql::Database db;
EXPECT_TRUE(db.Open(db_path()));
EXPECT_TRUE(table()->CreateTable(&db));
delegate()->set_rate_limits({
.time_window = base::TimeDelta::Max(),
.max_attributions_per_window = 2,
});
const url::Origin example_a = url::Origin::Create(GURL("https://a.example/"));
const url::Origin example_b = url::Origin::Create(GURL("https://b.example/"));
const url::Origin example_ba =
url::Origin::Create(GURL("https://a.b.example/"));
const url::Origin example_bb =
url::Origin::Create(GURL("https://b.b.example/"));
const url::Origin example_c = url::Origin::Create(GURL("https://c.example/"));
const url::Origin example_d = url::Origin::Create(GURL("https://d.example/"));
EXPECT_TRUE(
table()->AddRateLimit(&db, NewConversionReport(example_a, example_ba)));
clock()->Advance(base::TimeDelta::FromDays(2));
EXPECT_TRUE(
table()->AddRateLimit(&db, NewConversionReport(example_a, example_bb)));
clock()->Advance(base::TimeDelta::FromDays(2));
EXPECT_TRUE(
table()->AddRateLimit(&db, NewConversionReport(example_d, example_c)));
EXPECT_EQ(3u, GetRateLimitRows(&db));
base::Time now = clock()->Now();
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_a, example_b), now));
// Should delete nothing, because (example_d, example_c) is at now.
EXPECT_TRUE(table()->ClearDataForOriginsInRange(
&db, base::Time(), now - base::TimeDelta::FromDays(1),
base::BindRepeating(std::equal_to<url::Origin>(), example_c)));
EXPECT_EQ(3u, GetRateLimitRows(&db));
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_a, example_b), now));
// Should delete (example_a, example_ba).
EXPECT_TRUE(table()->ClearDataForOriginsInRange(
&db, base::Time(), base::Time::Max(),
base::BindRepeating(std::equal_to<url::Origin>(), example_ba)));
EXPECT_EQ(2u, GetRateLimitRows(&db));
EXPECT_TRUE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_a, example_b), now));
// Should delete (example_d, example_c), the only report >= now.
EXPECT_TRUE(table()->ClearDataForOriginsInRange(
&db, now, base::Time::Max(),
base::BindRepeating([](const url::Origin& origin) { return true; })));
EXPECT_EQ(1u, GetRateLimitRows(&db));
// Should delete (example_a, example_bb).
EXPECT_TRUE(table()->ClearDataForOriginsInRange(
&db, base::Time(), base::Time::Max(),
base::BindRepeating(std::equal_to<url::Origin>(), example_a)));
EXPECT_EQ(0u, GetRateLimitRows(&db));
}
TEST_F(RateLimitTableTest, AddRateLimit_DeletesExpiredRateLimits) {
sql::Database db;
EXPECT_TRUE(db.Open(db_path()));
EXPECT_TRUE(table()->CreateTable(&db));
EXPECT_TRUE(table()->AddRateLimit(
&db,
NewConversionReport(url::Origin::Create(GURL("https://a.example/")),
url::Origin::Create(GURL("https://b.example/")))));
EXPECT_TRUE(table()->AddRateLimit(
&db,
NewConversionReport(url::Origin::Create(GURL("https://c.example/")),
url::Origin::Create(GURL("https://d.example/")))));
delegate()->set_rate_limits({
.time_window = base::TimeDelta::FromDays(3),
.max_attributions_per_window = INT_MAX,
});
clock()->Advance(base::TimeDelta::FromDays(4));
EXPECT_TRUE(table()->AddRateLimit(
&db,
NewConversionReport(url::Origin::Create(GURL("https://e.example/")),
url::Origin::Create(GURL("https://f.example/")))));
EXPECT_EQ(1u, GetRateLimitRows(&db));
}
TEST_F(RateLimitTableTest, ClearDataForImpressionIds) {
sql::Database db;
EXPECT_TRUE(db.Open(db_path()));
EXPECT_TRUE(table()->CreateTable(&db));
delegate()->set_rate_limits({
.time_window = base::TimeDelta::Max(),
.max_attributions_per_window = 2,
});
const url::Origin example_a = url::Origin::Create(GURL("https://a.example/"));
const url::Origin example_b = url::Origin::Create(GURL("https://b.example/"));
const url::Origin example_c = url::Origin::Create(GURL("https://c.example/"));
const url::Origin example_d = url::Origin::Create(GURL("https://d.example/"));
base::Time now = clock()->Now();
EXPECT_TRUE(table()->AddRateLimit(
&db, NewConversionReport(example_a, example_b, /*impression_id=*/1)));
EXPECT_TRUE(table()->AddRateLimit(
&db, NewConversionReport(example_a, example_b, /*impression_id=*/2)));
EXPECT_TRUE(table()->AddRateLimit(
&db, NewConversionReport(example_c, example_d, /*impression_id=*/3)));
EXPECT_TRUE(table()->AddRateLimit(
&db, NewConversionReport(example_c, example_d, /*impression_id=*/4)));
EXPECT_EQ(4u, GetRateLimitRows(&db));
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_a, example_b), now));
EXPECT_FALSE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_c, example_d), now));
EXPECT_TRUE(table()->ClearDataForImpressionIds(&db, {1, 4}));
EXPECT_EQ(2u, GetRateLimitRows(&db));
EXPECT_TRUE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_a, example_b), now));
EXPECT_TRUE(table()->IsAttributionAllowed(
&db, NewConversionReport(example_c, example_d), now));
}
} // namespace content