chromium/chrome/renderer/net/net_error_helper_core_unittest.cc

// Copyright 2013 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/renderer/net/net_error_helper_core.h"

#include <stddef.h>

#include <map>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/common/available_offline_content.mojom.h"
#include "chrome/renderer/net/available_offline_content_helper.h"
#include "components/error_page/common/error.h"
#include "components/error_page/common/net_error_info.h"
#include "components/grit/components_resources.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/mock_render_thread.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "net/base/net_errors.h"
#include "net/dns/public/resolve_error_info.h"
#include "skia/ext/skia_utils_base.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_ANDROID)
#include "chrome/common/offline_page_auto_fetcher.mojom.h"
#endif

namespace {

const char kFailedUrl[] =;

// Creates a string from an error that is used as a mock locally generated
// error page for that error.
std::string ErrorToString(const error_page::Error& error, bool is_failed_post) {}

error_page::Error ProbeError(error_page::DnsProbeStatus status) {}

error_page::Error NetErrorForURL(net::Error net_error, const GURL& url) {}

error_page::Error NetError(net::Error net_error) {}

// Convenience functions that create an error string for a non-POST request.

std::string ProbeErrorString(error_page::DnsProbeStatus status) {}

std::string NetErrorStringForURL(net::Error net_error, const GURL& url) {}

std::string NetErrorString(net::Error net_error) {}

error_page::LocalizedError::PageState GetErrorPageState(int error_code,
                                                        bool is_kiosk_mode) {}

class NetErrorHelperCoreTest : public testing::Test,
                               public NetErrorHelperCore::Delegate {};

//------------------------------------------------------------------------------
// Basic tests that don't update the error page for probes.
//------------------------------------------------------------------------------

TEST_F(NetErrorHelperCoreTest, Null) {}

TEST_F(NetErrorHelperCoreTest, SuccessfulPageLoad) {}

TEST_F(NetErrorHelperCoreTest, MainFrameNonDnsError) {}

// Much like above tests, but with a bunch of spurious DNS status messages that
// should have no effect.
TEST_F(NetErrorHelperCoreTest, MainFrameNonDnsErrorSpuriousStatus) {}

TEST_F(NetErrorHelperCoreTest,
       UserModeErrBlockedByAdministratorContainsDetails) {}

TEST_F(NetErrorHelperCoreTest,
       KioskModeErrBlockedByAdministratorDoenNotContainDetails) {}

TEST_F(NetErrorHelperCoreTest, SubFrameErrorWithCustomErrorPage) {}

TEST_F(NetErrorHelperCoreTest, SubFrameDnsError) {}

// Much like above tests, but with a bunch of spurious DNS status messages that
// should have no effect.
TEST_F(NetErrorHelperCoreTest, SubFrameDnsErrorSpuriousStatus) {}

//------------------------------------------------------------------------------
// Tests for updating the error page in response to DNS probe results.
//------------------------------------------------------------------------------

// Test case where the error page finishes loading before receiving any DNS
// probe messages.
TEST_F(NetErrorHelperCoreTest, FinishedBeforeProbe) {}

// Same as above, but the probe is not run.
TEST_F(NetErrorHelperCoreTest, FinishedBeforeProbeNotRun) {}

// Same as above, but the probe result is inconclusive.
TEST_F(NetErrorHelperCoreTest, FinishedBeforeProbeInconclusive) {}

// Same as above, but the probe result is no internet.
TEST_F(NetErrorHelperCoreTest, FinishedBeforeProbeNoInternet) {}

// Same as above, but the probe result is bad config.
TEST_F(NetErrorHelperCoreTest, FinishedBeforeProbeBadConfig) {}

// Test case where the error page finishes loading after receiving the start
// DNS probe message.
TEST_F(NetErrorHelperCoreTest, FinishedAfterStartProbe) {}

// Test case where the error page finishes loading before receiving any DNS
// probe messages and the request is a POST.
TEST_F(NetErrorHelperCoreTest, FinishedBeforeProbePost) {}

// Test case where the probe finishes before the page is committed.
TEST_F(NetErrorHelperCoreTest, ProbeFinishesEarly) {}

// Test case where one error page loads completely before a new navigation
// results in another error page.  Probes are run for both pages.
TEST_F(NetErrorHelperCoreTest, TwoErrorsWithProbes) {}

// Test case where one error page loads completely before a new navigation
// results in another error page.  Probe results for the first probe are only
// received after the second load starts, but before it commits.
TEST_F(NetErrorHelperCoreTest, TwoErrorsWithProbesAfterSecondStarts) {}

// Same as above, but a new page is loaded before the error page commits.
TEST_F(NetErrorHelperCoreTest, ErrorPageLoadInterrupted) {}

TEST_F(NetErrorHelperCoreTest, ExplicitReloadSucceeds) {}

TEST_F(NetErrorHelperCoreTest, CanNotShowNetworkDiagnostics) {}

TEST_F(NetErrorHelperCoreTest, CanShowNetworkDiagnostics) {}

TEST_F(NetErrorHelperCoreTest, AlternativeErrorPageNoUpdates) {}

#if BUILDFLAG(IS_ANDROID)
TEST_F(NetErrorHelperCoreTest, Download) {
  DoErrorLoad(net::ERR_INTERNET_DISCONNECTED);
  EXPECT_EQ(0, download_count());
  core()->ExecuteButtonPress(NetErrorHelperCore::DOWNLOAD_BUTTON);
  EXPECT_EQ(1, download_count());
}

const char kThumbnailDataURI[] = "data:image/png;base64,abc";
const char kFaviconDataURI[] = "data:image/png;base64,def";

// Creates a couple of fake AvailableOfflineContent instances.
std::vector<chrome::mojom::AvailableOfflineContentPtr>
GetFakeAvailableContent() {
  std::vector<chrome::mojom::AvailableOfflineContentPtr> content;
  content.push_back(chrome::mojom::AvailableOfflineContent::New(
      "ID", "name_space", "title", "snippet", "date_modified", "attribution",
      GURL(kThumbnailDataURI), GURL(kFaviconDataURI),
      chrome::mojom::AvailableContentType::kPrefetchedPage));
  content.push_back(chrome::mojom::AvailableOfflineContent::New(
      "ID2", "name_space2", "title2", "snippet2", "date_modified2",
      "attribution2", GURL(kThumbnailDataURI), GURL(kFaviconDataURI),
      chrome::mojom::AvailableContentType::kOtherPage));
  return content;
}

// Builds the expected JSON representation of the AvailableOfflineContent
// instances returned by |GetFakeAvailableContent|.
const std::string GetExpectedAvailableContentAsJson() {
  // About the below data:
  // * |content_type| is an AvailableContentType enum value where
  //   0 = kPrefetchedPage and 3=kOtherPage.
  // * The base64 encoded values represent the encoded versions of the
  //   respective entries returned by |GetFakeAvailableContent|.
  std::string want_json = R"([
    {
      "ID": "ID",
      "attribution_base64": "AGEAdAB0AHIAaQBiAHUAdABpAG8Abg==",
      "content_type": 0,
      "date_modified": "date_modified",
      "favicon_data_uri": "data:image/png;base64,def",
      "name_space": "name_space",
      "snippet_base64": "AHMAbgBpAHAAcABlAHQ=",
      "thumbnail_data_uri": "data:image/png;base64,abc",
      "title_base64": "AHQAaQB0AGwAZQ=="
    },
    {
      "ID": "ID2",
      "attribution_base64": "AGEAdAB0AHIAaQBiAHUAdABpAG8AbgAy",
      "content_type": 3,
      "date_modified": "date_modified2",
      "favicon_data_uri": "data:image/png;base64,def",
      "name_space": "name_space2",
      "snippet_base64": "AHMAbgBpAHAAcABlAHQAMg==",
      "thumbnail_data_uri": "data:image/png;base64,abc",
      "title_base64": "AHQAaQB0AGwAZQAy"
    }
  ])";
  base::ReplaceChars(want_json, base::kWhitespaceASCII, "", &want_json);
  return want_json;
}

class FakeAvailableOfflineContentProvider
    : public chrome::mojom::AvailableOfflineContentProvider {
 public:
  FakeAvailableOfflineContentProvider() = default;

  FakeAvailableOfflineContentProvider(
      const FakeAvailableOfflineContentProvider&) = delete;
  FakeAvailableOfflineContentProvider& operator=(
      const FakeAvailableOfflineContentProvider&) = delete;

  void List(ListCallback callback) override {
    if (return_content_) {
      std::move(callback).Run(list_visible_by_prefs_,
                              GetFakeAvailableContent());
    } else {
      std::move(callback).Run(list_visible_by_prefs_, {});
    }
  }

  MOCK_METHOD2(LaunchItem,
               void(const std::string& item_ID, const std::string& name_space));
  MOCK_METHOD1(LaunchDownloadsPage, void(bool open_prefetched_articles_tab));
  MOCK_METHOD1(ListVisibilityChanged, void(bool is_visible));

  void AddBinding(
      mojo::PendingReceiver<chrome::mojom::AvailableOfflineContentProvider>
          receiver) {
    receivers_.Add(this, std::move(receiver));
  }

  void set_return_content(bool return_content) {
    return_content_ = return_content;
  }

  void set_list_visible_by_prefs(bool list_visible_by_prefs) {
    list_visible_by_prefs_ = list_visible_by_prefs;
  }

 private:
  bool return_content_ = true;
  bool list_visible_by_prefs_ = true;
  mojo::ReceiverSet<chrome::mojom::AvailableOfflineContentProvider> receivers_;
};

// Provides set up for testing the 'available offline content' feature.
class NetErrorHelperCoreAvailableOfflineContentTest
    : public NetErrorHelperCoreTest {
 public:
  void SetUp() override {
    NetErrorHelperCoreTest::SetUp();
    AvailableOfflineContentHelper::OverrideBinderForTesting(
        base::BindRepeating(&FakeAvailableOfflineContentProvider::AddBinding,
                            base::Unretained(&fake_provider_)));
  }

  void TearDown() override {
    AvailableOfflineContentHelper::OverrideBinderForTesting(
        base::NullCallback());
  }

 protected:
  FakeAvailableOfflineContentProvider fake_provider_;
  base::HistogramTester histogram_tester_;
};

TEST_F(NetErrorHelperCoreAvailableOfflineContentTest, ListAvailableContent) {
  set_offline_content_feature_enabled(true);
  fake_provider_.set_return_content(true);

  DoErrorLoad(net::ERR_INTERNET_DISCONNECTED);
  task_environment()->RunUntilIdle();
  EXPECT_TRUE(list_visible_by_prefs());
  EXPECT_EQ(GetExpectedAvailableContentAsJson(), offline_content_json());

  histogram_tester_.ExpectBucketCount(
      "Net.ErrorPageCounts",
      error_page::NETWORK_ERROR_PAGE_OFFLINE_SUGGESTIONS_SHOWN, 1);
  histogram_tester_.ExpectBucketCount(
      "Net.ErrorPageCounts",
      error_page::NETWORK_ERROR_PAGE_OFFLINE_SUGGESTIONS_SHOWN_COLLAPSED, 0);

  core()->LaunchOfflineItem("ID", "name_space");
  histogram_tester_.ExpectBucketCount(
      "Net.ErrorPageCounts",
      error_page::NETWORK_ERROR_PAGE_OFFLINE_SUGGESTION_CLICKED, 1);

  core()->LaunchDownloadsPage();
  histogram_tester_.ExpectBucketCount(
      "Net.ErrorPageCounts",
      error_page::NETWORK_ERROR_PAGE_OFFLINE_DOWNLOADS_PAGE_CLICKED, 1);
}

TEST_F(NetErrorHelperCoreAvailableOfflineContentTest, ListHiddenByPrefs) {
  set_offline_content_feature_enabled(true);
  fake_provider_.set_return_content(true);
  fake_provider_.set_list_visible_by_prefs(false);

  DoErrorLoad(net::ERR_INTERNET_DISCONNECTED);
  task_environment()->RunUntilIdle();
  EXPECT_FALSE(list_visible_by_prefs());
  EXPECT_EQ(GetExpectedAvailableContentAsJson(), offline_content_json());

  histogram_tester_.ExpectBucketCount(
      "Net.ErrorPageCounts",
      error_page::NETWORK_ERROR_PAGE_OFFLINE_SUGGESTIONS_SHOWN, 0);
  histogram_tester_.ExpectBucketCount(
      "Net.ErrorPageCounts",
      error_page::NETWORK_ERROR_PAGE_OFFLINE_SUGGESTIONS_SHOWN_COLLAPSED, 1);

  core()->LaunchOfflineItem("ID", "name_space");
  histogram_tester_.ExpectBucketCount(
      "Net.ErrorPageCounts",
      error_page::NETWORK_ERROR_PAGE_OFFLINE_SUGGESTION_CLICKED, 1);

  core()->LaunchDownloadsPage();
  histogram_tester_.ExpectBucketCount(
      "Net.ErrorPageCounts",
      error_page::NETWORK_ERROR_PAGE_OFFLINE_DOWNLOADS_PAGE_CLICKED, 1);
}

TEST_F(NetErrorHelperCoreAvailableOfflineContentTest, ListNoAvailableContent) {
  set_offline_content_feature_enabled(true);
  fake_provider_.set_return_content(false);

  DoErrorLoad(net::ERR_INTERNET_DISCONNECTED);
  task_environment()->RunUntilIdle();

  EXPECT_TRUE(list_visible_by_prefs());
  EXPECT_EQ("", offline_content_json());
  histogram_tester_.ExpectBucketCount(
      "Net.ErrorPageCounts",
      error_page::NETWORK_ERROR_PAGE_OFFLINE_SUGGESTIONS_SHOWN, 0);
  histogram_tester_.ExpectBucketCount(
      "Net.ErrorPageCounts",
      error_page::NETWORK_ERROR_PAGE_OFFLINE_SUGGESTIONS_SHOWN_COLLAPSED, 0);
}

TEST_F(NetErrorHelperCoreAvailableOfflineContentTest, NotAllowed) {
  set_offline_content_feature_enabled(false);
  fake_provider_.set_return_content(true);

  DoErrorLoad(net::ERR_INTERNET_DISCONNECTED);
  task_environment()->RunUntilIdle();

  EXPECT_TRUE(list_visible_by_prefs());
  EXPECT_EQ("", offline_content_json());
  histogram_tester_.ExpectBucketCount(
      "Net.ErrorPageCounts",
      error_page::NETWORK_ERROR_PAGE_OFFLINE_SUGGESTIONS_SHOWN, 0);
  histogram_tester_.ExpectBucketCount(
      "Net.ErrorPageCounts",
      error_page::NETWORK_ERROR_PAGE_OFFLINE_SUGGESTIONS_SHOWN_COLLAPSED, 0);
}

class FakeOfflinePageAutoFetcher
    : public chrome::mojom::OfflinePageAutoFetcher {
 public:
  FakeOfflinePageAutoFetcher() = default;

  FakeOfflinePageAutoFetcher(const FakeOfflinePageAutoFetcher&) = delete;
  FakeOfflinePageAutoFetcher& operator=(const FakeOfflinePageAutoFetcher&) =
      delete;

  struct TryScheduleParameters {
    bool user_requested;
    TryScheduleCallback callback;
  };

  void TrySchedule(bool user_requested, TryScheduleCallback callback) override {
    try_schedule_calls_.push_back({user_requested, std::move(callback)});
  }

  void CancelSchedule() override { cancel_calls_++; }

  void AddReceiver(
      mojo::PendingReceiver<chrome::mojom::OfflinePageAutoFetcher> receiver) {
    receivers_.Add(this, std::move(receiver));
  }

  int cancel_calls() const { return cancel_calls_; }
  std::vector<TryScheduleParameters> take_try_schedule_calls() {
    std::vector<TryScheduleParameters> result = std::move(try_schedule_calls_);
    try_schedule_calls_.clear();
    return result;
  }

 private:
  mojo::ReceiverSet<chrome::mojom::OfflinePageAutoFetcher> receivers_;
  int cancel_calls_ = 0;
  std::vector<TryScheduleParameters> try_schedule_calls_;
};
// This uses the real implementation of PageAutoFetcherHelper, but with a
// substituted fetcher.
class TestPageAutoFetcherHelper : public PageAutoFetcherHelper {
 public:
  explicit TestPageAutoFetcherHelper(
      base::RepeatingCallback<
          mojo::PendingRemote<chrome::mojom::OfflinePageAutoFetcher>()> binder)
      : PageAutoFetcherHelper(nullptr), binder_(binder) {}
  bool Bind() override {
    if (!fetcher_)
      fetcher_.Bind(binder_.Run());
    return true;
  }

 private:
  base::RepeatingCallback<
      mojo::PendingRemote<chrome::mojom::OfflinePageAutoFetcher>()>
      binder_;
};

// Provides set up for testing the 'auto fetch on dino' feature.
class NetErrorHelperCoreAutoFetchTest : public NetErrorHelperCoreTest {
 public:
  void SetUp() override {
    NetErrorHelperCoreTest::SetUp();
    auto binder = base::BindLambdaForTesting([&]() {
      mojo::PendingRemote<chrome::mojom::OfflinePageAutoFetcher> fetcher_remote;
      fake_fetcher_.AddReceiver(
          fetcher_remote.InitWithNewPipeAndPassReceiver());
      return fetcher_remote;
    });

    core()->SetPageAutoFetcherHelperForTesting(
        std::make_unique<TestPageAutoFetcherHelper>(binder));
  }

  void TearDown() override {
    AvailableOfflineContentHelper::OverrideBinderForTesting(
        base::NullCallback());
  }

 protected:
  FakeOfflinePageAutoFetcher fake_fetcher_;
};

TEST_F(NetErrorHelperCoreAutoFetchTest, NotAllowed) {
  set_auto_fetch_allowed(false);

  DoErrorLoad(net::ERR_INTERNET_DISCONNECTED);
  task_environment()->RunUntilIdle();

  // When auto fetch is not allowed, OfflinePageAutoFetcher is not called.
  std::vector<FakeOfflinePageAutoFetcher::TryScheduleParameters> calls =
      fake_fetcher_.take_try_schedule_calls();
  EXPECT_EQ(0ul, calls.size());
}

TEST_F(NetErrorHelperCoreAutoFetchTest, AutoFetchTriggered) {
  set_auto_fetch_allowed(true);

  DoErrorLoad(net::ERR_INTERNET_DISCONNECTED);
  task_environment()->RunUntilIdle();

  // Auto fetch is allowed, so OfflinePageAutoFetcher is called once.
  std::vector<FakeOfflinePageAutoFetcher::TryScheduleParameters> calls =
      fake_fetcher_.take_try_schedule_calls();
  EXPECT_EQ(1ul, calls.size());

  // Finalize the call to TrySchedule, and verify the delegate is called.
  std::move(calls[0].callback)
      .Run(chrome::mojom::OfflinePageAutoFetcherScheduleResult::kScheduled);
  task_environment()->RunUntilIdle();

  EXPECT_EQ(chrome::mojom::OfflinePageAutoFetcherScheduleResult::kScheduled,
            auto_fetch_state());
}

#endif  // BUILDFLAG(IS_ANDROID)

}  // namespace