// 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/signin/chrome_signin_url_loader_throttle.h"

#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/test/mock_callback.h"
#include "chrome/browser/signin/chrome_signin_helper.h"
#include "chrome/browser/signin/header_modification_delegate.h"
#include "services/network/public/cpp/http_request_headers_update_params.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using testing::ElementsAre;
using testing::Return;
using testing::_;

namespace signin {

namespace {

class MockDelegate : public HeaderModificationDelegate {
 public:
  MockDelegate() = default;

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

  ~MockDelegate() override = default;

  MOCK_METHOD(bool,
              ShouldInterceptNavigation,
              (content::WebContents * contents),
              (override));
  MOCK_METHOD(void,
              ProcessRequest,
              (ChromeRequestAdapter * request_adapter,
               const GURL& redirect_url),
              (override));
  MOCK_METHOD(void,
              ProcessResponse,
              (ResponseAdapter * response_adapter, const GURL& redirect_url),
              (override));
};

content::WebContents::Getter NullWebContentsGetter() {
  return base::BindRepeating([]() -> content::WebContents* { return nullptr; });
}

}  // namespace

TEST(ChromeSigninURLLoaderThrottleTest, NoIntercept) {
  auto* delegate = new MockDelegate();

  EXPECT_CALL(*delegate, ShouldInterceptNavigation).WillOnce(Return(false));
  EXPECT_FALSE(URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate),
                                              NullWebContentsGetter()));
}

TEST(ChromeSigninURLLoaderThrottleTest, Intercept) {
  auto* delegate = new MockDelegate();
  EXPECT_CALL(*delegate, ShouldInterceptNavigation).WillOnce(Return(true));
  auto throttle = URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate),
                                                 NullWebContentsGetter());
  ASSERT_TRUE(throttle);

  // Phase 1: Start the request.

  const GURL kTestURL("https://google.com/index.html");
  const GURL kTestReferrer("https://chrome.com/referrer.html");
  base::MockCallback<base::OnceClosure> destruction_callback;
  EXPECT_CALL(*delegate, ProcessRequest)
      .WillOnce([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
        EXPECT_EQ(kTestURL, adapter->GetUrl());
        EXPECT_EQ(network::mojom::RequestDestination::kDocument,
                  adapter->GetRequestDestination());
        EXPECT_TRUE(adapter->IsOutermostMainFrame());
        EXPECT_EQ(kTestReferrer, adapter->GetReferrer());

        EXPECT_TRUE(adapter->HasHeader("X-Request-1"));
        adapter->RemoveRequestHeaderByName("X-Request-1");
        EXPECT_FALSE(adapter->HasHeader("X-Request-1"));

        adapter->SetExtraHeaderByName("X-Request-2", "Bar");
        EXPECT_TRUE(adapter->HasHeader("X-Request-2"));

        EXPECT_EQ(GURL(), redirect_url);

        adapter->SetDestructionCallback(destruction_callback.Get());
      });

  network::ResourceRequest request;
  request.url = kTestURL;
  request.referrer = kTestReferrer;
  request.destination = network::mojom::RequestDestination::kDocument;
  request.is_outermost_main_frame = true;
  request.headers.SetHeader("X-Request-1", "Foo");
  bool defer = false;
  throttle->WillStartRequest(&request, &defer);

  EXPECT_FALSE(request.headers.HasHeader("X-Request-1"));
  EXPECT_THAT(request.headers.GetHeader("X-Request-2"),
              testing::Optional(std::string("Bar")));

  EXPECT_FALSE(defer);

  testing::Mock::VerifyAndClearExpectations(delegate);

  // Phase 2: Redirect the request.

  const GURL kTestRedirectURL("https://youtube.com/index.html");
  const void* const kResponseUserDataKey = &kResponseUserDataKey;
  std::unique_ptr<base::SupportsUserData::Data> response_user_data =
      std::make_unique<base::SupportsUserData::Data>();
  base::SupportsUserData::Data* response_user_data_ptr =
      response_user_data.get();

  EXPECT_CALL(*delegate, ProcessResponse)
      .WillOnce([&](ResponseAdapter* adapter, const GURL& redirect_url) {
        EXPECT_EQ(kTestURL, adapter->GetUrl());
        EXPECT_TRUE(adapter->IsOutermostMainFrame());

        adapter->SetUserData(kResponseUserDataKey,
                             std::move(response_user_data));
        EXPECT_EQ(response_user_data_ptr,
                  adapter->GetUserData(kResponseUserDataKey));

        const net::HttpResponseHeaders* headers = adapter->GetHeaders();
        EXPECT_TRUE(headers->HasHeader("X-Response-1"));
        EXPECT_TRUE(headers->HasHeader("X-Response-2"));
        adapter->RemoveHeader("X-Response-2");

        EXPECT_EQ(kTestRedirectURL, redirect_url);
      });

  base::MockCallback<base::OnceClosure> ignored_destruction_callback;
  EXPECT_CALL(*delegate, ProcessRequest)
      .WillOnce([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
        EXPECT_EQ(network::mojom::RequestDestination::kDocument,
                  adapter->GetRequestDestination());
        EXPECT_TRUE(adapter->IsOutermostMainFrame());

        // Changes to the URL and referrer take effect after the redirect
        // is followed.
        EXPECT_EQ(kTestURL, adapter->GetUrl());
        EXPECT_EQ(kTestReferrer, adapter->GetReferrer());

        // X-Request-1 and X-Request-2 were modified in the previous call to
        // ProcessRequest(). These changes should still be present.
        EXPECT_FALSE(adapter->HasHeader("X-Request-1"));
        EXPECT_TRUE(adapter->HasHeader("X-Request-2"));

        adapter->RemoveRequestHeaderByName("X-Request-2");
        EXPECT_FALSE(adapter->HasHeader("X-Request-2"));

        adapter->SetExtraHeaderByName("X-Request-3", "Baz");
        EXPECT_TRUE(adapter->HasHeader("X-Request-3"));

        EXPECT_EQ(kTestRedirectURL, redirect_url);

        adapter->SetDestructionCallback(ignored_destruction_callback.Get());
      });

  net::RedirectInfo redirect_info;
  redirect_info.new_url = kTestRedirectURL;
  // An HTTPS to HTTPS redirect such as this wouldn't normally change the
  // referrer but we do for testing purposes.
  redirect_info.new_referrer = kTestURL.spec();

  auto response_head = network::mojom::URLResponseHead::New();
  response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
  response_head->headers->SetHeader("X-Response-1", "Foo");
  response_head->headers->SetHeader("X-Response-2", "Bar");

  network::HttpRequestHeadersUpdateParams headers_update_params;
  throttle->WillRedirectRequest(&redirect_info, *response_head, &defer,
                                &headers_update_params);

  EXPECT_FALSE(defer);

  EXPECT_TRUE(response_head->headers->HasHeader("X-Response-1"));
  EXPECT_FALSE(response_head->headers->HasHeader("X-Response-2"));

  EXPECT_THAT(headers_update_params.removed_headers,
              ElementsAre("X-Request-2"));
  EXPECT_THAT(headers_update_params.modified_headers.GetHeader("X-Request-3"),
              testing::Optional(std::string("Baz")));

  EXPECT_TRUE(headers_update_params.modified_cors_exempt_headers.IsEmpty());

  testing::Mock::VerifyAndClearExpectations(delegate);

  // Phase 3: Complete the request.

  EXPECT_CALL(*delegate, ProcessResponse)
      .WillOnce([&](ResponseAdapter* adapter, const GURL& redirect_url) {
        EXPECT_EQ(kTestRedirectURL, adapter->GetUrl());
        EXPECT_TRUE(adapter->IsOutermostMainFrame());

        EXPECT_EQ(response_user_data_ptr,
                  adapter->GetUserData(kResponseUserDataKey));

        const net::HttpResponseHeaders* headers = adapter->GetHeaders();
        // This is a new response and so previous headers should not carry over.
        EXPECT_FALSE(headers->HasHeader("X-Response-1"));
        EXPECT_FALSE(headers->HasHeader("X-Response-2"));

        EXPECT_TRUE(headers->HasHeader("X-Response-3"));
        EXPECT_TRUE(headers->HasHeader("X-Response-4"));
        adapter->RemoveHeader("X-Response-3");

        EXPECT_EQ(GURL(), redirect_url);
      });

  response_head = network::mojom::URLResponseHead::New();
  response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
  response_head->headers->SetHeader("X-Response-3", "Foo");
  response_head->headers->SetHeader("X-Response-4", "Bar");

  throttle->WillProcessResponse(kTestRedirectURL, response_head.get(), &defer);

  EXPECT_FALSE(response_head->headers->HasHeader("X-Response-3"));
  EXPECT_TRUE(response_head->headers->HasHeader("X-Response-4"));

  EXPECT_FALSE(defer);

  EXPECT_CALL(destruction_callback, Run()).Times(1);
  EXPECT_CALL(ignored_destruction_callback, Run()).Times(0);
  throttle.reset();
}

TEST(ChromeSigninURLLoaderThrottleTest, InterceptSubFrame) {
  auto* delegate = new MockDelegate();
  EXPECT_CALL(*delegate, ShouldInterceptNavigation).WillOnce(Return(true));
  auto throttle = URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate),
                                                 NullWebContentsGetter());
  ASSERT_TRUE(throttle);

  EXPECT_CALL(*delegate, ProcessRequest)
      .Times(2)
      .WillRepeatedly(
          [](ChromeRequestAdapter* adapter, const GURL& redirect_url) {
            EXPECT_EQ(network::mojom::RequestDestination::kIframe,
                      adapter->GetRequestDestination());
            EXPECT_FALSE(adapter->IsOutermostMainFrame());
          });

  network::ResourceRequest request;
  request.url = GURL("https://google.com");
  request.destination = network::mojom::RequestDestination::kIframe;
  request.is_outermost_main_frame = false;

  bool defer = false;
  throttle->WillStartRequest(&request, &defer);
  EXPECT_FALSE(defer);

  EXPECT_CALL(*delegate, ProcessResponse)
      .Times(2)
      .WillRepeatedly([](ResponseAdapter* adapter, const GURL& redirect_url) {
        EXPECT_FALSE(adapter->IsOutermostMainFrame());
      });

  net::RedirectInfo redirect_info;
  redirect_info.new_url = GURL("https://youtube.com");
  auto response_head = network::mojom::URLResponseHead::New();
  network::HttpRequestHeadersUpdateParams headers_update_params;
  throttle->WillRedirectRequest(&redirect_info, *response_head, &defer,
                                &headers_update_params);
  EXPECT_FALSE(defer);

  EXPECT_TRUE(headers_update_params.removed_headers.empty());
  EXPECT_TRUE(headers_update_params.modified_headers.IsEmpty());
  EXPECT_TRUE(headers_update_params.modified_cors_exempt_headers.IsEmpty());

  throttle->WillProcessResponse(GURL("https://youtube.com"),
                                response_head.get(), &defer);
  EXPECT_FALSE(defer);
}

// Regression test for https://crbug.com/441955793.
TEST(ChromeSigninURLLoaderThrottleTest, InterceptNullHeaders) {
  auto* delegate = new MockDelegate();
  EXPECT_CALL(*delegate, ShouldInterceptNavigation).WillOnce(Return(true));
  auto throttle = URLLoaderThrottle::MaybeCreate(base::WrapUnique(delegate),
                                                 NullWebContentsGetter());
  ASSERT_TRUE(throttle);

  // Phase 1: Start a HTTPS request.

  const GURL kTestURL("https://google.com/index.html");
  const GURL kTestReferrer("https://chrome.com/referrer.html");
  EXPECT_CALL(*delegate, ProcessRequest);

  network::ResourceRequest request;
  request.url = kTestURL;
  request.referrer = kTestReferrer;
  bool defer = false;
  throttle->WillStartRequest(&request, &defer);
  EXPECT_FALSE(defer);
  testing::Mock::VerifyAndClearExpectations(delegate);

  // Phase 2: Redirect the request to a header-less URL.

  const GURL kTestRedirectURL("chrome-extension://abcd/index.html");
  EXPECT_CALL(*delegate, ProcessResponse)
      .WillOnce([&](ResponseAdapter* adapter, const GURL& redirect_url) {
        EXPECT_EQ(kTestURL, adapter->GetUrl());
        EXPECT_FALSE(adapter->GetHeaders());
        // This should not crash.
        adapter->RemoveHeader("TestHeader");
        EXPECT_EQ(kTestRedirectURL, redirect_url);
      });
  EXPECT_CALL(*delegate, ProcessRequest);

  net::RedirectInfo redirect_info;
  redirect_info.new_url = kTestRedirectURL;
  redirect_info.new_referrer = kTestURL.spec();

  // Response head with null headers.
  auto response_head = network::mojom::URLResponseHead::New();
  network::HttpRequestHeadersUpdateParams headers_update_params;
  throttle->WillRedirectRequest(&redirect_info, *response_head, &defer,
                                &headers_update_params);
  EXPECT_FALSE(defer);

  EXPECT_FALSE(response_head->headers);
  testing::Mock::VerifyAndClearExpectations(delegate);

  // Phase 3: Complete the request.

  EXPECT_CALL(*delegate, ProcessResponse)
      .WillOnce([&](ResponseAdapter* adapter, const GURL& redirect_url) {
        EXPECT_EQ(kTestRedirectURL, adapter->GetUrl());
        EXPECT_FALSE(adapter->GetHeaders());
        // This should not crash.
        adapter->RemoveHeader("TestHeader");
        EXPECT_EQ(GURL(), redirect_url);
      });

  // Response head with null headers.
  response_head = network::mojom::URLResponseHead::New();
  throttle->WillProcessResponse(kTestRedirectURL, response_head.get(), &defer);

  EXPECT_FALSE(response_head->headers);
  EXPECT_FALSE(defer);
}

}  // namespace signin
