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

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/memory/ref_counted.h"
#include "content/browser/frame_host/ancestor_throttle.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/test_renderer_host.h"
#include "net/http/http_response_headers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace content {

namespace {

using HeaderDisposition = AncestorThrottle::HeaderDisposition;

net::HttpResponseHeaders* GetAncestorHeaders(const char* xfo, const char* csp) {
  std::string header_string("HTTP/1.1 200 OK\nX-Frame-Options: ");
  header_string += xfo;
  if (csp != nullptr) {
    header_string += "\nContent-Security-Policy: ";
    header_string += csp;
  }
  header_string += "\n\n";
  std::replace(header_string.begin(), header_string.end(), '\n', '\0');
  net::HttpResponseHeaders* headers =
      new net::HttpResponseHeaders(header_string);
  EXPECT_TRUE(headers->HasHeader("X-Frame-Options"));
  if (csp != nullptr)
    EXPECT_TRUE(headers->HasHeader("Content-Security-Policy"));
  return headers;
}

}  // namespace

// AncestorThrottleTest
// -------------------------------------------------------------

class AncestorThrottleTest : public testing::Test {};

TEST_F(AncestorThrottleTest, ParsingXFrameOptions) {
  struct TestCase {
    const char* header;
    AncestorThrottle::HeaderDisposition expected;
    const char* value;
  } cases[] = {
      // Basic keywords
      {"DENY", HeaderDisposition::DENY, "DENY"},
      {"SAMEORIGIN", HeaderDisposition::SAMEORIGIN, "SAMEORIGIN"},
      {"ALLOWALL", HeaderDisposition::ALLOWALL, "ALLOWALL"},

      // Repeated keywords
      {"DENY,DENY", HeaderDisposition::DENY, "DENY, DENY"},
      {"SAMEORIGIN,SAMEORIGIN", HeaderDisposition::SAMEORIGIN,
       "SAMEORIGIN, SAMEORIGIN"},
      {"ALLOWALL,ALLOWALL", HeaderDisposition::ALLOWALL, "ALLOWALL, ALLOWALL"},

      // Case-insensitive
      {"deNy", HeaderDisposition::DENY, "deNy"},
      {"sAmEorIgIn", HeaderDisposition::SAMEORIGIN, "sAmEorIgIn"},
      {"AlLOWaLL", HeaderDisposition::ALLOWALL, "AlLOWaLL"},

      // Trim whitespace
      {" DENY", HeaderDisposition::DENY, "DENY"},
      {"SAMEORIGIN ", HeaderDisposition::SAMEORIGIN, "SAMEORIGIN"},
      {" ALLOWALL ", HeaderDisposition::ALLOWALL, "ALLOWALL"},
      {"   DENY", HeaderDisposition::DENY, "DENY"},
      {"SAMEORIGIN   ", HeaderDisposition::SAMEORIGIN, "SAMEORIGIN"},
      {"   ALLOWALL   ", HeaderDisposition::ALLOWALL, "ALLOWALL"},
      {" DENY , DENY ", HeaderDisposition::DENY, "DENY, DENY"},
      {"SAMEORIGIN,  SAMEORIGIN", HeaderDisposition::SAMEORIGIN,
       "SAMEORIGIN, SAMEORIGIN"},
      {"ALLOWALL  ,ALLOWALL", HeaderDisposition::ALLOWALL,
       "ALLOWALL, ALLOWALL"},
  };

  AncestorThrottle throttle(nullptr);
  for (const auto& test : cases) {
    SCOPED_TRACE(test.header);
    scoped_refptr<net::HttpResponseHeaders> headers =
        GetAncestorHeaders(test.header, nullptr);
    std::string header_value;
    EXPECT_EQ(test.expected,
              throttle.ParseHeader(headers.get(), &header_value));
    EXPECT_EQ(test.value, header_value);
  }
}

TEST_F(AncestorThrottleTest, ErrorsParsingXFrameOptions) {
  struct TestCase {
    const char* header;
    AncestorThrottle::HeaderDisposition expected;
    const char* failure;
  } cases[] = {
      // Empty == Invalid.
      {"", HeaderDisposition::INVALID, ""},

      // Invalid
      {"INVALID", HeaderDisposition::INVALID, "INVALID"},
      {"INVALID DENY", HeaderDisposition::INVALID, "INVALID DENY"},
      {"DENY DENY", HeaderDisposition::INVALID, "DENY DENY"},
      {"DE NY", HeaderDisposition::INVALID, "DE NY"},

      // Conflicts
      {"INVALID,DENY", HeaderDisposition::CONFLICT, "INVALID, DENY"},
      {"DENY,ALLOWALL", HeaderDisposition::CONFLICT, "DENY, ALLOWALL"},
      {"SAMEORIGIN,DENY", HeaderDisposition::CONFLICT, "SAMEORIGIN, DENY"},
      {"ALLOWALL,SAMEORIGIN", HeaderDisposition::CONFLICT,
       "ALLOWALL, SAMEORIGIN"},
      {"DENY,  SAMEORIGIN", HeaderDisposition::CONFLICT, "DENY, SAMEORIGIN"}};

  AncestorThrottle throttle(nullptr);
  for (const auto& test : cases) {
    SCOPED_TRACE(test.header);
    scoped_refptr<net::HttpResponseHeaders> headers =
        GetAncestorHeaders(test.header, nullptr);
    std::string header_value;
    EXPECT_EQ(test.expected,
              throttle.ParseHeader(headers.get(), &header_value));
    EXPECT_EQ(test.failure, header_value);
  }
}

TEST_F(AncestorThrottleTest, IgnoreWhenFrameAncestorsPresent) {
  struct TestCase {
    const char* csp;
    AncestorThrottle::HeaderDisposition expected;
  } cases[] = {
      {"", HeaderDisposition::DENY},
      {"frame-ancestors 'none'", HeaderDisposition::BYPASS},
      {"frame-ancestors *", HeaderDisposition::BYPASS},
      {"frame-ancestors 'self'", HeaderDisposition::BYPASS},
      {"frame-ancestors https://example.com", HeaderDisposition::BYPASS},
      {"fRaMe-AnCeStOrS *", HeaderDisposition::BYPASS},
      {"directive1; frame-ancestors 'none'", HeaderDisposition::BYPASS},
      {"directive1; frame-ancestors *", HeaderDisposition::BYPASS},
      {"directive1; frame-ancestors 'self'", HeaderDisposition::BYPASS},
      {"directive1; frame-ancestors https://example.com",
       HeaderDisposition::BYPASS},
      {"directive1; fRaMe-AnCeStOrS *", HeaderDisposition::BYPASS},
      {"policy, frame-ancestors 'none'", HeaderDisposition::BYPASS},
      {"policy, frame-ancestors *", HeaderDisposition::BYPASS},
      {"policy, frame-ancestors 'self'", HeaderDisposition::BYPASS},
      {"policy, frame-ancestors https://example.com",
       HeaderDisposition::BYPASS},
      {"policy, frame-ancestors 'none'", HeaderDisposition::BYPASS},
      {"policy, directive1; frame-ancestors *", HeaderDisposition::BYPASS},
      {"policy, directive1; frame-ancestors 'self'", HeaderDisposition::BYPASS},
      {"policy, directive1; frame-ancestors https://example.com",
       HeaderDisposition::BYPASS},
      {"policy, directive1; fRaMe-AnCeStOrS *", HeaderDisposition::BYPASS},
      {"policy, directive1; fRaMe-AnCeStOrS *", HeaderDisposition::BYPASS},

      {"not-frame-ancestors *", HeaderDisposition::DENY},
      {"frame-ancestors-are-lovely", HeaderDisposition::DENY},
      {"directive1; not-frame-ancestors *", HeaderDisposition::DENY},
      {"directive1; frame-ancestors-are-lovely", HeaderDisposition::DENY},
      {"policy, not-frame-ancestors *", HeaderDisposition::DENY},
      {"policy, frame-ancestors-are-lovely", HeaderDisposition::DENY},
      {"policy, directive1; not-frame-ancestors *", HeaderDisposition::DENY},
      {"policy, directive1; frame-ancestors-are-lovely",
       HeaderDisposition::DENY},
  };

  AncestorThrottle throttle(nullptr);
  for (const auto& test : cases) {
    SCOPED_TRACE(test.csp);
    scoped_refptr<net::HttpResponseHeaders> headers =
        GetAncestorHeaders("DENY", test.csp);
    std::string header_value;
    EXPECT_EQ(test.expected,
              throttle.ParseHeader(headers.get(), &header_value));
    EXPECT_EQ("DENY", header_value);
  }
}

}  // namespace content
