chromium/ios/web/navigation/wk_navigation_util_unittest.mm

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/web/navigation/wk_navigation_util.h"

#import <memory>
#import <vector>

#import "base/json/json_reader.h"
#import "base/strings/escape.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/values.h"
#import "ios/web/common/features.h"
#import "ios/web/navigation/navigation_item_impl.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/test/test_url_constants.h"
#import "net/base/apple/url_conversions.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"
#import "url/scheme_host_port.h"

namespace web {
namespace wk_navigation_util {

namespace {

// Creates a vector with given number of navigation items. All items will
// have distinct titles and URLs.
void CreateTestNavigationItems(
    size_t count,
    std::vector<std::unique_ptr<NavigationItem>>& items) {
  for (size_t i = 0; i < count; i++) {
    auto item = std::make_unique<NavigationItemImpl>();
    item->SetURL(GURL(base::StringPrintf("http://www.%zu.com", i)));
    item->SetTitle(base::ASCIIToUTF16(base::StringPrintf("Test%zu", i)));
    items.push_back(std::move(item));
  }
}

// Extracts session dictionary from `restore_session_url`.
base::JSONReader::Result ExtractSessionDict(GURL restore_session_url) {
  NSString* fragment = net::NSURLWithGURL(restore_session_url).fragment;
  NSString* encoded_session =
      [fragment substringFromIndex:strlen(kRestoreSessionSessionHashPrefix)];
  std::string session_json = base::UnescapeBinaryURLComponent(
      base::SysNSStringToUTF8(encoded_session));
  return base::JSONReader::ReadAndReturnValueWithError(session_json,
                                                       base::JSON_PARSE_RFC);
}

}  // namespace

typedef PlatformTest WKNavigationUtilTest;

// Tests various inputs for GetSafeItemRange.
TEST_F(WKNavigationUtilTest, GetSafeItemRange) {
  // Session size does not exceed kMaxSessionSize and last_committed_item_index
  // is in range.
  for (int item_count = 0; item_count <= kMaxSessionSize; item_count++) {
    for (int item_index = 0; item_index < item_count; item_index++) {
      int offset = 0;
      int size = 0;
      EXPECT_EQ(item_index,
                GetSafeItemRange(item_index, item_count, &offset, &size))
          << "item_count: " << item_count << " item_index: " << item_index;
      EXPECT_EQ(0, offset) << "item_count: " << item_count
                           << " item_index: " << item_index;
      EXPECT_EQ(item_count, size)
          << "item_count: " << item_count << " item_index: " << item_index;
    }
  }

  // Session size is 1 item longer than kMaxSessionSize.
  int offset = 0;
  int size = 0;
  EXPECT_EQ(0, GetSafeItemRange(0, kMaxSessionSize + 1, &offset, &size));
  EXPECT_EQ(0, offset);
  EXPECT_EQ(kMaxSessionSize, size);

  offset = 0;
  size = 0;
  EXPECT_EQ(
      kMaxSessionSize - 1,
      GetSafeItemRange(kMaxSessionSize, kMaxSessionSize + 1, &offset, &size));
  EXPECT_EQ(1, offset);
  EXPECT_EQ(kMaxSessionSize, size);

  offset = 0;
  size = 0;
  EXPECT_EQ(kMaxSessionSize / 2,
            GetSafeItemRange(kMaxSessionSize / 2, kMaxSessionSize + 1, &offset,
                             &size));
  EXPECT_EQ(0, offset);
  EXPECT_EQ(kMaxSessionSize, size);

  offset = 0;
  size = 0;
  EXPECT_EQ(kMaxSessionSize / 2,
            GetSafeItemRange(kMaxSessionSize / 2 + 1, kMaxSessionSize + 1,
                             &offset, &size));
  EXPECT_EQ(1, offset);
  EXPECT_EQ(kMaxSessionSize, size);

  offset = 0;
  size = 0;
  EXPECT_EQ(kMaxSessionSize / 2 - 1,
            GetSafeItemRange(kMaxSessionSize / 2 - 1, kMaxSessionSize + 1,
                             &offset, &size));
  EXPECT_EQ(0, offset);
  EXPECT_EQ(kMaxSessionSize, size);
}

TEST_F(WKNavigationUtilTest, CreateRestoreSessionUrl) {
  auto item0 = std::make_unique<NavigationItemImpl>();
  item0->SetURL(GURL("http://www.0.com"));
  item0->SetTitle(u"Test Website 0");
  auto item1 = std::make_unique<NavigationItemImpl>();
  item1->SetURL(GURL("http://www.1.com"));
  // Create an App-specific URL
  auto item2 = std::make_unique<NavigationItemImpl>();
  GURL url2("http://webui");
  GURL::Replacements scheme_replacements;
  scheme_replacements.SetSchemeStr(kTestWebUIScheme);
  item2->SetURL(url2.ReplaceComponents(scheme_replacements));

  std::vector<std::unique_ptr<NavigationItem>> items;
  items.push_back(std::move(item0));
  items.push_back(std::move(item1));
  items.push_back(std::move(item2));

  int first_index = 0;
  GURL restore_session_url;
  CreateRestoreSessionUrl(0 /* last_committed_item_index */, items,
                          &restore_session_url, &first_index);
  ASSERT_EQ(0, first_index);
  ASSERT_TRUE(IsRestoreSessionUrl(restore_session_url));
  ASSERT_TRUE(IsRestoreSessionUrl(net::NSURLWithGURL(restore_session_url)));

  std::string session_json =
      base::UnescapeBinaryURLComponent(restore_session_url.ref());

  EXPECT_EQ("session={\"offset\":-2,\"titles\":[\"Test Website 0\",\"\",\"\"],"
            "\"urls\":[\"http://www.0.com/\",\"http://www.1.com/\","
            "\"testwebui://webui/\"]}",
            session_json);
}

// In the past the math within CreateRestoreSessionUrl has had some edge case
// crashes.  Ensure that nothing crashes.
TEST_F(WKNavigationUtilTest, CreateRestoreSessionBruteForce) {
  int first_index = 0;
  GURL restore_session_url;
  for (int num_items = 70; num_items < 80; num_items++) {
    std::vector<std::unique_ptr<NavigationItem>> items;
    CreateTestNavigationItems(num_items, items);
    for (int last_committed_index = 0; last_committed_index < num_items;
         last_committed_index++) {
      CreateRestoreSessionUrl(last_committed_index, items, &restore_session_url,
                              &first_index);
      // Extract session JSON from restoration URL.
      auto value_with_error = ExtractSessionDict(restore_session_url);
      const base::Value::Dict& dict = value_with_error->GetDict();

      const base::Value::List* urls_value = dict.FindList("urls");
      if (num_items > kMaxSessionSize) {
        ASSERT_EQ(kMaxSessionSize, static_cast<int>(urls_value->size()));
      } else {
        ASSERT_EQ(num_items, static_cast<int>(urls_value->size()));
      }
    }
  }
}

// Verifies that large session can be stored in NSURL. GURL is converted to
// NSURL, because NSURL is passed to WKWebView during the session restoration.
TEST_F(WKNavigationUtilTest, CreateRestoreSessionUrlForLargeSession) {
  // Create restore session URL with large number of items.
  const size_t kItemCount = kMaxSessionSize;
  std::vector<std::unique_ptr<NavigationItem>> items;
  CreateTestNavigationItems(kItemCount, items);
  int first_index = 0;
  GURL restore_session_url;
  CreateRestoreSessionUrl(
      /*last_committed_item_index=*/0, items, &restore_session_url,
      &first_index);
  ASSERT_TRUE(IsRestoreSessionUrl(restore_session_url));
  ASSERT_TRUE(IsRestoreSessionUrl(net::NSURLWithGURL(restore_session_url)));

  // Extract session JSON from restoration URL.
  auto value_with_error = ExtractSessionDict(restore_session_url);
  ASSERT_TRUE(value_with_error.has_value());
  const base::Value::Dict& dict = value_with_error->GetDict();

  // Verify that all titles and URLs are present.
  const base::Value::List* titles_value = dict.FindList("titles");
  ASSERT_TRUE(titles_value);
  ASSERT_EQ(kItemCount, titles_value->size());

  const base::Value::List* urls_value = dict.FindList("urls");
  ASSERT_TRUE(urls_value);
  ASSERT_EQ(kItemCount, urls_value->size());
}

// Verifies that large session can be stored in NSURL and that extra items
// are trimmed from the right side of `last_committed_item_index`.
TEST_F(WKNavigationUtilTest, CreateRestoreSessionUrlForExtraLargeForwardList) {
  // Create restore session URL with large number of items that exceeds
  // kMaxSessionSize.
  const size_t kItemCount = kMaxSessionSize * 3;
  std::vector<std::unique_ptr<NavigationItem>> items;
  CreateTestNavigationItems(kItemCount, items);
  int first_index = 0;
  GURL restore_session_url;
  CreateRestoreSessionUrl(
      /*last_committed_item_index=*/0, items, &restore_session_url,
      &first_index);
  ASSERT_EQ(0, first_index);
  ASSERT_TRUE(IsRestoreSessionUrl(restore_session_url));
  ASSERT_TRUE(IsRestoreSessionUrl(net::NSURLWithGURL(restore_session_url)));

  // Extract session JSON from restoration URL.
  auto value_with_error = ExtractSessionDict(restore_session_url);
  ASSERT_TRUE(value_with_error.has_value());
  const base::Value::Dict& dict = value_with_error->GetDict();

  // Verify that first kMaxSessionSize titles and URLs are present.
  const base::Value::List* titles_value = dict.FindList("titles");
  ASSERT_TRUE(titles_value);
  ASSERT_EQ(static_cast<size_t>(kMaxSessionSize), titles_value->size());
  ASSERT_EQ("Test0", (*titles_value)[0].GetString());
  ASSERT_EQ("Test74", (*titles_value)[kMaxSessionSize - 1].GetString());

  const base::Value::List* urls_value = dict.FindList("urls");
  ASSERT_TRUE(urls_value);
  ASSERT_EQ(static_cast<size_t>(kMaxSessionSize), urls_value->size());
  ASSERT_EQ("http://www.0.com/", (*urls_value)[0].GetString());
  ASSERT_EQ("http://www.74.com/",
            (*urls_value)[kMaxSessionSize - 1].GetString());

  // Verify the offset is correct.
  ASSERT_EQ(1 - kMaxSessionSize, *dict.FindInt("offset"));
}

// Verifies that large session can be stored in NSURL and that extra items
// are trimmed from the left side of `last_committed_item_index`.
TEST_F(WKNavigationUtilTest, CreateRestoreSessionUrlForExtraLargeBackList) {
  // Create restore session URL with large number of items that exceeds
  // kMaxSessionSize.
  const size_t kItemCount = kMaxSessionSize * 3;
  std::vector<std::unique_ptr<NavigationItem>> items;
  CreateTestNavigationItems(kItemCount, items);
  int first_index = 0;
  GURL restore_session_url;
  CreateRestoreSessionUrl(
      /*last_committed_item_index=*/kItemCount - 1, items, &restore_session_url,
      &first_index);
  ASSERT_EQ(150, first_index);
  ASSERT_TRUE(IsRestoreSessionUrl(restore_session_url));
  ASSERT_TRUE(IsRestoreSessionUrl(net::NSURLWithGURL(restore_session_url)));

  // Extract session JSON from restoration URL.
  auto value_with_error = ExtractSessionDict(restore_session_url);
  ASSERT_TRUE(value_with_error.has_value());
  const base::Value::Dict& dict = value_with_error->GetDict();

  // Verify that last kMaxSessionSize titles and URLs are present.
  const base::Value::List* titles_value = dict.FindList("titles");
  ASSERT_TRUE(titles_value);
  ASSERT_EQ(static_cast<size_t>(kMaxSessionSize), titles_value->size());
  ASSERT_EQ("Test150", (*titles_value)[0].GetString());
  ASSERT_EQ("Test224", (*titles_value)[kMaxSessionSize - 1].GetString());

  const base::Value::List* urls_value = dict.FindList("urls");
  ASSERT_TRUE(urls_value);
  ASSERT_EQ(static_cast<size_t>(kMaxSessionSize), urls_value->size());
  ASSERT_EQ("http://www.150.com/", (*urls_value)[0].GetString());
  ASSERT_EQ("http://www.224.com/",
            (*urls_value)[kMaxSessionSize - 1].GetString());

  // Verify the offset is correct.
  ASSERT_EQ(0, *dict.FindInt("offset"));
}

// Verifies that large session can be stored in NSURL and that extra items
// are trimmed from the left and right sides of `last_committed_item_index`.
TEST_F(WKNavigationUtilTest,
       CreateRestoreSessionUrlForExtraLargeBackAndForwardList) {
  // Create restore session URL with large number of items that exceeds
  // kMaxSessionSize.
  const size_t kItemCount = kMaxSessionSize * 2;
  std::vector<std::unique_ptr<NavigationItem>> items;
  CreateTestNavigationItems(kItemCount, items);
  int first_index = 0;
  GURL restore_session_url;
  CreateRestoreSessionUrl(
      /*last_committed_item_index=*/kMaxSessionSize, items,
      &restore_session_url, &first_index);
  ASSERT_EQ(38, first_index);
  ASSERT_TRUE(IsRestoreSessionUrl(restore_session_url));
  ASSERT_TRUE(IsRestoreSessionUrl(net::NSURLWithGURL(restore_session_url)));

  // Extract session JSON from restoration URL.
  auto value_with_error = ExtractSessionDict(restore_session_url);
  ASSERT_TRUE(value_with_error.has_value());
  const base::Value::Dict& dict = value_with_error->GetDict();

  // Verify that last kMaxSessionSize titles and URLs are present.
  const base::Value::List* titles_value = dict.FindList("titles");
  ASSERT_TRUE(titles_value);
  ASSERT_EQ(static_cast<size_t>(kMaxSessionSize), titles_value->size());
  ASSERT_EQ("Test38", (*titles_value)[0].GetString());
  ASSERT_EQ("Test112", (*titles_value)[kMaxSessionSize - 1].GetString());

  const base::Value::List* urls_value = dict.FindList("urls");
  ASSERT_TRUE(urls_value);
  ASSERT_EQ(static_cast<size_t>(kMaxSessionSize), urls_value->size());
  ASSERT_EQ("http://www.38.com/", (*urls_value)[0].GetString());
  ASSERT_EQ("http://www.112.com/",
            (*urls_value)[kMaxSessionSize - 1].GetString());

  // Verify the offset is correct.
  ASSERT_EQ((1 - kMaxSessionSize) / 2, *dict.FindInt("offset"));
}

TEST_F(WKNavigationUtilTest, IsNotRestoreSessionUrl) {
  EXPECT_FALSE(IsRestoreSessionUrl(GURL()));
  EXPECT_FALSE(IsRestoreSessionUrl([NSURL URLWithString:@""]));
  EXPECT_FALSE(IsRestoreSessionUrl(GURL("file://somefile")));
  EXPECT_FALSE(IsRestoreSessionUrl([NSURL URLWithString:@"file://somefile"]));
  EXPECT_FALSE(IsRestoreSessionUrl(GURL("http://www.1.com")));
  EXPECT_FALSE(IsRestoreSessionUrl([NSURL URLWithString:@"http://www.1.com"]));
}

// Tests that app specific urls and non-placeholder about: urls do not need a
// user agent type, but normal urls and placeholders do.
TEST_F(WKNavigationUtilTest, URLNeedsUserAgentType) {
  // Not app specific or non-placeholder about urls.
  GURL non_user_agent_urls("http://newtab");
  GURL::Replacements scheme_replacements;
  scheme_replacements.SetSchemeStr(kTestAppSpecificScheme);
  EXPECT_FALSE(URLNeedsUserAgentType(
      non_user_agent_urls.ReplaceComponents(scheme_replacements)));
  scheme_replacements.SetSchemeStr(url::kAboutScheme);
  EXPECT_FALSE(URLNeedsUserAgentType(
      non_user_agent_urls.ReplaceComponents(scheme_replacements)));

  // about:blank pages.
  EXPECT_FALSE(URLNeedsUserAgentType(GURL("about:blank")));
  // Normal URL.
  EXPECT_TRUE(URLNeedsUserAgentType(GURL("http://www.0.com")));

  // file:// URL.
  EXPECT_FALSE(URLNeedsUserAgentType(GURL("file://foo.pdf")));

  // App specific URL or a placeholder for an app specific URL.
  GURL app_specific(
      url::SchemeHostPort(kTestAppSpecificScheme, "foo", 0).Serialize());
  EXPECT_FALSE(URLNeedsUserAgentType(app_specific));
}

}  // namespace wk_navigation_util
}  // namespace web