blob: 2959924734b0ffbbc3df99b1ce869926cfce555f [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/btm/btm_database.h"
#include <cstdio>
#include <optional>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/time/time.h"
#include "content/browser/btm/btm_test_utils.h"
#include "content/browser/btm/btm_utils.h"
#include "content/public/common/btm_utils.h"
#include "content/public/common/content_features.h"
#include "sql/database.h"
#include "sql/sqlite_result_code_values.h"
#include "sql/test/scoped_error_expecter.h"
#include "sql/test/test_helpers.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
using base::Time;
using testing::Optional;
namespace content {
namespace {
class TestDatabase : public BtmDatabase {
public:
explicit TestDatabase(const std::optional<base::FilePath>& db_path)
: BtmDatabase(db_path) {}
void LogDatabaseMetricsForTesting() { LogDatabaseMetrics(); }
};
enum ColumnType {
kUserActivation,
kBounce,
kWebAuthnAssertion
};
} // namespace
class BtmDatabaseTest : public testing::Test {
public:
explicit BtmDatabaseTest(bool in_memory) : in_memory_(in_memory) {}
// Small delta used to test before/after timestamps made with
// FromSecondsSinceUnixEpoch.
base::TimeDelta tiny_delta = base::Milliseconds(1);
TimestampRange ToRange(base::Time& time) { return {{time, time}}; }
protected:
base::SimpleTestClock clock_;
std::unique_ptr<TestDatabase> db_;
base::ScopedTempDir temp_dir_;
base::FilePath db_path_;
base::test::ScopedFeatureList features_;
// Test setup.
void SetUp() override {
if (in_memory_) {
db_ = std::make_unique<TestDatabase>(std::nullopt);
} else {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
db_path_ = temp_dir_.GetPath().AppendASCII("BTM.db");
db_ = std::make_unique<TestDatabase>(db_path_);
}
ASSERT_TRUE(db_->CheckDBInit());
db_->SetClockForTesting(clock());
}
void TearDown() override {
db_.reset();
// Deletes temporary directory from on-disk tests
if (!in_memory_) {
ASSERT_TRUE(temp_dir_.Delete());
}
}
base::Time Now() { return clock_.Now(); }
void AdvanceTimeTo(base::Time now) {
ASSERT_GE(now, clock_.Now());
clock_.SetNow(now);
}
void AdvanceTimeBy(base::TimeDelta delta) { clock_.Advance(delta); }
base::Clock* clock() { return &clock_; }
private:
bool in_memory_;
};
class BtmDatabaseErrorHistogramsTest
: public BtmDatabaseTest,
public testing::WithParamInterface<bool> {
public:
BtmDatabaseErrorHistogramsTest() : BtmDatabaseTest(GetParam()) {}
void SetUp() override {
BtmDatabaseTest::SetUp();
// Use inf ttl to prevent interactions (including web authn assertions) from
// expiring unintentionally.
features_.InitAndEnableFeatureWithParameters(features::kBtmTtl,
{{"interaction_ttl", "inf"}});
}
};
TEST_P(BtmDatabaseErrorHistogramsTest, kRead_EmptySite_InDb) {
base::HistogramTester histograms;
// Manually write an entry with an empty string `site`, then try to read it.
ASSERT_TRUE(db_->ExecuteSqlForTesting(
"INSERT INTO "
"bounces(site,first_bounce_time,last_bounce_time) VALUES ('',1,5)"));
EXPECT_EQ(db_->GetEntryCount(BtmDatabaseTable::kBounces), 1u);
EXPECT_EQ(db_->Read(""), std::nullopt);
histograms.ExpectUniqueSample("Privacy.DIPS.DIPSErrorCodes",
BtmErrorCode::kRead_EmptySite_InDb, 1);
// Verify the entry was deleted during the read attempt.
EXPECT_EQ(db_->GetEntryCount(BtmDatabaseTable::kBounces), 0u);
}
TEST_P(BtmDatabaseErrorHistogramsTest, Read_EmptySite_NotInDb) {
base::HistogramTester histograms;
EXPECT_EQ(db_->Read(""), std::nullopt);
histograms.ExpectUniqueSample("Privacy.DIPS.DIPSErrorCodes",
BtmErrorCode::kRead_EmptySite_NotInDb, 1);
}
TEST_P(BtmDatabaseErrorHistogramsTest, Write_EmptySite) {
base::HistogramTester histograms;
// Attempt to add a bounce for an empty site.
const std::string empty_site = GetSiteForBtm(GURL(""));
TimestampRange bounce(
{Time::FromSecondsSinceUnixEpoch(1), Time::FromSecondsSinceUnixEpoch(1)});
EXPECT_FALSE(
db_->Write(empty_site, TimestampRange(), bounce, TimestampRange()));
histograms.ExpectUniqueSample("Privacy.DIPS.DIPSErrorCodes",
BtmErrorCode::kWrite_EmptySite, 1);
}
// Verifies the histograms logged for the success case (i.e., writing an entry
// with a non-empty site).
TEST_P(BtmDatabaseErrorHistogramsTest, Write_None) {
base::HistogramTester histograms;
// Add a bounce for a non-empty site.
const std::string site = GetSiteForBtm(GURL("https://example.test"));
TimestampRange bounce(
{Time::FromSecondsSinceUnixEpoch(1), Time::FromSecondsSinceUnixEpoch(1)});
EXPECT_TRUE(db_->Write(site, TimestampRange(), bounce, TimestampRange()));
histograms.ExpectUniqueSample("Privacy.DIPS.DIPSErrorCodes",
BtmErrorCode::kWrite_None, 1);
}
INSTANTIATE_TEST_SUITE_P(All,
BtmDatabaseErrorHistogramsTest,
::testing::Bool());
// A test class that lets us ensure that we can add, read, update, and delete
// bounces for all columns in the BtmDatabase. Parameterized over whether the
// db is in memory, and what column we're testing.
class BtmDatabaseAllColumnTest
: public BtmDatabaseTest,
public testing::WithParamInterface<std::tuple<bool, ColumnType>> {
public:
BtmDatabaseAllColumnTest()
: BtmDatabaseTest(std::get<0>(GetParam())),
column_(std::get<1>(GetParam())) {}
void SetUp() override {
BtmDatabaseTest::SetUp();
// Use inf ttl to prevent interactions (including webauthn assertions) from
// expiring unintentionally.
features_.InitAndEnableFeatureWithParameters(features::kBtmTtl,
{{"interaction_ttl", "inf"}});
}
protected:
bool IsBounce(ColumnType column) { return column == kBounce; }
// Uses `times` to write to the first and last columns for `column_` in the
// `site` row in `db`. This also writes the empty time stamps to all other
// columns in `db` that are unrelated.
bool WriteToVariableColumn(const std::string& site,
const TimestampRange& times) {
return db_->Write(site,
column_ == kUserActivation ? times : TimestampRange(),
IsBounce(column_) ? times : TimestampRange(),
column_ == kWebAuthnAssertion ? times : TimestampRange());
}
TimestampRange ReadValueForVariableColumn(std::optional<StateValue> value) {
switch (column_) {
case ColumnType::kUserActivation:
return value->user_activation_times;
case ColumnType::kBounce:
return value->bounce_times;
case ColumnType::kWebAuthnAssertion:
return value->web_authn_assertion_times;
}
}
std::pair<std::string, std::string> GetVariableColumnNames() {
switch (column_) {
case ColumnType::kUserActivation:
return {"first_user_activation_time", "last_user_activation_time"};
case ColumnType::kBounce:
return {"first_bounce_time", "last_bounce_time"};
case ColumnType::kWebAuthnAssertion:
return {"first_web_authn_assertion_time",
"last_web_authn_assertion_time"};
}
}
private:
ColumnType column_;
};
// Test adding entries in the `bounces` table of the BtmDatabase.
TEST_P(BtmDatabaseAllColumnTest, AddBounce) {
// Add a bounce for site.
const std::string site = GetSiteForBtm(GURL("http://www.youtube.com/"));
TimestampRange bounce_1(
{Time::FromSecondsSinceUnixEpoch(1), Time::FromSecondsSinceUnixEpoch(1)});
EXPECT_TRUE(WriteToVariableColumn(site, bounce_1));
// Verify that site is in `bounces` using Read().
EXPECT_TRUE(db_->Read(site).has_value());
}
// Test updating entries in the `bounces` table of the BtmDatabase.
TEST_P(BtmDatabaseAllColumnTest, UpdateBounce) {
// Add a bounce for site.
const std::string site = GetSiteForBtm(GURL("http://www.youtube.com/"));
TimestampRange bounce_1(
{Time::FromSecondsSinceUnixEpoch(1), Time::FromSecondsSinceUnixEpoch(1)});
EXPECT_TRUE(WriteToVariableColumn(site, bounce_1));
// Verify that site's entry in `bounces` is now at t = 1
EXPECT_EQ(ReadValueForVariableColumn(db_->Read(site)), bounce_1);
// Update site's entry with a bounce at t = 2
TimestampRange bounce_2(
{Time::FromSecondsSinceUnixEpoch(2), Time::FromSecondsSinceUnixEpoch(3)});
EXPECT_TRUE(WriteToVariableColumn(site, bounce_2));
// Verify that site's entry in `bounces` is now at t = 2
EXPECT_EQ(ReadValueForVariableColumn(db_->Read(site)), bounce_2);
}
// Test deleting an entry from the `bounces` table of the BtmDatabase.
TEST_P(BtmDatabaseAllColumnTest, DeleteBounce) {
// Add a bounce for site.
const std::string site = GetSiteForBtm(GURL("http://www.youtube.com/"));
TimestampRange bounce(
{Time::FromSecondsSinceUnixEpoch(1), Time::FromSecondsSinceUnixEpoch(1)});
EXPECT_TRUE(WriteToVariableColumn(site, bounce));
// Verify that site has state tracked in bounces.
EXPECT_TRUE(db_->Read(site).has_value());
// Delete site's entry in bounces.
EXPECT_TRUE(db_->RemoveRow(BtmDatabaseTable::kBounces, site));
// Query the bounces for site, making sure there is no state now.
EXPECT_FALSE(db_->Read(site).has_value());
}
// Test deleting many entries from the `bounces` table of the BtmDatabase.
TEST_P(BtmDatabaseAllColumnTest, DeleteSeveralBounces) {
// Add a bounce for site.
const std::string site1 = GetSiteForBtm(GURL("http://www.youtube.com/"));
const std::string site2 = GetSiteForBtm(GURL("http://www.picasa.com/"));
TimestampRange bounce(
{Time::FromSecondsSinceUnixEpoch(1), Time::FromSecondsSinceUnixEpoch(1)});
EXPECT_TRUE(WriteToVariableColumn(site1, bounce));
EXPECT_TRUE(WriteToVariableColumn(site2, bounce));
// Verify that both sites are in the bounces table.
EXPECT_TRUE(db_->Read(site1).has_value());
EXPECT_TRUE(db_->Read(site2).has_value());
// Delete site's entry in bounces.
EXPECT_TRUE(db_->RemoveRows(BtmDatabaseTable::kBounces, {site1, site2}));
// Query the bounces for site, making sure there is no state now.
EXPECT_FALSE(db_->Read(site1).has_value());
EXPECT_FALSE(db_->Read(site2).has_value());
}
// Test reading the `bounces` table of the BtmDatabase.
TEST_P(BtmDatabaseAllColumnTest, ReadBounce) {
// Add a bounce for site.
const std::string site = GetSiteForBtm(GURL("https://example.test"));
TimestampRange bounce(
{Time::FromSecondsSinceUnixEpoch(1), Time::FromSecondsSinceUnixEpoch(1)});
EXPECT_TRUE(WriteToVariableColumn(site, bounce));
EXPECT_EQ(ReadValueForVariableColumn(db_->Read(site)), bounce);
// Query a site that never had BTM State, verifying that is has no entry.
EXPECT_FALSE(
db_->Read(GetSiteForBtm(GURL("https://www.not-in-db.com/"))).has_value());
}
// Verifies actions on the `popups` table of the BTM database.
class BtmDatabasePopupsTest : public BtmDatabaseTest,
public testing::WithParamInterface<bool> {
public:
BtmDatabasePopupsTest() : BtmDatabaseTest(GetParam()) {}
};
// Test adding entries in the `popups` table of the BtmDatabase.
TEST_P(BtmDatabasePopupsTest, AddPopup) {
const std::string opener_site =
GetSiteForBtm(GURL("http://www.youtube.com/"));
const std::string popup_site =
GetSiteForBtm(GURL("http://www.doubleclick.net/"));
uint64_t access_id = 123;
base::Time popup_time = Time::FromSecondsSinceUnixEpoch(1);
bool is_current_interaction = true;
bool is_authentication_interaction = true;
EXPECT_TRUE(db_->WritePopup(opener_site, popup_site, access_id, popup_time,
is_current_interaction,
is_authentication_interaction));
auto popups_state_value = db_->ReadPopup(opener_site, popup_site);
ASSERT_TRUE(popups_state_value.has_value());
EXPECT_EQ(popups_state_value.value().access_id, access_id);
EXPECT_EQ(popups_state_value.value().last_popup_time, popup_time);
EXPECT_EQ(popups_state_value.value().is_current_interaction,
is_current_interaction);
EXPECT_EQ(popups_state_value.value().is_authentication_interaction,
is_authentication_interaction);
}
// Test updating entries in the `popups` table of the BtmDatabase.
TEST_P(BtmDatabasePopupsTest, UpdatePopup) {
const std::string opener_site =
GetSiteForBtm(GURL("http://www.youtube.com/"));
const std::string popup_site =
GetSiteForBtm(GURL("http://www.doubleclick.net/"));
uint64_t first_access_id = 123;
uint64_t second_access_id = 456;
base::Time first_popup_time = Time::FromSecondsSinceUnixEpoch(1);
base::Time second_popup_time = Time::FromSecondsSinceUnixEpoch(2);
// Write the initial entry and verify it was added to the db.
EXPECT_TRUE(db_->WritePopup(
opener_site, popup_site, first_access_id, first_popup_time,
/*is_current_interaction=*/true, /*is_authentication_interaction=*/true));
auto popups_state_value = db_->ReadPopup(opener_site, popup_site);
EXPECT_EQ(popups_state_value.value_or(PopupsStateValue()).last_popup_time,
first_popup_time);
EXPECT_EQ(popups_state_value.value().is_authentication_interaction, true);
// Update the entry with a new popup time of t = 2, is_current_interaction =
// false, and is_authentication_interaction = false.
EXPECT_TRUE(db_->WritePopup(opener_site, popup_site, second_access_id,
second_popup_time,
/*is_current_interaction=*/false,
/*is_authentication_interaction=*/false));
// Verify the new entry.
popups_state_value = db_->ReadPopup(opener_site, popup_site);
ASSERT_TRUE(popups_state_value.has_value());
EXPECT_EQ(popups_state_value.value().access_id, second_access_id);
EXPECT_EQ(popups_state_value.value().last_popup_time, second_popup_time);
EXPECT_EQ(popups_state_value.value().is_current_interaction, false);
EXPECT_EQ(popups_state_value.value().is_authentication_interaction, false);
}
// Test deleting an entry from the `popups` table of the BtmDatabase. An entry
// should be deleted if the input site matches either opener_site or
// popup_site.
TEST_P(BtmDatabasePopupsTest, DeletePopup) {
const std::string opener_site =
GetSiteForBtm(GURL("http://www.youtube.com/"));
const std::string popup_site =
GetSiteForBtm(GURL("http://www.doubleclick.net/"));
uint64_t access_id = 123;
base::Time popup_time = Time::FromSecondsSinceUnixEpoch(1);
// Write the popup to db, and verify.
EXPECT_TRUE(db_->WritePopup(opener_site, popup_site, access_id, popup_time,
/*is_current_interaction=*/true,
/*is_authentication_interaction=*/false));
EXPECT_TRUE(db_->ReadPopup(opener_site, popup_site).has_value());
// Delete the entry in db by opener_site, and verify.
EXPECT_TRUE(db_->RemoveRow(BtmDatabaseTable::kPopups, opener_site));
EXPECT_FALSE(db_->ReadPopup(opener_site, popup_site).has_value());
// Write the popup to db, and verify.
EXPECT_TRUE(db_->WritePopup(opener_site, popup_site, access_id, popup_time,
/*is_current_interaction=*/true,
/*is_authentication_interaction=*/false));
EXPECT_TRUE(db_->ReadPopup(opener_site, popup_site).has_value());
// Delete the entry in db by popup_site, and verify.
EXPECT_TRUE(db_->RemoveRow(BtmDatabaseTable::kPopups, popup_site));
EXPECT_FALSE(db_->ReadPopup(opener_site, popup_site).has_value());
}
// Test deleting many entries from the `popups` table of the BtmDatabase.
TEST_P(BtmDatabasePopupsTest, DeleteSeveralPopups) {
// Add popups to db.
const std::string opener_site_1 =
GetSiteForBtm(GURL("http://www.youtube.com/"));
const std::string opener_site_2 =
GetSiteForBtm(GURL("http://www.picasa.com/"));
const std::string popup_site =
GetSiteForBtm(GURL("http://www.doubleclick.net/"));
EXPECT_TRUE(db_->WritePopup(
opener_site_1, popup_site,
/*access_id=*/123, Time::FromSecondsSinceUnixEpoch(1),
/*is_current_interaction=*/true, /*is_authentication_interaction=*/true));
EXPECT_TRUE(db_->WritePopup(opener_site_2, popup_site,
/*access_id=*/456,
Time::FromSecondsSinceUnixEpoch(2),
/*is_current_interaction=*/true,
/*is_authentication_interaction=*/false));
// Verify that both sites are in the `popups` table.
EXPECT_TRUE(db_->ReadPopup(opener_site_1, popup_site).has_value());
EXPECT_TRUE(db_->ReadPopup(opener_site_2, popup_site).has_value());
// Delete site's entry in `popups`.
EXPECT_TRUE(db_->RemoveRows(BtmDatabaseTable::kBounces,
{opener_site_1, opener_site_2}));
// Verify that both sites are deleted from the `popups` table.
EXPECT_FALSE(db_->Read(opener_site_1).has_value());
EXPECT_FALSE(db_->Read(opener_site_2).has_value());
}
// Test the `ReadRecentPopupsWithInteraction` function which retrieves a list of
// `popups` table entries with recent popup timestamps.
TEST_P(BtmDatabasePopupsTest, ReadRecentPopupsWithInteraction) {
base::Time now = Now();
// Add popups to db.
const std::string opener_site_1 =
GetSiteForBtm(GURL("http://www.youtube.com/"));
const std::string opener_site_2 =
GetSiteForBtm(GURL("http://www.picasa.com/"));
const std::string opener_site_3 =
GetSiteForBtm(GURL("http://www.google.com/"));
const std::string popup_site =
GetSiteForBtm(GURL("http://www.doubleclick.net/"));
EXPECT_TRUE(db_->WritePopup(opener_site_1, popup_site,
/*access_id=*/123, now - base::Seconds(10),
/*is_current_interaction=*/true,
/*is_authentication_interaction=*/false));
EXPECT_TRUE(db_->WritePopup(opener_site_2, popup_site,
/*access_id=*/456, now - base::Seconds(10),
/*is_current_interaction=*/false,
/*is_authentication_interaction=*/false));
EXPECT_TRUE(db_->WritePopup(opener_site_3, popup_site,
/*access_id=*/789, now - base::Seconds(30),
/*is_current_interaction=*/true,
/*is_authentication_interaction=*/false));
// Verify that all three sites are in the `popups` table.
EXPECT_TRUE(db_->ReadPopup(opener_site_1, popup_site).has_value());
EXPECT_TRUE(db_->ReadPopup(opener_site_2, popup_site).has_value());
EXPECT_TRUE(db_->ReadPopup(opener_site_3, popup_site).has_value());
// Expect no popups recorded in the last 5 seconds.
std::vector<PopupWithTime> very_recent_popups =
db_->ReadRecentPopupsWithInteraction(base::Seconds(5));
EXPECT_TRUE(very_recent_popups.empty());
// Expect one popup in the last 20 seconds with a current interaction.
std::vector<PopupWithTime> recent_popups =
db_->ReadRecentPopupsWithInteraction(base::Seconds(20));
ASSERT_EQ(recent_popups.size(), 1u);
EXPECT_EQ(recent_popups.at(0).opener_site, opener_site_1);
EXPECT_EQ(recent_popups.at(0).popup_site, popup_site);
EXPECT_EQ(recent_popups.at(0).last_popup_time, now - base::Seconds(10));
}
INSTANTIATE_TEST_SUITE_P(All, BtmDatabasePopupsTest, ::testing::Bool());
TEST_P(BtmDatabaseAllColumnTest, ErrorHistograms_OpenEndedRange_NullStart) {
base::HistogramTester histograms;
ASSERT_TRUE(db_->ExecuteSqlForTesting(base::StringPrintf(
"INSERT INTO bounces(site,%s,%s) VALUES ('site.test',NULL,0)",
GetVariableColumnNames().first.c_str(),
GetVariableColumnNames().second.c_str())));
db_->Read("site.test");
histograms.ExpectUniqueSample("Privacy.DIPS.DIPSErrorCodes",
BtmErrorCode::kRead_OpenEndedRange_NullStart,
1);
}
TEST_P(BtmDatabaseAllColumnTest, ErrorHistograms_OpenEndedRange_NullEnd) {
base::HistogramTester histograms;
ASSERT_TRUE(db_->ExecuteSqlForTesting(base::StringPrintf(
"INSERT INTO bounces(site,%s,%s) VALUES ('site.test',0,NULL)",
GetVariableColumnNames().first.c_str(),
GetVariableColumnNames().second.c_str())));
db_->Read("site.test");
histograms.ExpectUniqueSample("Privacy.DIPS.DIPSErrorCodes",
BtmErrorCode::kRead_OpenEndedRange_NullEnd, 1);
}
// Verifies the histograms logged for the success case.
TEST_P(BtmDatabaseAllColumnTest, ErrorHistograms_EmptyRangeExcluded) {
base::HistogramTester histograms;
ASSERT_TRUE(db_->ExecuteSqlForTesting(
base::StringPrintf("INSERT INTO bounces(site,%s,%s) VALUES "
"('empty-site.test',NULL,NULL)",
GetVariableColumnNames().first.c_str(),
GetVariableColumnNames().second.c_str())));
db_->Read("empty-site.test");
histograms.ExpectUniqueSample("Privacy.DIPS.DIPSErrorCodes",
BtmErrorCode::kRead_None, 1);
}
INSTANTIATE_TEST_SUITE_P(
All,
BtmDatabaseAllColumnTest,
::testing::Combine(::testing::Bool(),
::testing::Values(ColumnType::kUserActivation,
ColumnType::kBounce,
ColumnType::kWebAuthnAssertion)));
// A test class that verifies the behavior of the BtmDatabase with respect to
// interactions and Web Authn Assertions (WAA).
//
// Parameterized over whether the db is in memory.
class BtmDatabaseInteractionTest : public BtmDatabaseTest,
public testing::WithParamInterface<bool> {
public:
BtmDatabaseInteractionTest() : BtmDatabaseTest(GetParam()) {
features_.InitWithFeatures({features::kBtmTtl, features::kBtm}, {});
}
// This test only focuses on user activation and WAA times, that's the
// reason why the other times like bounce times not being tested here are left
// NULL throughout.
void LoadDatabase() {
DCHECK(db_);
// Case1: last_web_authn_assertion_time == last_user_activation_time.
EXPECT_TRUE(db_->Write("case1.test", {{dummy_time, dummy_time}}, {},
{{dummy_time, dummy_time}}));
// Case2: last_web_authn_assertion_time > last_user_activation_time.
EXPECT_TRUE(db_->Write("case2.test", {{dummy_time, dummy_time}}, {},
{{dummy_time, dummy_time + tiny_delta}}));
// Case3: last_web_authn_assertion_time < last_user_activation_time.
EXPECT_TRUE(
db_->Write("case3.test", {{dummy_time, dummy_time}}, {},
{{dummy_time - tiny_delta, dummy_time - tiny_delta}}));
// Case4: last_web_authn_assertion_time is NULL.
EXPECT_TRUE(db_->Write("case4.test", {{dummy_time, dummy_time}}, {}, {}));
// Case5: last_user_activation_time is NULL.
EXPECT_TRUE(db_->Write("case5.test", {}, {}, {{dummy_time, dummy_time}}));
// Case6: last_web_authn_assertion_time and last_user_activation_time are
// NULL.
EXPECT_TRUE(db_->Write("case6.test", {}, {}, {}));
}
protected:
base::Time dummy_time = Time::FromSecondsSinceUnixEpoch(100);
};
TEST_P(BtmDatabaseInteractionTest, ClearExpiredRowsFromBouncesTable) {
LoadDatabase();
EXPECT_THAT(
db_->GetAllSitesForTesting(BtmDatabaseTable::kBounces),
testing::UnorderedElementsAre("case1.test", "case2.test", "case3.test",
"case4.test", "case5.test", "case6.test"));
AdvanceTimeTo(dummy_time + features::kBtmInteractionTtl.Get());
EXPECT_EQ(db_->ClearExpiredRows(), 0u);
EXPECT_THAT(
db_->GetAllSitesForTesting(BtmDatabaseTable::kBounces),
testing::UnorderedElementsAre("case1.test", "case2.test", "case3.test",
"case4.test", "case5.test", "case6.test"));
AdvanceTimeTo(dummy_time + features::kBtmInteractionTtl.Get() + tiny_delta);
EXPECT_EQ(db_->ClearExpiredRows(), 4u);
EXPECT_THAT(db_->GetAllSitesForTesting(BtmDatabaseTable::kBounces),
testing::UnorderedElementsAre("case2.test", "case6.test"));
// Time travel to a point by which all interactions and WAAs should've
// expired.
AdvanceTimeTo(dummy_time + features::kBtmInteractionTtl.Get() +
tiny_delta * 2);
EXPECT_EQ(db_->ClearExpiredRows(), 1u);
EXPECT_THAT(db_->GetAllSitesForTesting(BtmDatabaseTable::kBounces),
testing::ElementsAre("case6.test"));
}
TEST_P(BtmDatabaseInteractionTest, ReadWithExpiredRows) {
LoadDatabase();
EXPECT_TRUE(db_->Read("case1.test").has_value());
EXPECT_TRUE(db_->Read("case2.test").has_value());
EXPECT_TRUE(db_->Read("case3.test").has_value());
EXPECT_TRUE(db_->Read("case4.test").has_value());
EXPECT_TRUE(db_->Read("case5.test").has_value());
EXPECT_TRUE(db_->Read("case6.test").has_value());
AdvanceTimeTo(dummy_time + features::kBtmInteractionTtl.Get());
EXPECT_TRUE(db_->Read("case1.test").has_value());
EXPECT_TRUE(db_->Read("case2.test").has_value());
EXPECT_TRUE(db_->Read("case3.test").has_value());
EXPECT_TRUE(db_->Read("case4.test").has_value());
EXPECT_TRUE(db_->Read("case5.test").has_value());
EXPECT_TRUE(db_->Read("case6.test").has_value());
AdvanceTimeTo(dummy_time + features::kBtmInteractionTtl.Get() + tiny_delta);
EXPECT_EQ(db_->Read("case1.test"), std::nullopt);
EXPECT_TRUE(db_->Read("case2.test").has_value());
EXPECT_EQ(db_->Read("case3.test"), std::nullopt);
EXPECT_EQ(db_->Read("case4.test"), std::nullopt);
EXPECT_EQ(db_->Read("case5.test"), std::nullopt);
EXPECT_TRUE(db_->Read("case6.test").has_value());
// Time travel to a point by which all interactions and WAAs should've
// expired.
AdvanceTimeTo(dummy_time + features::kBtmInteractionTtl.Get() +
tiny_delta * 2);
EXPECT_EQ(db_->Read("case1.test"), std::nullopt);
EXPECT_EQ(db_->Read("case2.test"), std::nullopt);
EXPECT_EQ(db_->Read("case3.test"), std::nullopt);
EXPECT_EQ(db_->Read("case4.test"), std::nullopt);
EXPECT_EQ(db_->Read("case5.test"), std::nullopt);
EXPECT_TRUE(db_->Read("case6.test").has_value());
}
TEST_P(BtmDatabaseInteractionTest, ClearExpiredRowsFromPopupsTable) {
// Add popups to db.
const std::string opener_site_1 =
GetSiteForBtm(GURL("http://www.youtube.com/"));
const std::string opener_site_2 =
GetSiteForBtm(GURL("http://www.picasa.com/"));
const std::string popup_site =
GetSiteForBtm(GURL("http://www.doubleclick.net/"));
const base::Time first_popup_time = Time::FromSecondsSinceUnixEpoch(1);
const base::Time second_popup_time = Time::FromSecondsSinceUnixEpoch(2);
EXPECT_TRUE(db_->WritePopup(opener_site_1, popup_site,
/*access_id=*/123, first_popup_time,
/*is_current_interaction=*/true,
/*is_authentication_interaction=*/false));
EXPECT_TRUE(db_->WritePopup(opener_site_2, popup_site,
/*access_id=*/456, second_popup_time,
/*is_current_interaction=*/true,
/*is_authentication_interaction=*/false));
// Advance to just before the first popup expires.
AdvanceTimeTo(first_popup_time + BtmDatabase::kPopupTtl - tiny_delta);
// Verify that both sites are present.
EXPECT_EQ(db_->ClearExpiredRows(), 0u);
EXPECT_THAT(db_->GetAllSitesForTesting(BtmDatabaseTable::kPopups),
testing::UnorderedElementsAre("youtube.com", "doubleclick.net",
"picasa.com", "doubleclick.net"));
// Advance to after the first popup expires.
AdvanceTimeTo(first_popup_time + BtmDatabase::kPopupTtl + tiny_delta);
// Verify that only the first popup was removed from the db.
EXPECT_EQ(db_->ClearExpiredRows(), 1u);
EXPECT_THAT(db_->GetAllSitesForTesting(BtmDatabaseTable::kPopups),
testing::UnorderedElementsAre("picasa.com", "doubleclick.net"));
// Advance to after the second popup expires.
AdvanceTimeTo(second_popup_time + BtmDatabase::kPopupTtl + tiny_delta);
// Verify that both popups were removed from the db.
EXPECT_EQ(db_->ClearExpiredRows(), 1u);
EXPECT_THAT(db_->GetAllSitesForTesting(BtmDatabaseTable::kPopups),
testing::IsEmpty());
}
TEST_P(BtmDatabaseInteractionTest, FilterSites) {
LoadDatabase();
const std::set<std::string> sites_to_filter = {
"doesnotexist.test", "case1.test", "case2.test", "case3.test",
"case4.test", "case5.test", "case6.test"};
EXPECT_THAT(db_->FilterSites(/*sites=*/{},
BtmDatabase::BounceFilterType::kProtectiveEvent),
testing::IsEmpty());
EXPECT_THAT(
db_->FilterSites(sites_to_filter,
BtmDatabase::BounceFilterType::kProtectiveEvent),
testing::UnorderedElementsAre("case1.test", "case2.test", "case3.test",
"case4.test", "case5.test"));
EXPECT_THAT(db_->FilterSites(/*sites=*/{},
BtmDatabase::BounceFilterType::kUserActivation),
testing::IsEmpty());
EXPECT_THAT(db_->FilterSites(sites_to_filter,
BtmDatabase::BounceFilterType::kUserActivation),
testing::UnorderedElementsAre("case1.test", "case2.test",
"case3.test", "case4.test"));
EXPECT_THAT(
db_->FilterSites(/*sites=*/{},
BtmDatabase::BounceFilterType::kWebAuthnAssertion),
testing::IsEmpty());
EXPECT_THAT(
db_->FilterSites(sites_to_filter,
BtmDatabase::BounceFilterType::kWebAuthnAssertion),
testing::UnorderedElementsAre("case1.test", "case2.test", "case3.test",
"case5.test"));
}
INSTANTIATE_TEST_SUITE_P(All, BtmDatabaseInteractionTest, ::testing::Bool());
// A test class that verifies the behavior of the methods used to query the
// BtmDatabase to find all sites which should have their state cleared by BTM.
class BtmDatabaseQueryTest : public BtmDatabaseTest,
public testing::WithParamInterface<bool> {
public:
using QueryMethod = base::RepeatingCallback<std::vector<std::string>(void)>;
BtmDatabaseQueryTest() : BtmDatabaseTest(/*in_memory=*/GetParam()) {
// Test with the prod feature's parameter to ensure the tested scenarios are
// also valid/respected within prod env.
features_.InitWithFeatures({features::kBtmTtl, features::kBtm}, {});
}
void SetUp() override {
BtmDatabaseTest::SetUp();
grace_period = features::kBtmGracePeriod.Get();
interaction_ttl = features::kBtmInteractionTtl.Get();
}
QueryMethod GetSitesToClearQuery() {
return base::BindLambdaForTesting(
[&]() { return db_->GetSitesThatBounced(grace_period); });
}
void WriteForCurrentAction(const std::string& site,
TimestampRange event_times,
TimestampRange interaction_times,
TimestampRange waa_times) {
db_->Write(site, interaction_times,
/*bounce_times=*/event_times, waa_times);
}
protected:
base::TimeDelta grace_period;
base::TimeDelta interaction_ttl;
};
TEST_P(BtmDatabaseQueryTest, ProtectedDuringGracePeriod) {
// The result of running `query` shouldn't include sites which are currently
// in their grace period after first performing a BTM-triggering event.
QueryMethod query = GetSitesToClearQuery();
base::Time event = Time::FromSecondsSinceUnixEpoch(1);
TimestampRange event_times = {{event, event}};
WriteForCurrentAction("site.test", event_times, {}, {});
// Time-travel to the start of the grace period of the triggering event.
AdvanceTimeTo(event);
EXPECT_THAT(query.Run(), testing::IsEmpty());
// Time-travel to within the grace period of the triggering event.
AdvanceTimeTo(event + (grace_period / 2));
EXPECT_THAT(query.Run(), testing::IsEmpty());
// Time-travel to the end of the grace period of the triggering event.
AdvanceTimeTo(event + grace_period);
EXPECT_THAT(query.Run(), testing::IsEmpty());
// Site is no longer protected right after the grace period ends.
AdvanceTimeTo(event + grace_period + tiny_delta);
EXPECT_THAT(query.Run(), testing::ElementsAre("site.test"));
}
TEST_P(BtmDatabaseQueryTest, ProtectedByInteractionBeforeGracePeriod) {
// The result of running `query` shouldn't include sites who've received
// interactions from the user before performing a BTM-triggering event.
QueryMethod query = GetSitesToClearQuery();
base::Time interaction = Time::FromSecondsSinceUnixEpoch(1);
TimestampRange interaction_times = {{interaction, interaction}};
base::Time event = Time::FromSecondsSinceUnixEpoch(2);
TimestampRange event_times = {{event, event}};
WriteForCurrentAction("site.test", event_times, interaction_times, {});
// 'site.test' shouldn't be returned when querying after the grace period
// as the early interaction protects it from being cleared.
AdvanceTimeTo(event + grace_period + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
// "site.test" should still be protected up until the interaction expires.
AdvanceTimeTo(interaction + interaction_ttl - tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
// Once `interaction` expires, "site.test" restarts the BTM-procedure and
// `interaction` no longer protects it from BTM clearing.
AdvanceTimeTo(interaction + interaction_ttl + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
base::Time after_interaction_expiry = Now();
WriteForCurrentAction("site.test",
{{after_interaction_expiry, after_interaction_expiry}},
{}, {});
EXPECT_THAT(query.Run(), testing::IsEmpty());
// "site.test" is no longer protected by `interaction` and is cleared right
// after its grace period ends.
AdvanceTimeTo(after_interaction_expiry + grace_period + tiny_delta);
EXPECT_THAT(query.Run(), testing::ElementsAre("site.test"));
}
// The results of running `query` shouldn't include `site` with existing
// (expired or unexpired) WAAs (performed by the user before a BTM-triggering
// event occurred).
TEST_P(BtmDatabaseQueryTest, ProtectedByWaaBeforeGracePeriod) {
const QueryMethod query = GetSitesToClearQuery();
const std::string site = "site.test";
// Set up an event that happens after the WAA.
{
auto waa_time = Time::FromSecondsSinceUnixEpoch(100);
base::Time event_time = waa_time + tiny_delta;
WriteForCurrentAction(site, {{event_time, event_time}}, {},
{{waa_time, waa_time}});
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_TRUE(db_->Read(site).has_value());
// The `site` should remain protected by the existing WAA even after the
// `grace_period`:
EXPECT_GE(interaction_ttl, grace_period + tiny_delta);
AdvanceTimeTo(event_time + grace_period + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_TRUE(db_->Read(site).has_value());
// The `site` should still be protected before WAA expiry:
AdvanceTimeTo(waa_time + interaction_ttl);
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_TRUE(db_->Read(site).has_value());
// The `site`'s entry should be cleared by
// `BtmDatabase::ClearExpiredRows()` from the DB (hence implicitly
// protected) after WAA expiry:
AdvanceTimeTo(waa_time + interaction_ttl + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_EQ(db_->Read(site), std::nullopt);
}
// Set up an event that happens after WAA expired and the old entry cleared.
{
base::Time event_time = Now() + interaction_ttl + tiny_delta;
WriteForCurrentAction(site, {{event_time, event_time}}, {}, {});
EXPECT_THAT(query.Run(), testing::IsEmpty());
// The `site`'s new entry is no longer protected by WAAs after the
// `grace_period` and will be acted-upon by BTM:
AdvanceTimeTo(event_time + grace_period + tiny_delta);
EXPECT_THAT(query.Run(), testing::ElementsAre(site));
EXPECT_TRUE(db_->Read(site).has_value());
}
}
TEST_P(BtmDatabaseQueryTest, ProtectedByInteractionDuringGracePeriod) {
// The result of running `query` shouldn't include sites who've received
// interactions during the grace period following a BTM-triggering event.
QueryMethod query = GetSitesToClearQuery();
// Set up an interaction that happens during the event's grace period.
base::Time event = Time::FromSecondsSinceUnixEpoch(1);
TimestampRange event_times = {{event, event}};
base::Time interaction = Time::FromSecondsSinceUnixEpoch(4);
TimestampRange interaction_times = {{interaction, interaction}};
ASSERT_TRUE(interaction < event + grace_period);
WriteForCurrentAction("site.test", event_times, interaction_times, {});
// 'site.test' shouldn't be returned as the interaction protects
// it from being cleared.
AdvanceTimeTo(event + grace_period + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
// "site.test" should still be protected up until the interaction expires.
AdvanceTimeTo(interaction + interaction_ttl - tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
// Once `interaction` expires, "site.test" restarts the BTM-procedure and
// `interaction` no longer protects it from BTM clearing.
AdvanceTimeTo(interaction + interaction_ttl + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
base::Time after_interaction_expiry = Now();
WriteForCurrentAction("site.test",
{{after_interaction_expiry, after_interaction_expiry}},
{}, {});
EXPECT_THAT(query.Run(), testing::IsEmpty());
// "site.test" is no longer protected by `interaction` and is cleared right
// after its grace period ends.
AdvanceTimeTo(after_interaction_expiry + grace_period + tiny_delta);
EXPECT_THAT(query.Run(), testing::ElementsAre("site.test"));
}
// The results of running `query` shouldn't include `site` with existing
// (expired or unexpired) WAAs (performed by the user after a BTM-triggering
// event occurred).
TEST_P(BtmDatabaseQueryTest, ProtectedByWaaDuringGracePeriod) {
const QueryMethod query = GetSitesToClearQuery();
const std::string site = "site.test";
// Set up an event with a WAA happening before the end of the event's
// `grace_period`.
{
auto event_time = Time::FromSecondsSinceUnixEpoch(100);
base::Time waa_time = event_time + grace_period;
WriteForCurrentAction(site, {{event_time, event_time}}, {},
{{waa_time, waa_time}});
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_TRUE(db_->Read(site).has_value());
// The `site` should remain protected by the existing WAA even after the
// `grace_period`:
EXPECT_GE(interaction_ttl, grace_period + tiny_delta);
AdvanceTimeTo(event_time + grace_period + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_TRUE(db_->Read(site).has_value());
// The `site` should still be protected before WAA expiry:
AdvanceTimeTo(waa_time + interaction_ttl);
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_TRUE(db_->Read(site).has_value());
// The `site`'s entry should be cleared by
// `BtmDatabase::ClearExpiredRows()` from the DB (hence implicitly
// protected) after WAA expiry:
AdvanceTimeTo(waa_time + interaction_ttl + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_EQ(db_->Read(site), std::nullopt);
}
// Set up an event that happens after WAA expired and the old entry cleared.
{
base::Time event_time = Now() + interaction_ttl + tiny_delta;
WriteForCurrentAction(site, {{event_time, event_time}}, {}, {});
EXPECT_THAT(query.Run(), testing::IsEmpty());
// The `site`'s new entry is no longer protected by WAAs after the
// `grace_period` and will be acted-upon by BTM.
AdvanceTimeTo(event_time + grace_period + tiny_delta);
EXPECT_THAT(query.Run(), testing::ElementsAre(site));
EXPECT_TRUE(db_->Read(site).has_value());
}
}
TEST_P(BtmDatabaseQueryTest, SiteWithoutInteractionsAreUnprotected) {
// The result of running `query` should include sites who've never received
// interaction from the user before, or during the grace period after,
// performing a BTM-triggering event.
base::RepeatingCallback<std::vector<std::string>(void)> query =
GetSitesToClearQuery();
// Set up an event with no corresponding interaction.
base::Time event = Time::FromSecondsSinceUnixEpoch(2);
TimestampRange event_times = {{event, event}};
WriteForCurrentAction("site.test", event_times, {}, {});
// 'site.test' is returned since there is no interaction protecting it.
AdvanceTimeTo(event + grace_period + tiny_delta);
EXPECT_THAT(query.Run(), testing::ElementsAre("site.test"));
}
// This is an edge-case and the current accepted behavior is as expressed by
// this test coverage.
TEST_P(BtmDatabaseQueryTest, ProtectedByWaaAfterGracePeriod) {
const QueryMethod query = GetSitesToClearQuery();
const std::string site = "site.test";
// Sets up an event with a WAA happening after the end of the event's
// `grace_period` but before the subsequent BTM-trigger:
auto event_time = Time::FromSecondsSinceUnixEpoch(100);
auto waa_time = event_time + grace_period + tiny_delta;
WriteForCurrentAction(site, {{event_time, event_time}}, {},
{{waa_time, waa_time}});
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_TRUE(db_->Read(site).has_value());
// The `site` should still be protected before WAA expiry:
EXPECT_GT(interaction_ttl, grace_period);
AdvanceTimeTo(waa_time + interaction_ttl);
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_TRUE(db_->Read(site).has_value());
// The `site`'s entry should be cleared by
// `BtmDatabase::ClearExpiredRows()` from the DB (hence implicitly protected)
// after WAA expiry:
AdvanceTimeTo(waa_time + interaction_ttl + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_EQ(db_->Read(site), std::nullopt);
}
TEST_P(BtmDatabaseQueryTest, ProtectedByInteractionThenWaa) {
const QueryMethod query = GetSitesToClearQuery();
const std::string site = "site.test";
// Sets up an event with a interaction happening before the end of the event's
// `grace_period` and an WAA some moments later:
auto event_time = Time::FromSecondsSinceUnixEpoch(100);
auto interaction_time = event_time + grace_period;
auto waa_time = interaction_time + tiny_delta;
WriteForCurrentAction(site, {{event_time, event_time}},
{{interaction_time, interaction_time}},
{{waa_time, waa_time}});
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_TRUE(db_->Read(site).has_value());
// The `site` should still be protected after interaction expiry:
EXPECT_GT(interaction_ttl, grace_period);
AdvanceTimeTo(interaction_time + interaction_ttl + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_TRUE(db_->Read(site).has_value());
// The `site`'s entry should be cleared by
// `BtmDatabase::ClearExpiredRows()` from the DB (hence implicitly protected)
// after WAA expiry:
AdvanceTimeTo(waa_time + interaction_ttl + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_EQ(db_->Read(site), std::nullopt);
}
TEST_P(BtmDatabaseQueryTest, ProtectedByWaaThenInteraction) {
const QueryMethod query = GetSitesToClearQuery();
const std::string site = "site.test";
// Sets up an event with a WAA happening before the end of the event's
// `grace_period` and an interaction some moments later:
auto event_time = Time::FromSecondsSinceUnixEpoch(100);
auto waa_time = event_time + tiny_delta;
auto interaction_time = waa_time + grace_period;
WriteForCurrentAction(site, {{event_time, event_time}},
{{interaction_time, interaction_time}},
{{waa_time, waa_time}});
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_TRUE(db_->Read(site).has_value());
// The `site` should still be protected after WAA expiry:
EXPECT_GT(interaction_ttl, grace_period);
AdvanceTimeTo(waa_time + interaction_ttl + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_TRUE(db_->Read(site).has_value());
// The `site`'s entry should be cleared by
// `BtmDatabase::ClearExpiredRows()` from the DB (hence implicitly protected)
// after interaction expiry:
AdvanceTimeTo(interaction_time + interaction_ttl + tiny_delta);
EXPECT_THAT(query.Run(), testing::IsEmpty());
EXPECT_EQ(db_->Read(site), std::nullopt);
}
INSTANTIATE_TEST_SUITE_P(All, BtmDatabaseQueryTest, ::testing::Bool());
// A test class that verifies BtmDatabase garbage collection behavior for both
// tables.
class BtmDatabaseGarbageCollectionTest
: public BtmDatabaseTest,
public testing::WithParamInterface<BtmDatabaseTable> {
public:
BtmDatabaseGarbageCollectionTest() : BtmDatabaseTest(true) {
table_ = GetParam();
}
explicit BtmDatabaseGarbageCollectionTest(BtmDatabaseTable table)
: BtmDatabaseTest(true) {
table_ = table;
}
void SetUp() override {
BtmDatabaseTest::SetUp();
features_.InitAndEnableFeatureWithParameters(
features::kBtmTtl,
{{"interaction_ttl",
base::StringPrintf("%dh", base::Days(90).InHours())}});
DCHECK(db_);
db_->SetMaxEntriesForTesting(200);
db_->SetPurgeEntriesForTesting(20);
recent_interaction = Now();
old_interaction = Now() - base::Days(180);
}
void AddEntry(const std::string& site,
TimestampRange interaction_times,
TimestampRange waa_times) {
if (table_ == BtmDatabaseTable::kBounces) {
ASSERT_TRUE(db_->Write(site, interaction_times, {}, waa_times));
} else {
ASSERT_TRUE(db_->WritePopup(site, "doubleclick.net", /*access_id=*/123,
interaction_times->second,
/*is_current_interaction=*/true,
/*is_authentication_interaction=*/false));
}
}
void BloatBouncesForGC(int num_recent_entries, int num_old_entries) {
DCHECK(db_);
for (int i = 0; i < num_recent_entries; i++) {
AddEntry(
base::StrCat({"recent_interaction.test", base::NumberToString(i)}),
ToRange(recent_interaction), {});
}
for (int i = 0; i < num_old_entries; i++) {
AddEntry(base::StrCat({"old_interaction.test", base::NumberToString(i)}),
ToRange(old_interaction), {});
}
}
void LoadDatabase() {
clock_.SetNow(Time::FromSecondsSinceUnixEpoch(100));
std::vector<base::Time> times{Now(), Now() + tiny_delta,
Now() + tiny_delta * 2};
for (int i = 1; i <= 3; i++) {
if (table_ == BtmDatabaseTable::kBounces) {
ASSERT_TRUE(db_->Write(base::StringPrintf("entry%d.test", 7 - i),
ToRange(times[(i + 1) % 3]), {},
ToRange(times[(i) % 3])));
} else {
ASSERT_TRUE(db_->WritePopup(base::StringPrintf("entry%d.test", 7 - i),
"doubleclick.net", /*access_id=*/123,
times[(i + 1) % 3],
/*is_current_interaction=*/true,
/*is_authentication_interaction=*/false));
}
for (auto& time : times) {
time += tiny_delta * 3;
}
}
for (int i = 3; i <= 6; i++) {
if (table_ == BtmDatabaseTable::kBounces) {
ASSERT_TRUE(db_->Write(base::StringPrintf("entry%d.test", 7 - i),
ToRange(times[(i + 1) % 3]), {},
ToRange(times[i % 3])));
} else {
ASSERT_TRUE(db_->WritePopup(base::StringPrintf("entry%d.test", 7 - i),
"doubleclick.net", /*access_id=*/123,
times[(i + 1) % 3],
/*is_current_interaction=*/true,
/*is_authentication_interaction=*/false));
}
for (auto& time : times) {
time += tiny_delta * 3;
}
}
if (table_ == BtmDatabaseTable::kBounces) {
EXPECT_THAT(
db_->GetAllSitesForTesting(BtmDatabaseTable::kBounces),
testing::ElementsAre("entry1.test", "entry2.test", "entry3.test",
"entry4.test", "entry5.test", "entry6.test"));
} else {
EXPECT_THAT(
db_->GetAllSitesForTesting(BtmDatabaseTable::kPopups),
testing::IsSupersetOf({"entry1.test", "entry2.test", "entry3.test",
"entry4.test", "entry5.test", "entry6.test"}));
}
}
protected:
BtmDatabaseTable table_;
base::Time recent_interaction;
base::Time old_interaction;
base::Time storage = Time::FromSecondsSinceUnixEpoch(2);
};
// More than |max_entries_| entries with recent user interaction; garbage
// collection should purge down to |max_entries_| - |purge_entries_| entries.
TEST_P(BtmDatabaseGarbageCollectionTest, RemovesRecentOverMax) {
BloatBouncesForGC(/*num_recent_entries=*/db_->GetMaxEntries() * 2,
/*num_old_entries=*/0);
EXPECT_EQ(db_->GarbageCollect(),
db_->GetMaxEntries() + db_->GetPurgeEntries());
EXPECT_EQ(db_->GetEntryCount(GetParam()),
db_->GetMaxEntries() - db_->GetPurgeEntries());
}
TEST_P(BtmDatabaseGarbageCollectionTest, RemovesExpired_RemovesRecent_GT_Max) {
BloatBouncesForGC(/*num_recent_entries=*/db_->GetMaxEntries() * 2,
/*num_old_entries=*/db_->GetMaxEntries());
EXPECT_EQ(db_->GarbageCollect(),
db_->GetMaxEntries() * 2 + db_->GetPurgeEntries());
EXPECT_EQ(db_->GetEntryCount(GetParam()),
db_->GetMaxEntries() - db_->GetPurgeEntries());
}
// Less than |max_entries_| entries, none of which are expired;
// no entries should be garbage collected.
TEST_P(BtmDatabaseGarbageCollectionTest, PreservesUnderMax) {
BloatBouncesForGC(
/*num_recent_entries=*/(db_->GetMaxEntries() - db_->GetPurgeEntries()) /
2,
/*num_old_entries=*/0);
EXPECT_EQ(db_->GarbageCollect(), static_cast<size_t>(0));
EXPECT_EQ(db_->GetEntryCount(GetParam()),
(db_->GetMaxEntries() - db_->GetPurgeEntries()) / 2);
}
// Exactly |max_entries_| entries, none of which are expired;
// no entries should be garbage collected.
TEST_P(BtmDatabaseGarbageCollectionTest, PreservesMax) {
BloatBouncesForGC(/*num_recent_entries=*/db_->GetMaxEntries(),
/*num_old_entries=*/0);
EXPECT_EQ(db_->GarbageCollect(), static_cast<size_t>(0));
EXPECT_EQ(db_->GetEntryCount(GetParam()), db_->GetMaxEntries());
}
TEST_P(BtmDatabaseGarbageCollectionTest, RemovesExpired_PreserveRecent_LE_Max) {
BloatBouncesForGC(/*num_recent_entries=*/db_->GetMaxEntries(),
/*num_old_entries=*/db_->GetMaxEntries());
EXPECT_EQ(db_->GarbageCollect(), db_->GetMaxEntries());
EXPECT_EQ(db_->GetEntryCount(GetParam()), db_->GetMaxEntries());
}
TEST_P(BtmDatabaseGarbageCollectionTest, GarbageCollectOldest) {
LoadDatabase();
EXPECT_THAT(
db_->GetGarbageCollectOldestSitesForTesting(GetParam()),
testing::ElementsAre("entry6.test", "entry5.test", "entry4.test",
"entry3.test", "entry2.test", "entry1.test"));
EXPECT_EQ(db_->GarbageCollectOldest(GetParam(), 3), static_cast<size_t>(3));
if (GetParam() == BtmDatabaseTable::kBounces) {
EXPECT_THAT(
db_->GetAllSitesForTesting(BtmDatabaseTable::kBounces),
testing::ElementsAre("entry1.test", "entry2.test", "entry3.test"));
} else {
EXPECT_THAT(db_->GetAllSitesForTesting(BtmDatabaseTable::kPopups),
testing::ElementsAre("entry1.test", "doubleclick.net",
"entry2.test", "doubleclick.net",
"entry3.test", "doubleclick.net"));
}
}
INSTANTIATE_TEST_SUITE_P(All,
BtmDatabaseGarbageCollectionTest,
::testing::Values(BtmDatabaseTable::kBounces,
BtmDatabaseTable::kPopups));
// These tests only apply to the `bounces` table.
class BtmDatabaseBounceTableGarbageCollectionTest
: public BtmDatabaseGarbageCollectionTest {
public:
BtmDatabaseBounceTableGarbageCollectionTest()
: BtmDatabaseGarbageCollectionTest(BtmDatabaseTable::kBounces) {}
};
TEST_F(BtmDatabaseBounceTableGarbageCollectionTest,
GarbageCollectOldest_NullStorageTimes) {
LoadDatabase();
for (int i = 1; i <= 6; i++) {
auto state = db_->Read(base::StringPrintf("entry%d.test", i));
AddEntry(base::StringPrintf("entry%d.test", i),
state->user_activation_times, state->web_authn_assertion_times);
}
EXPECT_THAT(
db_->GetGarbageCollectOldestSitesForTesting(BtmDatabaseTable::kBounces),
testing::ElementsAre("entry6.test", "entry5.test", "entry4.test",
"entry3.test", "entry2.test", "entry1.test"));
}
TEST_F(BtmDatabaseBounceTableGarbageCollectionTest,
GarbageCollectOldest_NullInteractionTimes) {
LoadDatabase();
for (int i = 1; i <= 6; i++) {
auto state = db_->Read(base::StringPrintf("entry%d.test", i));
AddEntry(base::StringPrintf("entry%d.test", i), {},
state->web_authn_assertion_times);
}
EXPECT_THAT(
db_->GetGarbageCollectOldestSitesForTesting(BtmDatabaseTable::kBounces),
testing::ElementsAre("entry6.test", "entry5.test", "entry4.test",
"entry3.test", "entry2.test", "entry1.test"));
}
TEST_F(BtmDatabaseBounceTableGarbageCollectionTest,
GarbageCollectOldest_NullWaaTimes) {
LoadDatabase();
for (int i = 1; i <= 6; i++) {
auto state = db_->Read(base::StringPrintf("entry%d.test", i));
AddEntry(base::StringPrintf("entry%d.test", i),
state->user_activation_times, {});
}
EXPECT_THAT(
db_->GetGarbageCollectOldestSitesForTesting(BtmDatabaseTable::kBounces),
testing::ElementsAre("entry6.test", "entry5.test", "entry4.test",
"entry3.test", "entry2.test", "entry1.test"));
}
// Making sure having only one of storage, user interaction or WAA times
// shouldn't alter the oldest site ordering of the garbage collection. In this
// explicit case we only have user interaction times; we should expect the same
// behavior for the other times.
TEST_F(BtmDatabaseBounceTableGarbageCollectionTest,
GarbageCollectOldest_SingleNonNull) {
LoadDatabase();
for (int i = 1; i <= 6; i++) {
auto state = db_->Read(base::StringPrintf("entry%d.test", i));
AddEntry(base::StringPrintf("entry%d.test", i),
state->user_activation_times, {});
}
EXPECT_THAT(
db_->GetGarbageCollectOldestSitesForTesting(BtmDatabaseTable::kBounces),
testing::ElementsAre("entry6.test", "entry5.test", "entry4.test",
"entry3.test", "entry2.test", "entry1.test"));
}
// A test class that verifies BtmDatabase database health metrics collection
// behavior. Created on-disk so opening a corrupt database file can be tested.
class BtmDatabaseHistogramTest : public BtmDatabaseTest {
public:
BtmDatabaseHistogramTest() : BtmDatabaseTest(false) {}
void SetUp() override {
BtmDatabaseTest::SetUp();
features_.InitAndEnableFeatureWithParameters(features::kBtmTtl,
{{"interaction_ttl", "inf"}});
}
const base::HistogramTester& histograms() const { return histogram_tester_; }
protected:
base::HistogramTester histogram_tester_;
};
TEST_F(BtmDatabaseHistogramTest, HealthMetrics) {
// The database was initialized successfully.
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseErrors", 0);
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseInit", 1);
histograms().ExpectUniqueSample("Privacy.DIPS.DatabaseInit", 1, 1);
// These should each have one sample after database initialization.
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseHealthMetricsTime", 1);
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseSize", 1);
// The database should be empty.
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseEntryCount", 1);
histograms().ExpectUniqueSample("Privacy.DIPS.DatabaseEntryCount", 0, 1);
// Write an entry to the db.
db_->Write("url1.test",
{{Time::FromSecondsSinceUnixEpoch(1),
Time::FromSecondsSinceUnixEpoch(1)}},
{}, {});
db_->LogDatabaseMetricsForTesting();
// These should be unchanged.
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseErrors", 0);
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseInit", 1);
histograms().ExpectUniqueSample("Privacy.DIPS.DatabaseInit", 1, 1);
// These should each have two samples now.
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseHealthMetricsTime", 2);
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseSize", 2);
// The database should now have one entry.
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseEntryCount", 2);
histograms().ExpectBucketCount("Privacy.DIPS.DatabaseEntryCount", 1, 1);
}
TEST_F(BtmDatabaseHistogramTest, ErrorMetrics) {
// The database was initialized successfully.
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseErrors", 0);
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseInit", 1);
histograms().ExpectUniqueSample("Privacy.DIPS.DatabaseInit", 1, 1);
// Write an entry to the db.
db_->Write("url1.test",
{{Time::FromSecondsSinceUnixEpoch(1),
Time::FromSecondsSinceUnixEpoch(1)}},
{}, {});
EXPECT_EQ(db_->GetEntryCount(BtmDatabaseTable::kBounces),
static_cast<size_t>(1));
// Corrupt the database.
db_.reset();
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(db_ = std::make_unique<TestDatabase>(db_path_));
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseInit", 2);
histograms().ExpectUniqueSample("Privacy.DIPS.DatabaseInit", 1, 2);
// No data should be present as the database should have been razed.
EXPECT_EQ(db_->GetEntryCount(BtmDatabaseTable::kBounces),
static_cast<size_t>(0));
// Verify that the corruption error was reported.
EXPECT_TRUE(expecter.SawExpectedErrors());
histograms().ExpectTotalCount("Privacy.DIPS.DatabaseErrors", 1);
histograms().ExpectUniqueSample("Privacy.DIPS.DatabaseErrors",
sql::SqliteLoggedResultCode::kCorrupt, 1);
}
TEST_F(BtmDatabaseHistogramTest, PerformanceMetrics) {
histograms().ExpectTotalCount("Privacy.DIPS.Database.Operation.WriteTime", 0);
histograms().ExpectTotalCount("Privacy.DIPS.Database.Operation.ReadTime", 0);
// Write an entry to the db.
db_->Write("url.test",
{{Time::FromSecondsSinceUnixEpoch(1),
Time::FromSecondsSinceUnixEpoch(1)}},
{}, {});
histograms().ExpectTotalCount("Privacy.DIPS.Database.Operation.ReadTime", 0);
histograms().ExpectTotalCount("Privacy.DIPS.Database.Operation.WriteTime", 1);
// Read back the entry from the db.
db_->Read("site.test");
histograms().ExpectTotalCount("Privacy.DIPS.Database.Operation.ReadTime", 1);
histograms().ExpectTotalCount("Privacy.DIPS.Database.Operation.WriteTime", 1);
}
class BtmDatabaseInitializationTest : public testing::Test {
public:
BtmDatabaseInitializationTest() {
features_.InitAndEnableFeatureWithParameters(features::kBtmTtl,
{{"interaction_ttl", "inf"}});
}
protected:
void InitializeDatabase() { TestDatabase db(db_path_); }
void ValidateSchemaAndMetadataMatchLatestVersion(sql::Database* db) {
ValidateMetadataMatchesLatestVersion(db);
ValidateBouncesTableMatchesLatestSchemaVersion(db);
ValidatePopupsTableMatchesLatestSchemaVersion(db);
ValidateConfigTableMatchesLatestSchemaVersion(db);
}
int GetDatabaseVersion(sql::Database* db) {
sql::Statement kGetVersionSql(
db->GetUniqueStatement("SELECT value FROM meta WHERE key='version'"));
if (!kGetVersionSql.Step()) {
return 0;
}
return kGetVersionSql.ColumnInt(0);
}
int GetDatabaseLastCompatibleVersion(sql::Database* db) {
sql::Statement kGetLastCompatibleVersionSql(db->GetUniqueStatement(
"SELECT value FROM meta WHERE key='last_compatible_version'"));
if (!kGetLastCompatibleVersionSql.Step()) {
return 0;
}
return kGetLastCompatibleVersionSql.ColumnInt(0);
}
std::optional<int> GetPrepopulatedFromMetaTable(sql::Database* db) {
sql::Statement kGetPrepopulatedSql(db->GetUniqueStatement(
"SELECT value FROM meta WHERE key='prepopulated'"));
if (!kGetPrepopulatedSql.Step()) {
return std::nullopt;
}
return kGetPrepopulatedSql.ColumnInt(0);
}
std::optional<int64_t> GetPrepopulatedFromConfigTable(sql::Database* db) {
sql::Statement kGetPrepopulatedSql(db->GetUniqueStatement(
"SELECT int_value FROM config WHERE key='prepopulated'"));
if (!kGetPrepopulatedSql.Step()) {
return std::nullopt;
}
return kGetPrepopulatedSql.ColumnInt64(0);
}
base::FilePath db_path() { return db_path_; }
void LoadDatabase(const char* file_name) {
base::FilePath root;
ASSERT_TRUE(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &root));
base::FilePath file_path = root.AppendASCII("content")
.AppendASCII("test")
.AppendASCII("data")
.AppendASCII("btm")
.AppendASCII(file_name);
EXPECT_TRUE(base::PathExists(file_path));
ASSERT_TRUE(sql::test::CreateDatabaseFromSQL(db_path(), file_path));
}
std::string RowCount(sql::Database* db, const char* table) {
return sql::test::ExecuteWithResult(
db, base::StringPrintf("SELECT COUNT(*) FROM %s", table));
}
private:
base::test::ScopedFeatureList features_;
std::unique_ptr<TestDatabase> db_;
base::ScopedTempDir temp_dir_;
base::FilePath db_path_;
// Test setup.
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
db_path_ = temp_dir_.GetPath().AppendASCII("BTM.db");
}
void TearDown() override {
db_.reset();
ASSERT_TRUE(temp_dir_.Delete());
}
void ValidateMetadataMatchesLatestVersion(sql::Database* db) {
EXPECT_EQ(GetDatabaseVersion(db), BtmDatabase::kLatestSchemaVersion);
EXPECT_EQ(GetDatabaseLastCompatibleVersion(db),
BtmDatabase::kMinCompatibleSchemaVersion);
// We no longer mark prepopulation in the meta table.
EXPECT_EQ(GetPrepopulatedFromMetaTable(db), std::nullopt);
}
void ValidateBouncesTableMatchesLatestSchemaVersion(sql::Database* db) {
EXPECT_TRUE(db->DoesTableExist("bounces"));
EXPECT_TRUE(db->DoesColumnExist("bounces", "site"));
EXPECT_TRUE(db->DoesColumnExist("bounces", "first_bounce_time"));
EXPECT_TRUE(db->DoesColumnExist("bounces", "last_bounce_time"));
EXPECT_TRUE(db->DoesColumnExist("bounces", "first_user_activation_time"));
EXPECT_TRUE(db->DoesColumnExist("bounces", "last_user_activation_time"));
EXPECT_TRUE(
db->DoesColumnExist("bounces", "first_web_authn_assertion_time"));
EXPECT_TRUE(
db->DoesColumnExist("bounces", "last_web_authn_assertion_time"));
// Expect obsolete and temporary columns to have been removed.
EXPECT_FALSE(db->DoesColumnExist("bounces", "first_stateless_bounce_time"));
EXPECT_FALSE(db->DoesColumnExist("bounces", "last_stateless_bounce_time"));
EXPECT_FALSE(db->DoesColumnExist("bounces", "first_stateful_bounce_time"));
EXPECT_FALSE(db->DoesColumnExist("bounces", "last_stateful_bounce_time"));
EXPECT_FALSE(db->DoesColumnExist("bounces", "first_site_storage_time"));
EXPECT_FALSE(db->DoesColumnExist("bounces", "last_site_storage_time"));
}
void ValidatePopupsTableMatchesLatestSchemaVersion(sql::Database* db) {
EXPECT_TRUE(db->DoesTableExist("popups"));
EXPECT_TRUE(db->DoesColumnExist("popups", "opener_site"));
EXPECT_TRUE(db->DoesColumnExist("popups", "popup_site"));
EXPECT_TRUE(db->DoesColumnExist("popups", "access_id"));
EXPECT_TRUE(db->DoesColumnExist("popups", "last_popup_time"));
EXPECT_TRUE(db->DoesColumnExist("popups", "is_current_interaction"));
}
void ValidateConfigTableMatchesLatestSchemaVersion(sql::Database* db) {
EXPECT_TRUE(db->DoesTableExist("config"));
EXPECT_TRUE(db->DoesColumnExist("config", "key"));
EXPECT_TRUE(db->DoesColumnExist("config", "int_value"));
}
};
TEST_F(BtmDatabaseInitializationTest, InitializeEmptyDBWithLatestSchema) {
// Initialize with an empty DB.
InitializeDatabase();
// Validate aspects of current schema.
{
sql::Database db(sql::test::kTestTag);
ASSERT_TRUE(db.Open(db_path()));
ValidateSchemaAndMetadataMatchLatestVersion(&db);
}
}
TEST_F(BtmDatabaseInitializationTest, RazeIfIncompatible_TooNew) {
ASSERT_NO_FATAL_FAILURE(LoadDatabase("v2.sql"));
// Manipulations on the database version number are not necessary, but
// performed for consistency.
//
// Verify pre migration conditions.
{
sql::Database db(sql::test::kTestTag);
ASSERT_TRUE(db.Open(db_path()));
// Matches what is in "v2.sql" file:
const auto v2sql_version_num = 2;
const auto v2sql_compatible_version_num = 2;
const auto v2sql_prepopulated = 1;
EXPECT_EQ(GetDatabaseVersion(&db), v2sql_version_num);
EXPECT_EQ(GetDatabaseLastCompatibleVersion(&db),
v2sql_compatible_version_num);
EXPECT_EQ(GetPrepopulatedFromMetaTable(&db), v2sql_prepopulated);
sql::MetaTable meta_table;
ASSERT_TRUE(
meta_table.Init(&db, v2sql_version_num, v2sql_compatible_version_num));
// Prepare simulation of raze if incompatible. by making this DB
// incompatible.
const int tiny_increment = 1;
ASSERT_TRUE(meta_table.SetVersionNumber(BtmDatabase::kLatestSchemaVersion +
tiny_increment));
ASSERT_TRUE(meta_table.SetCompatibleVersionNumber(
BtmDatabase::kLatestSchemaVersion + tiny_increment));
EXPECT_EQ(GetDatabaseVersion(&db),
BtmDatabase::kLatestSchemaVersion + tiny_increment);
EXPECT_EQ(GetDatabaseLastCompatibleVersion(&db),
BtmDatabase::kLatestSchemaVersion + tiny_increment);
ASSERT_EQ(RowCount(&db, "bounces"), "4");
}
InitializeDatabase();
// Verify post migration conditions.
{
sql::Database db(sql::test::kTestTag);
ASSERT_TRUE(db.Open(db_path()));
// We should be on the latest schema version after razing.
ValidateSchemaAndMetadataMatchLatestVersion(&db);
// The DB was razed, so it shouldn't be marked as prepopulated, even though
// it was marked as prepopulated before the raze.
EXPECT_EQ(GetPrepopulatedFromConfigTable(&db), std::nullopt);
// The raze should have deleted all existing data.
EXPECT_EQ(RowCount(&db, "bounces"), "0");
}
}
TEST_F(BtmDatabaseInitializationTest, MigrateOldSchemaToLatestVersion) {
ASSERT_NO_FATAL_FAILURE(LoadDatabase("v2.sql"));
{
sql::Database db(sql::test::kTestTag);
ASSERT_TRUE(db.Open(db_path()));
EXPECT_EQ(GetDatabaseVersion(&db), 2);
EXPECT_EQ(GetDatabaseLastCompatibleVersion(&db), 2);
}
InitializeDatabase();
{
sql::Database db(sql::test::kTestTag);
ASSERT_TRUE(db.Open(db_path()));
ValidateSchemaAndMetadataMatchLatestVersion(&db);
}
}
// Verifies actions on the `config` table of the BTM database.
class BtmDatabaseConfigTest : public BtmDatabaseTest {
public:
BtmDatabaseConfigTest() : BtmDatabaseTest(/*in_memory=*/true) {}
};
TEST_F(BtmDatabaseConfigTest, GetUnknownKeyReturnsNullopt) {
EXPECT_EQ(db_->GetConfigValueForTesting("test"), std::nullopt);
}
TEST_F(BtmDatabaseConfigTest, WriteAndRead) {
ASSERT_TRUE(db_->SetConfigValueForTesting("test", 42));
EXPECT_THAT(db_->GetConfigValueForTesting("test"), Optional(42));
}
TEST_F(BtmDatabaseConfigTest, Overwrite) {
ASSERT_TRUE(db_->SetConfigValueForTesting("test", 42));
ASSERT_TRUE(db_->SetConfigValueForTesting("test", 99));
EXPECT_THAT(db_->GetConfigValueForTesting("test"), Optional(99));
}
TEST_F(BtmDatabaseConfigTest, MultipleKeys) {
ASSERT_TRUE(db_->SetConfigValueForTesting("foo", 42));
ASSERT_TRUE(db_->SetConfigValueForTesting("bar", 99));
EXPECT_THAT(db_->GetConfigValueForTesting("foo"), Optional(42));
EXPECT_THAT(db_->GetConfigValueForTesting("bar"), Optional(99));
}
TEST_F(BtmDatabaseConfigTest, TimerLastFired) {
const base::Time time = Time::FromSecondsSinceUnixEpoch(1);
ASSERT_EQ(db_->GetTimerLastFired(), std::nullopt);
ASSERT_TRUE(db_->SetTimerLastFired(time));
ASSERT_EQ(db_->GetTimerLastFired(), time);
}
} // namespace content