// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/offline_pages/android/auto_fetch_page_load_watcher.h"
#include <memory>
#include <string>
#include <utility>
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/offline_pages/android/offline_page_auto_fetcher_service.h"
#include "chrome/browser/offline_pages/android/offline_page_auto_fetcher_service_factory.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/offline_pages/core/client_namespace_constants.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/render_frame_host_test_support.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_web_contents_factory.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
namespace offline_pages {
namespace {
using ::offline_pages::auto_fetch_internal::AndroidTabFinder;
using ::offline_pages::auto_fetch_internal::InternalImpl;
using ::offline_pages::auto_fetch_internal::RequestInfo;
using ::offline_pages::auto_fetch_internal::TabInfo;
using ::testing::_;
constexpr int kDefaultTabId = 123;
GURL TestURL() {
return GURL("http://www.url.com");
}
GURL OtherURL() {
return GURL("http://other.com");
}
RequestInfo TestInfo(
int64_t id,
const GURL& url = TestURL(),
SavePageRequest::AutoFetchNotificationState notification_state =
SavePageRequest::AutoFetchNotificationState::kUnknown) {
RequestInfo info;
info.request_id = id;
info.url = url;
info.notification_state = notification_state;
info.metadata.android_tab_id = kDefaultTabId;
return info;
}
class MockAutoFetchNotifier : public AutoFetchNotifier {
public:
MOCK_METHOD1(NotifyInProgress, void(int in_flight_count));
MOCK_METHOD1(InProgressCountChanged, void(int in_flight_count));
};
class FakeInternalImplDelegate : public InternalImpl::Delegate {
public:
~FakeInternalImplDelegate() override {}
void SetNotificationStateToShown(int64_t request_id) override {
set_notification_state_requests.push_back(request_id);
}
void RemoveRequests(const std::vector<int64_t>& request_ids) override {
removed_requests.insert(removed_requests.end(), request_ids.begin(),
request_ids.end());
}
std::vector<int64_t> removed_requests;
std::vector<int64_t> set_notification_state_requests;
};
// Note that TabAndroid doesn't work in unit tests, so this stubs out access to
// tab information.
class StubTabFinder : public AutoFetchPageLoadWatcher::AndroidTabFinder {
public:
~StubTabFinder() override {}
// AutoFetchPageLoadWatcher::AndroidTabFinder.
std::map<int, TabInfo> FindAndroidTabs(
std::vector<int> android_tab_ids) override {
std::map<int, TabInfo> result;
for (const int tab_id : android_tab_ids) {
if (tabs_.count(tab_id)) {
result[tab_id] = TabInfo{tab_id, tabs_[tab_id]};
}
}
return result;
}
std::optional<TabInfo> FindNavigationTab(
content::WebContents* web_contents) override {
if (!tabs_.count(current_tab_id_))
return std::nullopt;
return TabInfo{current_tab_id_, tabs_[current_tab_id_]};
}
// Methods to alter stub behavior.
void SetTabs(std::map<int, GURL> tab_urls) { tabs_ = std::move(tab_urls); }
void SetCurrentTabId(int tab_id) { current_tab_id_ = tab_id; }
private:
std::map<int, GURL> tabs_;
// ID of the current tab to return from FindNavigationTab.
int current_tab_id_ = kDefaultTabId;
};
// Note: This unittest doesn't attempt to directly test
// |AutoFetchPageLoadWatcher| because the set-up is difficult, especially for
// testing various call orderings. Additionally, TabAndroid can't be tested
// in a unit test. Instead, see OfflinePageAutoFetchTest.java for coverage.
class AutoFetchInternalImplTest : public testing::Test {
public:
protected:
// A WebContents* is needed for some |InternalImpl| methods, but nullptr is
// sufficient because |StubTabFinder| doesn't inspect the value.
const raw_ptr<content::WebContents> web_contents_ = nullptr;
MockAutoFetchNotifier notifier_;
FakeInternalImplDelegate delegate_;
raw_ptr<StubTabFinder> tab_finder_ = new StubTabFinder;
InternalImpl impl_{¬ifier_, &delegate_,
base::WrapUnique(tab_finder_.get())};
};
TEST_F(AutoFetchInternalImplTest, NoInitialization) {
// Just verify there is no crash.
impl_.SuccessfulPageNavigation(TestURL());
impl_.NavigationFrom(TestURL(), web_contents_);
}
TEST_F(AutoFetchInternalImplTest, RemoveRequestOnSuccessfulNavigation) {
impl_.TabModelReady();
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, TestURL()}});
impl_.RequestListInitialized(
std::vector<RequestInfo>{TestInfo(1, TestURL())});
impl_.SuccessfulPageNavigation(TestURL());
EXPECT_EQ(std::vector<int64_t>({1}), delegate_.removed_requests);
}
TEST_F(AutoFetchInternalImplTest,
RemoveRequestOnSuccessfulNavigationBeforeInitialization) {
impl_.TabModelReady();
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, TestURL()}});
impl_.SuccessfulPageNavigation(TestURL());
impl_.RequestListInitialized(
std::vector<RequestInfo>{TestInfo(1, TestURL())});
EXPECT_EQ(std::vector<int64_t>({1}), delegate_.removed_requests);
}
TEST_F(AutoFetchInternalImplTest,
RemoveRequestOnSuccessfulNavigationBeforeTabModelReady) {
impl_.SuccessfulPageNavigation(TestURL());
impl_.RequestListInitialized(
std::vector<RequestInfo>{TestInfo(1, TestURL())});
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, TestURL()}});
impl_.TabModelReady();
EXPECT_EQ(std::vector<int64_t>({1}), delegate_.removed_requests);
}
// Successful navigation to a URL that we are not auto fetching should not
// remove any requests from the list of in progress auto fetches.
TEST_F(AutoFetchInternalImplTest, SuccessfulNavigationToOtherURL) {
impl_.TabModelReady();
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, TestURL()}});
impl_.RequestListInitialized(
std::vector<RequestInfo>{TestInfo(1, TestURL())});
impl_.SuccessfulPageNavigation(OtherURL());
EXPECT_EQ(std::vector<int64_t>(), delegate_.removed_requests);
}
TEST_F(AutoFetchInternalImplTest,
SuccessfulNavigationToOtherURLBeforeInitialization) {
impl_.TabModelReady();
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, TestURL()}});
impl_.SuccessfulPageNavigation(OtherURL());
impl_.RequestListInitialized(
std::vector<RequestInfo>{TestInfo(1, TestURL())});
EXPECT_EQ(std::vector<int64_t>(), delegate_.removed_requests);
}
TEST_F(AutoFetchInternalImplTest, NavigatingFromNotifies) {
impl_.TabModelReady();
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, TestURL()}});
impl_.RequestListInitialized(
std::vector<RequestInfo>{TestInfo(1, TestURL())});
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, OtherURL()}});
impl_.NavigationFrom(TestURL(), web_contents_);
EXPECT_EQ(std::vector<int64_t>({1}),
delegate_.set_notification_state_requests);
EXPECT_CALL(notifier_, NotifyInProgress(1));
impl_.SetNotificationStateComplete(1, true);
}
TEST_F(AutoFetchInternalImplTest, CompletedRequestUpdatesInProgressCount) {
impl_.TabModelReady();
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, TestURL()}});
impl_.RequestListInitialized(
std::vector<RequestInfo>{TestInfo(1, TestURL())});
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, OtherURL()}});
impl_.NavigationFrom(TestURL(), web_contents_);
EXPECT_CALL(notifier_, InProgressCountChanged(0));
impl_.RequestRemoved(TestInfo(1, TestURL()));
}
TEST_F(AutoFetchInternalImplTest, NavigatingFromNotifiesTwoRequests) {
impl_.TabModelReady();
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, TestURL()}});
impl_.RequestListInitialized(
{TestInfo(1, TestURL()), TestInfo(2, TestURL())});
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, OtherURL()}});
impl_.NavigationFrom(TestURL(), web_contents_);
EXPECT_EQ(std::vector<int64_t>({1, 2}),
delegate_.set_notification_state_requests);
EXPECT_CALL(notifier_, NotifyInProgress(2)).Times(2);
impl_.SetNotificationStateComplete(1, true);
impl_.SetNotificationStateComplete(2, true);
}
TEST_F(AutoFetchInternalImplTest, NavigatingFromBeforeInitialization) {
impl_.TabModelReady();
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, OtherURL()}});
impl_.RequestListInitialized(
{TestInfo(1, TestURL()), TestInfo(2, TestURL())});
EXPECT_EQ(std::vector<int64_t>({1, 2}),
delegate_.set_notification_state_requests);
EXPECT_CALL(notifier_, NotifyInProgress(2)).Times(2);
impl_.SetNotificationStateComplete(1, true);
impl_.SetNotificationStateComplete(2, true);
}
TEST_F(AutoFetchInternalImplTest, TabCloseNotifies) {
impl_.TabModelReady();
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, TestURL()}});
impl_.RequestListInitialized(
std::vector<RequestInfo>{TestInfo(1, TestURL())});
impl_.TabClosed(kDefaultTabId);
EXPECT_EQ(std::vector<int64_t>({1}),
delegate_.set_notification_state_requests);
EXPECT_CALL(notifier_, NotifyInProgress(1)).Times(1);
impl_.SetNotificationStateComplete(1, true);
}
TEST_F(AutoFetchInternalImplTest, RequestRemovedWhileSettingNotificationState) {
impl_.TabModelReady();
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, OtherURL()}});
impl_.RequestListInitialized(
std::vector<RequestInfo>{TestInfo(1, TestURL())});
EXPECT_EQ(std::vector<int64_t>({1}),
delegate_.set_notification_state_requests);
impl_.RequestRemoved({TestInfo(1, TestURL())});
EXPECT_CALL(notifier_, NotifyInProgress(1)).Times(0);
impl_.SetNotificationStateComplete(1, false);
}
TEST_F(AutoFetchInternalImplTest, OtherTabClosedDoesNotNotify) {
impl_.TabModelReady();
tab_finder_->SetTabs(std::map<int, GURL>{{kDefaultTabId, TestURL()}});
impl_.RequestListInitialized(
std::vector<RequestInfo>{TestInfo(1, TestURL())});
impl_.TabClosed(kDefaultTabId + 1);
EXPECT_EQ(std::vector<int64_t>(), delegate_.set_notification_state_requests);
}
class AutoFetchInternalImplFencedFrameTest
: public ChromeRenderViewHostTestHarness {
public:
AutoFetchInternalImplFencedFrameTest() = default;
~AutoFetchInternalImplFencedFrameTest() override = default;
void SetUp() override {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kFencedFrames, {{"implementation_type", "mparch"}});
ChromeRenderViewHostTestHarness::SetUp();
}
content::RenderFrameHost* CreateFencedFrame(
content::RenderFrameHost* parent) {
content::RenderFrameHost* fenced_frame =
content::RenderFrameHostTester::For(parent)->AppendFencedFrame();
return fenced_frame;
}
protected:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(AutoFetchInternalImplFencedFrameTest,
DoNotHandleFencedFrameNavigationAsSuccessfulPageNavigation) {
const GURL kTestUrl("http://mystery.site/foo.html");
auto web_content = CreateTestWebContents();
AutoFetchPageLoadWatcher::CreateForWebContents(web_content.get());
AutoFetchPageLoadWatcher* page_load_watcher =
OfflinePageAutoFetcherServiceFactory::GetForBrowserContext(
web_content->GetBrowserContext())
->page_load_watcher();
content::WebContentsTester::For(web_content.get())
->NavigateAndCommit(kTestUrl);
EXPECT_EQ(page_load_watcher->loaded_pages_for_testing().size(), 1U);
EXPECT_EQ(kTestUrl, page_load_watcher->loaded_pages_for_testing()[0]);
// Create a fenced frame and navigate a different URL.
content::RenderFrameHostTester::For(main_rfh())
->InitializeRenderFrameIfNeeded();
content::RenderFrameHost* fenced_frame_rfh =
CreateFencedFrame(web_content->GetPrimaryMainFrame());
GURL kFencedFrameUrl("http://fencedframe.com");
std::unique_ptr<content::NavigationSimulator> navigation_simulator =
content::NavigationSimulator::CreateRendererInitiated(kFencedFrameUrl,
fenced_frame_rfh);
navigation_simulator->Commit();
EXPECT_TRUE(fenced_frame_rfh->IsFencedFrameRoot());
// AutoFetchPageLoadWatcher should not store the URL navigated in the fenced
// frame.
EXPECT_EQ(page_load_watcher->loaded_pages_for_testing().size(), 1U);
EXPECT_EQ(kTestUrl, page_load_watcher->loaded_pages_for_testing()[0]);
}
} // namespace
} // namespace offline_pages