blob: 3bc3b378f9491a3b3d9cc6a7a90d9bde80a18bcf [file] [log] [blame]
// Copyright 2020 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/conversion_storage_sql.h"
#include <functional>
#include <memory>
#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.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 "content/browser/conversions/storable_conversion.h"
#include "content/browser/conversions/storable_impression.h"
#include "sql/database.h"
#include "sql/test/scoped_error_expecter.h"
#include "sql/test/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace content {
class ConversionStorageSqlTest : public testing::Test {
public:
ConversionStorageSqlTest() = default;
void SetUp() override { ASSERT_TRUE(temp_directory_.CreateUniqueTempDir()); }
void OpenDatabase() {
storage_.reset();
auto delegate = std::make_unique<ConfigurableStorageDelegate>();
delegate_ = delegate.get();
storage_ = std::make_unique<ConversionStorageSql>(
temp_directory_.GetPath(), std::move(delegate), &clock_);
}
void CloseDatabase() { storage_.reset(); }
void AddReportToStorage() {
storage_->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
storage_->MaybeCreateAndStoreConversionReports(DefaultConversion());
}
base::FilePath db_path() {
return temp_directory_.GetPath().Append(FILE_PATH_LITERAL("Conversions"));
}
base::SimpleTestClock* clock() { return &clock_; }
ConversionStorage* storage() { return storage_.get(); }
ConfigurableStorageDelegate* delegate() { return delegate_; }
protected:
base::ScopedTempDir temp_directory_;
private:
std::unique_ptr<ConversionStorage> storage_;
ConfigurableStorageDelegate* delegate_ = nullptr;
base::SimpleTestClock clock_;
};
TEST_F(ConversionStorageSqlTest,
DatabaseInitialized_TablesAndIndexesLazilyInitialized) {
base::HistogramTester histograms;
OpenDatabase();
CloseDatabase();
// An unused ConversionStorageSql instance should not create the database.
EXPECT_FALSE(base::PathExists(db_path()));
// Operations which don't need to run on an empty database should not create
// the database.
OpenDatabase();
EXPECT_EQ(0u, storage()->GetConversionsToReport(clock()->Now()).size());
CloseDatabase();
EXPECT_FALSE(base::PathExists(db_path()));
// DB init UMA should not be recorded.
histograms.ExpectTotalCount("Conversions.Storage.CreationTime", 0);
histograms.ExpectTotalCount("Conversions.Storage.MigrationTime", 0);
// Storing an impression should create and initialize the database.
OpenDatabase();
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
CloseDatabase();
// DB creation histograms should be recorded.
histograms.ExpectTotalCount("Conversions.Storage.CreationTime", 1);
histograms.ExpectTotalCount("Conversions.Storage.MigrationTime", 0);
{
sql::Database raw_db;
EXPECT_TRUE(raw_db.Open(db_path()));
// [impressions], [conversions], [meta], [rate_limits].
EXPECT_EQ(4u, sql::test::CountSQLTables(&raw_db));
// [conversion_domain_idx], [impression_expiry_idx],
// [impression_origin_idx], [conversion_report_time_idx],
// [conversion_impression_id_idx], [rate_limit_origin_type_idx],
// [rate_limit_conversion_time_idx], [rate_limit_impression_id_idx] and the
// meta table index.
EXPECT_EQ(9u, sql::test::CountSQLIndices(&raw_db));
}
}
TEST_F(ConversionStorageSqlTest, DatabaseReopened_DataPersisted) {
OpenDatabase();
AddReportToStorage();
EXPECT_EQ(1u, storage()->GetConversionsToReport(clock()->Now()).size());
CloseDatabase();
OpenDatabase();
EXPECT_EQ(1u, storage()->GetConversionsToReport(clock()->Now()).size());
}
TEST_F(ConversionStorageSqlTest, CorruptDatabase_RecoveredOnOpen) {
OpenDatabase();
AddReportToStorage();
EXPECT_EQ(1u, storage()->GetConversionsToReport(clock()->Now()).size());
CloseDatabase();
// Corrupt the database.
EXPECT_TRUE(sql::test::CorruptSizeInHeader(db_path()));
sql::test::ScopedErrorExpecter expecter;
expecter.ExpectError(SQLITE_CORRUPT);
// Open that database and ensure that it does not fail.
EXPECT_NO_FATAL_FAILURE(OpenDatabase());
// Data should be recovered.
EXPECT_EQ(1u, storage()->GetConversionsToReport(clock()->Now()).size());
EXPECT_TRUE(expecter.SawExpectedErrors());
}
// Create an impression with two conversions (C1 and C2). Craft a query that
// will target C2, which will in turn delete the impression. We should ensure
// that C1 is properly deleted (conversions should not be stored unattributed).
TEST_F(ConversionStorageSqlTest, ClearDataWithVestigialConversion) {
base::HistogramTester histograms;
OpenDatabase();
base::Time start = clock()->Now();
auto impression =
ImpressionBuilder(start).SetExpiry(base::TimeDelta::FromDays(30)).Build();
storage()->StoreImpression(impression);
clock()->Advance(base::TimeDelta::FromDays(1));
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
clock()->Advance(base::TimeDelta::FromDays(1));
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
// Use a time range that only intersects the last conversion.
storage()->ClearData(clock()->Now(), clock()->Now(),
base::BindRepeating(std::equal_to<url::Origin>(),
impression.impression_origin()));
EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty());
CloseDatabase();
// Verify that everything is deleted.
sql::Database raw_db;
EXPECT_TRUE(raw_db.Open(db_path()));
size_t conversion_rows;
size_t impression_rows;
size_t rate_limit_rows;
sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows);
sql::test::CountTableRows(&raw_db, "impressions", &impression_rows);
sql::test::CountTableRows(&raw_db, "rate_limits", &rate_limit_rows);
EXPECT_EQ(0u, conversion_rows);
EXPECT_EQ(0u, impression_rows);
EXPECT_EQ(0u, rate_limit_rows);
histograms.ExpectUniqueSample(
"Conversions.ImpressionsDeletedInDataClearOperation", 1, 1);
histograms.ExpectUniqueSample(
"Conversions.ReportsDeletedInDataClearOperation", 2, 1);
}
// Same as the above test, but with a null filter.
TEST_F(ConversionStorageSqlTest, ClearAllDataWithVestigialConversion) {
base::HistogramTester histograms;
OpenDatabase();
base::Time start = clock()->Now();
auto impression =
ImpressionBuilder(start).SetExpiry(base::TimeDelta::FromDays(30)).Build();
storage()->StoreImpression(impression);
clock()->Advance(base::TimeDelta::FromDays(1));
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
clock()->Advance(base::TimeDelta::FromDays(1));
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
// Use a time range that only intersects the last conversion.
auto null_filter = base::RepeatingCallback<bool(const url::Origin&)>();
storage()->ClearData(clock()->Now(), clock()->Now(), null_filter);
EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty());
CloseDatabase();
// Verify that everything is deleted.
sql::Database raw_db;
EXPECT_TRUE(raw_db.Open(db_path()));
size_t conversion_rows;
size_t impression_rows;
size_t rate_limit_rows;
sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows);
sql::test::CountTableRows(&raw_db, "impressions", &impression_rows);
sql::test::CountTableRows(&raw_db, "rate_limits", &rate_limit_rows);
EXPECT_EQ(0u, conversion_rows);
EXPECT_EQ(0u, impression_rows);
EXPECT_EQ(0u, rate_limit_rows);
histograms.ExpectUniqueSample(
"Conversions.ImpressionsDeletedInDataClearOperation", 1, 1);
histograms.ExpectUniqueSample(
"Conversions.ReportsDeletedInDataClearOperation", 2, 1);
}
// The max time range with a null filter should delete everything.
TEST_F(ConversionStorageSqlTest, DeleteEverything) {
base::HistogramTester histograms;
OpenDatabase();
base::Time start = clock()->Now();
for (int i = 0; i < 10; i++) {
auto impression = ImpressionBuilder(start)
.SetExpiry(base::TimeDelta::FromDays(30))
.Build();
storage()->StoreImpression(impression);
clock()->Advance(base::TimeDelta::FromDays(1));
}
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
clock()->Advance(base::TimeDelta::FromDays(1));
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
auto null_filter = base::RepeatingCallback<bool(const url::Origin&)>();
storage()->ClearData(base::Time::Min(), base::Time::Max(), null_filter);
EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty());
CloseDatabase();
// Verify that everything is deleted.
sql::Database raw_db;
EXPECT_TRUE(raw_db.Open(db_path()));
size_t conversion_rows;
size_t impression_rows;
size_t rate_limit_rows;
sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows);
sql::test::CountTableRows(&raw_db, "impressions", &impression_rows);
sql::test::CountTableRows(&raw_db, "rate_limits", &rate_limit_rows);
EXPECT_EQ(0u, conversion_rows);
EXPECT_EQ(0u, impression_rows);
EXPECT_EQ(0u, rate_limit_rows);
histograms.ExpectUniqueSample(
"Conversions.ImpressionsDeletedInDataClearOperation", 1, 1);
histograms.ExpectUniqueSample(
"Conversions.ReportsDeletedInDataClearOperation", 2, 1);
}
TEST_F(ConversionStorageSqlTest, MaxImpressionsPerOrigin) {
OpenDatabase();
delegate()->set_max_impressions_per_origin(2);
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
CloseDatabase();
sql::Database raw_db;
EXPECT_TRUE(raw_db.Open(db_path()));
size_t impression_rows;
sql::test::CountTableRows(&raw_db, "impressions", &impression_rows);
EXPECT_EQ(1u, impression_rows);
size_t rate_limit_rows;
sql::test::CountTableRows(&raw_db, "rate_limits", &rate_limit_rows);
EXPECT_EQ(1u, rate_limit_rows);
}
TEST_F(ConversionStorageSqlTest, MaxConversionsPerOrigin) {
OpenDatabase();
delegate()->set_max_conversions_per_origin(2);
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
EXPECT_EQ(
1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
EXPECT_EQ(
0, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion()));
CloseDatabase();
sql::Database raw_db;
EXPECT_TRUE(raw_db.Open(db_path()));
size_t conversion_rows;
sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows);
EXPECT_EQ(2u, conversion_rows);
size_t rate_limit_rows;
sql::test::CountTableRows(&raw_db, "rate_limits", &rate_limit_rows);
EXPECT_EQ(2u, rate_limit_rows);
}
TEST_F(ConversionStorageSqlTest,
DeleteRateLimitRowsForSubdomainImpressionOrigin) {
OpenDatabase();
delegate()->set_max_conversions_per_impression(1);
delegate()->set_rate_limits({
.time_window = base::TimeDelta::FromDays(7),
.max_attributions_per_window = INT_MAX,
});
const url::Origin impression_origin =
url::Origin::Create(GURL("https://sub.impression.example/"));
const url::Origin reporting_origin =
url::Origin::Create(GURL("https://a.example/"));
const url::Origin conversion_origin =
url::Origin::Create(GURL("https://b.example/"));
storage()->StoreImpression(ImpressionBuilder(clock()->Now())
.SetExpiry(base::TimeDelta::FromDays(30))
.SetImpressionOrigin(impression_origin)
.SetReportingOrigin(reporting_origin)
.SetConversionOrigin(conversion_origin)
.Build());
clock()->Advance(base::TimeDelta::FromDays(1));
StorableConversion conversion("1", net::SchemefulSite(conversion_origin),
reporting_origin);
EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion));
clock()->Advance(base::TimeDelta::FromDays(1));
EXPECT_TRUE(storage()->DeleteConversion(1));
EXPECT_EQ(1, storage()->DeleteExpiredImpressions());
storage()->ClearData(
base::Time::Min(), base::Time::Max(),
base::BindRepeating(std::equal_to<url::Origin>(), impression_origin));
CloseDatabase();
sql::Database raw_db;
EXPECT_TRUE(raw_db.Open(db_path()));
size_t conversion_rows;
sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows);
EXPECT_EQ(0u, conversion_rows);
size_t rate_limit_rows;
sql::test::CountTableRows(&raw_db, "rate_limits", &rate_limit_rows);
EXPECT_EQ(0u, rate_limit_rows);
}
TEST_F(ConversionStorageSqlTest,
DeleteRateLimitRowsForSubdomainConversionOrigin) {
OpenDatabase();
delegate()->set_max_conversions_per_impression(1);
delegate()->set_rate_limits({
.time_window = base::TimeDelta::FromDays(7),
.max_attributions_per_window = INT_MAX,
});
const url::Origin impression_origin =
url::Origin::Create(GURL("https://b.example/"));
const url::Origin reporting_origin =
url::Origin::Create(GURL("https://a.example/"));
const url::Origin conversion_origin =
url::Origin::Create(GURL("https://sub.impression.example/"));
storage()->StoreImpression(ImpressionBuilder(clock()->Now())
.SetExpiry(base::TimeDelta::FromDays(30))
.SetImpressionOrigin(impression_origin)
.SetReportingOrigin(reporting_origin)
.SetConversionOrigin(conversion_origin)
.Build());
clock()->Advance(base::TimeDelta::FromDays(1));
StorableConversion conversion("1", net::SchemefulSite(conversion_origin),
reporting_origin);
EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion));
clock()->Advance(base::TimeDelta::FromDays(1));
EXPECT_TRUE(storage()->DeleteConversion(1));
EXPECT_EQ(1, storage()->DeleteExpiredImpressions());
storage()->ClearData(
base::Time::Min(), base::Time::Max(),
base::BindRepeating(std::equal_to<url::Origin>(), conversion_origin));
CloseDatabase();
sql::Database raw_db;
EXPECT_TRUE(raw_db.Open(db_path()));
size_t conversion_rows;
sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows);
EXPECT_EQ(0u, conversion_rows);
size_t rate_limit_rows;
sql::test::CountTableRows(&raw_db, "rate_limits", &rate_limit_rows);
EXPECT_EQ(0u, rate_limit_rows);
}
TEST_F(ConversionStorageSqlTest, CantOpenDb_FailsSilentlyInRelease) {
base::CreateDirectoryAndGetError(db_path(), nullptr);
auto sql_storage = std::make_unique<ConversionStorageSql>(
temp_directory_.GetPath(),
std::make_unique<ConfigurableStorageDelegate>(), clock());
sql_storage->set_ignore_errors_for_testing(true);
std::unique_ptr<ConversionStorage> storage = std::move(sql_storage);
// These calls should be no-ops.
storage->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
EXPECT_EQ(0,
storage->MaybeCreateAndStoreConversionReports(DefaultConversion()));
}
TEST_F(ConversionStorageSqlTest, DatabaseDirDoesExist_CreateDirAndOpenDB) {
// Give the storage layer a database directory that doesn't exist.
std::unique_ptr<ConversionStorage> storage =
std::make_unique<ConversionStorageSql>(
temp_directory_.GetPath().Append(
FILE_PATH_LITERAL("ConversionFolder/")),
std::make_unique<ConfigurableStorageDelegate>(), clock());
// The directory should be created, and the database opened.
storage->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
EXPECT_EQ(1,
storage->MaybeCreateAndStoreConversionReports(DefaultConversion()));
}
TEST_F(ConversionStorageSqlTest, DBinitializationSucceeds_HistogramRecorded) {
base::HistogramTester histograms;
OpenDatabase();
storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build());
CloseDatabase();
histograms.ExpectUniqueSample("Conversions.Storage.Sql.InitStatus",
ConversionStorageSql::InitStatus::kSuccess, 1);
}
} // namespace content