blob: 8b4afa88b4e134ab4b45415ac9b0c2789d59fa77 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/feed/core/v2/scheduling.h"
#include "base/check.h"
#include "base/json/json_writer.h"
#include "base/strings/string_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "components/feed/core/v2/config.h"
#include "components/feed/core/v2/feedstore_util.h"
#include "components/feed/core/v2/public/stream_type.h"
#include "components/feed/feed_feature_list.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace feed {
namespace {
const base::Time kAnchorTime = base::Time::UnixEpoch() + base::Hours(6);
const base::TimeDelta kDefaultScheduleInterval = base::Hours(24);
std::string ToJSON(base::ValueView value) {
std::string json;
CHECK(base::JSONWriter::WriteWithOptions(
value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json));
// Don't use \r\n on windows.
base::RemoveChars(json, "\r", &json);
return json;
}
TEST(RequestSchedule, CanSerialize) {
RequestSchedule schedule;
schedule.anchor_time = kAnchorTime;
schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};
schedule.type = RequestSchedule::Type::kScheduledRefresh;
const base::Value::Dict schedule_dict = RequestScheduleToDict(schedule);
ASSERT_EQ(R"({
"anchor": "11644495200000000",
"offsets": [ "3600000000", "21600000000" ],
"type": 0
}
)",
ToJSON(schedule_dict));
RequestSchedule deserialized_schedule =
RequestScheduleFromDict(schedule_dict);
EXPECT_EQ(schedule.anchor_time, deserialized_schedule.anchor_time);
EXPECT_EQ(schedule.refresh_offsets, deserialized_schedule.refresh_offsets);
EXPECT_EQ(schedule.type, deserialized_schedule.type);
}
TEST(RequestSchedule, GetScheduleType) {
RequestSchedule schedule;
schedule.anchor_time = kAnchorTime;
schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};
schedule.type = RequestSchedule::Type::kScheduledRefresh;
EXPECT_EQ(RequestSchedule::Type::kScheduledRefresh,
RequestScheduleFromDict(RequestScheduleToDict(schedule)).type);
schedule.type = RequestSchedule::Type::kFeedCloseRefresh;
base::Value::Dict schedule_dict = RequestScheduleToDict(schedule);
EXPECT_EQ(RequestSchedule::Type::kFeedCloseRefresh,
RequestScheduleFromDict(schedule_dict).type);
// Default to kScheduledRefresh if the type isn't valid.
schedule_dict.Set("type", -1);
EXPECT_EQ(RequestSchedule::Type::kScheduledRefresh,
RequestScheduleFromDict(schedule_dict).type);
}
class NextScheduledRequestTimeTest : public testing::Test {
public:
void SetUp() override {
Config config = GetFeedConfig();
config.default_background_refresh_interval = kDefaultScheduleInterval;
SetFeedConfigForTesting(config);
}
};
TEST_F(NextScheduledRequestTimeTest, NormalUsage) {
RequestSchedule schedule;
schedule.anchor_time = kAnchorTime;
schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};
// |kNow| is in the normal range [kAnchorTime, kAnchorTime+1hr)
base::Time kNow = kAnchorTime + base::Minutes(12);
EXPECT_EQ(kAnchorTime + base::Hours(1),
NextScheduledRequestTime(kNow, &schedule));
kNow += base::Hours(1);
EXPECT_EQ(kAnchorTime + base::Hours(6),
NextScheduledRequestTime(kNow, &schedule));
kNow += base::Hours(6);
EXPECT_EQ(kNow + kDefaultScheduleInterval,
NextScheduledRequestTime(kNow, &schedule));
}
TEST_F(NextScheduledRequestTimeTest, NowPastRequestTimeSkipsRequest) {
RequestSchedule schedule;
schedule.anchor_time = kAnchorTime;
schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};
base::Time kNow = kAnchorTime + base::Minutes(61);
EXPECT_EQ(kAnchorTime + base::Hours(6),
NextScheduledRequestTime(kNow, &schedule));
kNow += base::Hours(6);
EXPECT_EQ(kNow + kDefaultScheduleInterval,
NextScheduledRequestTime(kNow, &schedule));
}
TEST_F(NextScheduledRequestTimeTest, NowPastAllRequestTimes) {
RequestSchedule schedule;
schedule.anchor_time = kAnchorTime;
schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};
base::Time kNow = kAnchorTime + base::Hours(7);
EXPECT_EQ(kNow + kDefaultScheduleInterval,
NextScheduledRequestTime(kNow, &schedule));
}
TEST_F(NextScheduledRequestTimeTest, NowInPast) {
RequestSchedule schedule;
schedule.anchor_time = kAnchorTime;
schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};
// Since |kNow| is in the past, deltas are recomputed using |kNow|.
base::Time kNow = kAnchorTime - base::Minutes(12);
EXPECT_EQ(kNow + base::Hours(1), NextScheduledRequestTime(kNow, &schedule));
EXPECT_EQ(kNow, schedule.anchor_time);
}
TEST_F(NextScheduledRequestTimeTest, NowInFarFuture) {
RequestSchedule schedule;
schedule.anchor_time = kAnchorTime;
schedule.refresh_offsets = {base::Hours(1), base::Hours(6)};
// Since |kNow| is in the far future, deltas are recomputed using |kNow|.
base::Time kNow = kAnchorTime + base::Days(12);
EXPECT_EQ(kNow + base::Hours(1), NextScheduledRequestTime(kNow, &schedule));
EXPECT_EQ(kNow, schedule.anchor_time);
}
class ContentLifetimeTest : public testing::Test {
public:
const base::TimeDelta kDefaultContentExpiration = base::Hours(24);
const base::TimeDelta kDefaultStaleContentThreshold = base::Hours(4);
const base::TimeDelta kDefaultSubscriptionlessContentExpiration =
base::Days(14);
void SetUp() override {
Config config = GetFeedConfig();
config.content_expiration_threshold = kDefaultContentExpiration;
config.stale_content_threshold = kDefaultStaleContentThreshold;
SetFeedConfigForTesting(config);
metadata_ = feedstore::Metadata();
feedstore::Metadata::StreamMetadata* sm = metadata_.add_stream_metadata();
sm->set_stream_key(
std::string(feedstore::StreamKey(StreamType(StreamKind::kForYou))));
}
protected:
void set_content_lifetime(int64_t stale_age_ms, int64_t invalid_age_ms) {
feedstore::Metadata::StreamMetadata::ContentLifetime* content_lifetime =
MetadataForStream(metadata_, StreamType(StreamKind::kForYou))
.mutable_content_lifetime();
content_lifetime->set_stale_age_ms(stale_age_ms);
content_lifetime->set_invalid_age_ms(invalid_age_ms);
}
void set_stale_age(base::TimeDelta stale_age) {
set_content_lifetime(stale_age.InMilliseconds(), 0);
}
void set_invalid_age(base::TimeDelta invalid_age) {
set_content_lifetime(0, invalid_age.InMilliseconds());
}
base::TimeDelta WithEpsilon(base::TimeDelta duration) {
return duration + base::Milliseconds(1);
}
feedstore::Metadata metadata_;
};
TEST_F(ContentLifetimeTest, ShouldWaitForNewContent_DefaultThreshold) {
EXPECT_FALSE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kForYou), kDefaultStaleContentThreshold,
/*is_web_feed_subscriber=*/true));
EXPECT_TRUE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kForYou),
WithEpsilon(kDefaultStaleContentThreshold), true));
EXPECT_TRUE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kForYou), base::Hours(5),
/*is_web_feed_subscriber=*/true));
EXPECT_FALSE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kForYou), base::Hours(3),
/*is_web_feed_subscriber=*/true));
// If the web feed onboarding feature is turned off, then we should return
// true even if the user is not subscribed.
EXPECT_TRUE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kForYou), base::Days(8),
/*is_web_feed_subscriber=*/false));
EXPECT_TRUE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kForYou), base::Days(6),
/*is_web_feed_subscriber=*/false));
}
TEST_F(ContentLifetimeTest, ShouldWaitForNewContent_ServerThreshold_Valid) {
set_stale_age(base::Minutes(60));
EXPECT_TRUE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kForYou), base::Minutes(61),
/*is_web_feed_subscriber=*/true));
EXPECT_FALSE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kForYou), base::Minutes(59),
/*is_web_feed_subscriber=*/true));
}
TEST_F(ContentLifetimeTest, ShouldWaitForNewContent_WithNoSubscriptions) {
// Enable WebFeed and WebFeedOnboarding flags.
base::test::ScopedFeatureList features;
std::vector<base::test::FeatureRef> enabled_features = {kWebFeedOnboarding},
disabled_features = {};
features.InitWithFeatures(enabled_features, disabled_features);
EXPECT_FALSE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kFollowing), base::Days(6),
/*is_web_feed_subscriber=*/false));
EXPECT_TRUE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kFollowing), base::Days(8),
/*is_web_feed_subscriber=*/false));
}
TEST_F(ContentLifetimeTest, ShouldWaitForNewContent_ServerThreshold_Invalid) {
// We ignore stale ages greater than the default.
EXPECT_TRUE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kForYou),
WithEpsilon(kDefaultStaleContentThreshold), true));
set_stale_age(kDefaultStaleContentThreshold + base::Minutes(1));
EXPECT_TRUE(
ShouldWaitForNewContent(metadata_, StreamType(StreamKind::kForYou),
WithEpsilon(kDefaultStaleContentThreshold),
/*is_web_feed_subscriber=*/true));
// We ignore zero durations.
set_stale_age(base::Days(0));
EXPECT_FALSE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kForYou), kDefaultStaleContentThreshold,
/*is_web_feed_subscriber=*/true));
EXPECT_TRUE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kForYou),
WithEpsilon(kDefaultStaleContentThreshold), true));
// We ignore negative durations.
set_stale_age(base::Days(-1));
EXPECT_FALSE(ShouldWaitForNewContent(
metadata_, StreamType(StreamKind::kForYou), kDefaultStaleContentThreshold,
/*is_web_feed_subscriber=*/true));
EXPECT_TRUE(
ShouldWaitForNewContent(metadata_, StreamType(StreamKind::kForYou),
WithEpsilon(kDefaultStaleContentThreshold),
/*is_web_feed_subscriber=*/true));
}
TEST_F(ContentLifetimeTest, ContentInvalidFromAge_DefaultThreshold) {
EXPECT_FALSE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
kDefaultContentExpiration,
/*is_web_feed_subscriber=*/true));
EXPECT_TRUE(
ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
kDefaultContentExpiration + base::Milliseconds(1),
/*is_web_feed_subscriber=*/true));
EXPECT_TRUE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
base::Hours(25),
/*is_web_feed_subscriber=*/true));
EXPECT_FALSE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
base::Hours(23),
/*is_web_feed_subscriber=*/true));
}
TEST_F(ContentLifetimeTest, ContentInvalidFromAge_ServerThreshold_Valid) {
set_invalid_age(base::Minutes(60));
EXPECT_TRUE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
base::Minutes(61),
/*is_web_feed_subscriber=*/true));
EXPECT_FALSE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
base::Minutes(59),
/*is_web_feed_subscriber=*/true));
}
TEST_F(ContentLifetimeTest, ContentInvalidFromAge_ServerThreshold_Invalid) {
// We ignore stale ages greater than the default.
EXPECT_TRUE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
WithEpsilon(kDefaultContentExpiration),
/*is_web_feed_subscriber=*/true));
set_invalid_age(kDefaultContentExpiration + base::Minutes(1));
EXPECT_TRUE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
WithEpsilon(kDefaultContentExpiration),
/*is_web_feed_subscriber=*/true));
// We ignore zero durations.
set_invalid_age(base::Days(0));
EXPECT_FALSE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
kDefaultContentExpiration,
/*is_web_feed_subscriber=*/true));
EXPECT_TRUE(ContentInvalidFromAge(
metadata_, StreamType(StreamKind::kForYou),
WithEpsilon(kDefaultSubscriptionlessContentExpiration),
/*is_web_feed_subscriber=*/true));
// We ignore negative durations.
set_invalid_age(base::Days(-1));
EXPECT_FALSE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
kDefaultContentExpiration,
/*is_web_feed_subscriber=*/true));
EXPECT_TRUE(ContentInvalidFromAge(metadata_, StreamType(StreamKind::kForYou),
WithEpsilon(kDefaultContentExpiration),
/*is_web_feed_subscriber=*/true));
}
TEST_F(ContentLifetimeTest, ContentInvalidFromAge_SubscriptionlessThreshold) {
// Enable WebFeed and WebFeedOnboarding flags.
base::test::ScopedFeatureList features;
std::vector<base::test::FeatureRef> enabled_features = {kWebFeedOnboarding},
disabled_features = {};
features.InitWithFeatures(enabled_features, disabled_features);
EXPECT_FALSE(ContentInvalidFromAge(metadata_,
StreamType(StreamKind::kFollowing),
kDefaultSubscriptionlessContentExpiration,
/*is_web_feed_subscriber=*/false));
EXPECT_TRUE(ContentInvalidFromAge(
metadata_, StreamType(StreamKind::kFollowing),
kDefaultSubscriptionlessContentExpiration + base::Milliseconds(1),
/*is_web_feed_subscriber=*/false));
EXPECT_FALSE(ContentInvalidFromAge(
metadata_, StreamType(StreamKind::kFollowing), base::Days(13),
/*is_web_feed_subscriber=*/false));
EXPECT_TRUE(ContentInvalidFromAge(
metadata_, StreamType(StreamKind::kFollowing), base::Days(15),
/*is_web_feed_subscriber=*/false));
}
} // namespace
} // namespace feed