chromium/chrome/browser/download/android/available_offline_content_provider_unittest.cc

// 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/download/android/available_offline_content_provider.h"

#include <memory>
#include <tuple>
#include <utility>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/offline_items_collection/offline_content_aggregator_factory.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/common/available_offline_content.mojom-test-utils.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_profile.h"
#include "components/feed/core/shared_prefs/pref_names.h"
#include "components/offline_items_collection/core/offline_content_aggregator.h"
#include "components/offline_items_collection/core/offline_item.h"
#include "components/offline_items_collection/core/offline_item_state.h"
#include "components/offline_items_collection/core/test_support/mock_offline_content_provider.h"
#include "components/offline_pages/core/offline_page_feature.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "url/gurl.h"

namespace android {
namespace {

using offline_items_collection::OfflineContentAggregator;
using offline_items_collection::OfflineItem;
using offline_items_collection::OfflineItemState;
using offline_items_collection::OfflineItemVisuals;
using testing::_;
const char kProviderNamespace[] = "offline_pages";

OfflineItem UninterestingImageItem() {
  OfflineItem item;
  item.original_url = GURL("https://uninteresting");
  item.filter = offline_items_collection::FILTER_IMAGE;
  item.id.id = "UninterestingItem";
  item.id.name_space = kProviderNamespace;
  return item;
}

OfflineItem OfflinePageItem() {
  OfflineItem item;
  item.original_url = GURL("https://already_read");
  item.filter = offline_items_collection::FILTER_PAGE;
  item.id.id = "NonSuggestedOfflinePage";
  item.id.name_space = kProviderNamespace;
  item.last_accessed_time = base::Time::Now();
  return item;
}

OfflineItem SuggestedOfflinePageItem() {
  OfflineItem item;
  item.original_url = GURL("https://read_prefetched_page");
  item.filter = offline_items_collection::FILTER_PAGE;
  item.id.id = "SuggestedOfflinePage";
  item.id.name_space = kProviderNamespace;
  item.is_suggested = true;
  item.title = "Page Title";
  item.description = "snippet";
  // Using Time::Now() isn't ideal, but this should result in "4 hours ago"
  // even if the test takes 1 hour to run.
  item.creation_time = base::Time::Now() - base::Minutes(60 * 3.5);
  item.last_accessed_time = base::Time::Now();
  item.attribution = "attribution";
  return item;
}

OfflineItem VideoItem() {
  OfflineItem item;
  item.original_url = GURL("https://video");
  item.filter = offline_items_collection::FILTER_VIDEO;
  item.id.id = "VideoItem";
  item.id.name_space = kProviderNamespace;
  return item;
}

OfflineItem AudioItem() {
  OfflineItem item;
  item.original_url = GURL("https://audio");
  item.filter = offline_items_collection::FILTER_AUDIO;
  item.id.id = "AudioItem";
  item.id.name_space = kProviderNamespace;
  return item;
}

OfflineItem TransientItem() {
  OfflineItem item = VideoItem();
  item.is_transient = true;
  return item;
}

OfflineItem OffTheRecordItem() {
  OfflineItem item = VideoItem();
  item.is_off_the_record = true;
  return item;
}

OfflineItem IncompleteItem() {
  OfflineItem item = VideoItem();
  item.state = OfflineItemState::PAUSED;
  return item;
}

OfflineItem DangerousItem() {
  OfflineItem item = VideoItem();
  item.is_dangerous = true;
  return item;
}

OfflineItemVisuals TestThumbnail() {
  OfflineItemVisuals visuals;
  visuals.icon = gfx::test::CreateImage(2, 4);
  visuals.custom_favicon = gfx::test::CreateImage(4, 4);
  return visuals;
}

class AvailableOfflineContentTest : public ChromeRenderViewHostTestHarness {
 protected:
  void SetUp() override {
    ChromeRenderViewHostTestHarness::SetUp();

    content_provider_ = std::make_unique<
        offline_items_collection::MockOfflineContentProvider>();
    provider_ = std::make_unique<AvailableOfflineContentProvider>(
        main_rfh()->GetProcess()->GetID());

    aggregator_ =
        OfflineContentAggregatorFactory::GetForKey(profile()->GetProfileKey());
    aggregator_->RegisterProvider(kProviderNamespace, content_provider_.get());
    content_provider_->SetVisuals({});
  }

  void TearDown() override {
    provider_.release();
    content_provider_.release();

    ChromeRenderViewHostTestHarness::TearDown();
  }

  std::tuple<bool, std::vector<chrome::mojom::AvailableOfflineContentPtr>>
  ListAndWait() {
    base::test::TestFuture<
        bool, std::vector<chrome::mojom::AvailableOfflineContentPtr>>
        future;
    provider_->List(future.GetCallback());
    return future.Take();
  }

  std::unique_ptr<base::test::ScopedFeatureList> scoped_feature_list_ =
      std::make_unique<base::test::ScopedFeatureList>();
  raw_ptr<OfflineContentAggregator> aggregator_;
  std::unique_ptr<offline_items_collection::MockOfflineContentProvider>
      content_provider_;
  std::unique_ptr<AvailableOfflineContentProvider> provider_;
};

TEST_F(AvailableOfflineContentTest, NoContent) {
  auto [list_visible_by_prefs, suggestions] = ListAndWait();

  EXPECT_TRUE(suggestions.empty());
  EXPECT_TRUE(list_visible_by_prefs);
}

TEST_F(AvailableOfflineContentTest, TooFewInterestingItems) {
  // Adds items so that we're one-ff of reaching the minimum required count so
  // that any extra item considered interesting would effect the results.
  content_provider_->SetItems({UninterestingImageItem(), OfflinePageItem(),
                               SuggestedOfflinePageItem(), VideoItem(),
                               TransientItem(), OffTheRecordItem(),
                               IncompleteItem(), DangerousItem()});

  // Call List().
  auto [list_visible_by_prefs, suggestions] = ListAndWait();

  // As interesting items are below the minimum to show, nothing should be
  // reported.
  EXPECT_TRUE(suggestions.empty());
  EXPECT_TRUE(list_visible_by_prefs);
}

TEST_F(AvailableOfflineContentTest, FourInterestingItems) {
  // We need at least 4 interesting items for anything to show up at all.
  content_provider_->SetItems({UninterestingImageItem(), VideoItem(),
                               SuggestedOfflinePageItem(), AudioItem(),
                               OfflinePageItem()});

  content_provider_->SetVisuals(
      {{SuggestedOfflinePageItem().id, TestThumbnail()}});

  // Call List().
  auto [list_visible_by_prefs, suggestions] = ListAndWait();

  // Check that the right suggestions have been received in order.
  EXPECT_EQ(3ul, suggestions.size());
  EXPECT_EQ(SuggestedOfflinePageItem().id.id, suggestions[0]->id);
  EXPECT_EQ(VideoItem().id.id, suggestions[1]->id);
  EXPECT_EQ(AudioItem().id.id, suggestions[2]->id);
  EXPECT_TRUE(list_visible_by_prefs);

  // For a single suggestion, make sure all the fields are correct. We can
  // assume the other items match.
  const chrome::mojom::AvailableOfflineContentPtr& first = suggestions[0];
  const OfflineItem page_item = SuggestedOfflinePageItem();
  EXPECT_EQ(page_item.id.id, first->id);
  EXPECT_EQ(page_item.id.name_space, first->name_space);
  EXPECT_EQ(page_item.title, first->title);
  EXPECT_EQ(page_item.description, first->snippet);
  EXPECT_EQ("4 hours ago", first->date_modified);
  // At the time of writing this test, the output was:
  // data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAAECAYAAACk7+45AAAAFk
  // lEQVQYlWNk+M/wn4GBgYGJAQowGQBCcgIG00vTRwAAAABJRU5ErkJggg==
  // Since other encodings are possible, just check the prefix. PNGs all have
  // the same 8 byte header.
  EXPECT_TRUE(base::StartsWith(first->thumbnail_data_uri.spec(),
                               "data:image/png;base64,iVBORw0K",
                               base::CompareCase::SENSITIVE));
  EXPECT_TRUE(base::StartsWith(first->favicon_data_uri.spec(),
                               "data:image/png;base64,iVBORw0K",
                               base::CompareCase::SENSITIVE));
  EXPECT_EQ(page_item.attribution, first->attribution);
}

TEST_F(AvailableOfflineContentTest, ListVisibilityChanges) {
  // We need at least 4 interesting items for anything to show up at all.
  content_provider_->SetItems({UninterestingImageItem(), VideoItem(),
                               SuggestedOfflinePageItem(), AudioItem(),
                               OfflinePageItem()});

  content_provider_->SetVisuals(
      {{SuggestedOfflinePageItem().id, TestThumbnail()}});
  // Set pref to hide the list.
  profile()->GetPrefs()->SetBoolean(feed::prefs::kArticlesListVisible, false);

  // Call List().
  auto [list_visible_by_prefs, suggestions] = ListAndWait();

  // Check that suggestions have been received and the list is not visible.
  EXPECT_EQ(3ul, suggestions.size());
  EXPECT_FALSE(list_visible_by_prefs);

  // Simulate visibility changed by the user to "shown".
  provider_->ListVisibilityChanged(true);

  EXPECT_TRUE(
      profile()->GetPrefs()->GetBoolean(feed::prefs::kArticlesListVisible));

  // Call List() again and check list is not visible.
  std::tie(list_visible_by_prefs, suggestions) = ListAndWait();
  EXPECT_TRUE(list_visible_by_prefs);
}

}  // namespace
}  // namespace android