chromium/components/ukm/ios/ukm_url_recorder_unittest.mm

// 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 "components/ukm/ios/ukm_url_recorder.h"

#include <optional>

#include "base/functional/bind.h"
#import "base/test/ios/wait_util.h"
#include "components/ukm/test_ukm_recorder.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/test/fakes/fake_navigation_context.h"
#import "ios/web/public/test/web_test_with_web_state.h"
#import "ios/web/public/web_state.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "services/metrics/public/cpp/ukm_source.h"
#include "url/gurl.h"

namespace ukm {
namespace {

std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
    const net::test_server::HttpRequest& request) {
  if (request.GetURL().path() == "/title1.html") {
    auto result = std::make_unique<net::test_server::BasicHttpResponse>();
    result->set_content_type("text/html");
    result->set_content("<html><head></head><body>NoTitle</body></html>");
    return std::move(result);
  }
  if (request.GetURL().path() == "/page_with_iframe.html") {
    auto result = std::make_unique<net::test_server::BasicHttpResponse>();
    result->set_content_type("text/html");
    result->set_content(
        "<html><head></head><body><iframe src=\"title1.html\"></body></html>");
    return std::move(result);
  }
  if (request.GetURL().path() == "/download") {
    auto result = std::make_unique<net::test_server::BasicHttpResponse>();
    result->set_content_type("application/vnd.test");
    result->set_content("TestDownloadContent");
    return std::move(result);
  }
  if (request.GetURL().path() == "/redirect") {
    auto result = std::make_unique<net::test_server::BasicHttpResponse>();
    result->set_code(net::HTTP_MOVED_PERMANENTLY);
    result->AddCustomHeader("Location", "/title1.html");
    return std::move(result);
  }
  return nullptr;
}

}  // namespace

class UkmUrlRecorderTest : public web::WebTestWithWebState {
 protected:
  UkmUrlRecorderTest() {
    server_.RegisterDefaultHandler(base::BindRepeating(&HandleRequest));
  }

  void SetUp() override {
    web::WebTestWithWebState::SetUp();
    ASSERT_TRUE(server_.Start());
    ukm::InitializeSourceUrlRecorderForWebState(web_state());
  }

  bool LoadUrlAndWait(const GURL& url) {
    web::NavigationManager::WebLoadParams params(url);
    web_state()->GetNavigationManager()->LoadURLWithParams(params);
    return base::test::ios::WaitUntilConditionOrTimeout(
        base::test::ios::kWaitForPageLoadTimeout, ^{
          return !web_state()->IsLoading();
        });
  }

  void MaybeRecordUrl(web::NavigationContext* context, const GURL& url) {
    internal::SourceUrlRecorderWebStateObserver* observer =
        GetSourceUrlRecorderForWebStateForWebState(web_state());
    observer->MaybeRecordUrl(context, url);
  }

  testing::AssertionResult RecordedUrl(
      ukm::SourceId source_id,
      GURL expected_url,
      std::optional<GURL> expected_initial_url) {
    auto* source = test_ukm_recorder_.GetSourceForSourceId(source_id);
    if (!source)
      return testing::AssertionFailure() << "No URL recorded";
    if (source->url() != expected_url)
      return testing::AssertionFailure()
             << "Url was " << source->url() << ", expected: " << expected_url;
    std::optional<GURL> initial_url;
    if (source->urls().size() > 1u)
      initial_url = source->urls().front();
    if (expected_initial_url != initial_url) {
      return testing::AssertionFailure()
             << "Initial Url was " << initial_url.value_or(GURL())
             << ", expected: " << expected_initial_url.value_or(GURL());
    }
    return testing::AssertionSuccess();
  }

  testing::AssertionResult DidNotRecordUrl(GURL url) {
    const auto& sources = test_ukm_recorder_.GetSources();
    for (const auto& kv : sources) {
      if (kv.second->url() == url)
        return testing::AssertionFailure()
               << "Url " << url << " was recorded with SourceId: " << kv.first;
      if (kv.second->url() == url)
        return testing::AssertionFailure()
               << "Url " << url
               << " was recorded as an initial URL with SourceId: " << kv.first;
    }
    return testing::AssertionSuccess();
  }

 protected:
  net::EmbeddedTestServer server_;
  ukm::TestAutoSetUkmRecorder test_ukm_recorder_;
};

// Tests that URLs get recorded for pages visited.
TEST_F(UkmUrlRecorderTest, Basic) {
  GURL url = server_.GetURL("/title1.html");
  EXPECT_TRUE(LoadUrlAndWait(url));
  ukm::SourceId source_id = ukm::GetSourceIdForWebStateDocument(web_state());
  EXPECT_TRUE(RecordedUrl(source_id, url, std::nullopt));
}

// Tests that subframe URLs do not get recorded.
TEST_F(UkmUrlRecorderTest, IgnoreUrlInSubframe) {
  GURL main_url = server_.GetURL("/page_with_iframe.html");
  GURL subframe_url = server_.GetURL("/title1.html");
  EXPECT_TRUE(LoadUrlAndWait(main_url));
  ukm::SourceId source_id = ukm::GetSourceIdForWebStateDocument(web_state());
  EXPECT_TRUE(RecordedUrl(source_id, main_url, std::nullopt));
  EXPECT_TRUE(DidNotRecordUrl(subframe_url));
}

// Tests that download URLs do not get recorded.
TEST_F(UkmUrlRecorderTest, IgnoreDownloadUrl) {
  GURL url = server_.GetURL("/download");
  EXPECT_TRUE(LoadUrlAndWait(url));
  EXPECT_TRUE(DidNotRecordUrl(url));
}

// Tests that redirected URLs record initial and final URL.
TEST_F(UkmUrlRecorderTest, InitialUrl) {
  GURL redirect_url = server_.GetURL("/redirect");
  GURL target_url = server_.GetURL("/title1.html");
  EXPECT_TRUE(LoadUrlAndWait(redirect_url));
  ukm::SourceId source_id = ukm::GetSourceIdForWebStateDocument(web_state());
  EXPECT_TRUE(RecordedUrl(source_id, target_url, redirect_url));
}

// Checks that if a NavigationContext is erroneously reused, its reuse is
// handled gracefully by the UKM recorder. See crbug.com/346017703.
TEST_F(UkmUrlRecorderTest, ReusedNavigationContext) {
  EXPECT_EQ(0u, test_ukm_recorder_.sources_count());

  web::FakeNavigationContext context_1;
  const int64_t navigation_id_1 = context_1.GetNavigationId();
  const GURL url_1("https://example.com#foo");
  context_1.SetIsSameDocument(false);
  context_1.SetUrl(url_1);

  MaybeRecordUrl(&context_1, url_1);
  EXPECT_EQ(1u, test_ukm_recorder_.sources_count());

  web::FakeNavigationContext context_2;
  const int64_t navigation_id_2 = context_2.GetNavigationId();
  const GURL url_2("https://example.com#bar");
  context_2.SetIsSameDocument(false);
  context_2.SetUrl(url_2);

  EXPECT_NE(navigation_id_1, navigation_id_2);

  MaybeRecordUrl(&context_2, url_2);
  EXPECT_EQ(2u, test_ukm_recorder_.sources_count());

  // If a NavigationContext is reused, UKM recorder should not crash on
  // receiving the same navigation id, hence also the UKM source id, again.
  // Instead no new source should be added to the UKM recorder.
  MaybeRecordUrl(&context_1, url_1);
  EXPECT_EQ(2u, test_ukm_recorder_.sources_count());
}

}  // namespace ukm