text: // Copyright 2021 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/test/scoped_feature_list.h"
  #include "testing/gtest/include/gtest/gtest.h"
  #include "third_party/blink/public/mojom/permissions_policy/policy_disposition.mojom-blink.h"
  #include "third_party/blink/renderer/core/frame/local_dom_window.h"
  #include "third_party/blink/renderer/core/permissions_policy/policy_helper.h"
  #include "third_party/blink/renderer/core/testing/sim/sim_request.h"
  #include "third_party/blink/renderer/core/testing/sim/sim_test.h"
  #include "third_party/blink/renderer/platform/testing/histogram_tester.h"
  #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
  #include "third_party/blink/renderer/platform/testing/url_test_helpers.h"
  
  namespace blink {
  
  class DocumentPolicySimTest : public SimTest {
   public:
    DocumentPolicySimTest()
        : scoped_document_policy_(true),
          scoped_document_policy_negotiation_(true) {
      ResetAvailableDocumentPolicyFeaturesForTest();
    }
  
   private:
    ScopedDocumentPolicyForTest scoped_document_policy_;
    ScopedDocumentPolicyNegotiationForTest scoped_document_policy_negotiation_;
  };
  
  // When runtime feature DocumentPolicy is not enabled, specifying
  // Document-Policy, Require-Document-Policy and policy attribute
  // should have no effect, i.e.
  // document load should not be blocked even if the required policy and incoming
  // policy are incompatible and calling
  // |Document::IsFeatureEnabled(DocumentPolicyFeature...)| should always return
  // true.
  TEST_F(DocumentPolicySimTest, DocumentPolicyNoEffectWhenFlagNotSet) {
    ScopedDocumentPolicyForTest sdp(false);
    ScopedDocumentPolicyNegotiationForTest sdpn(false);
    ResetAvailableDocumentPolicyFeaturesForTest();
  
    SimRequest::Params main_params;
    main_params.response_http_headers = {
        {"Require-Document-Policy", "lossless-images-max-bpp=1.0"}};
  
    SimRequest::Params iframe_params;
    iframe_params.response_http_headers = {
        {"Document-Policy", "lossless-images-max-bpp=1.1"}};
  
    SimRequest main_resource("https://example.com", "text/html", main_params);
    SimRequest iframe_resource("https://example.com/foo.html", "text/html",
                               iframe_params);
  
    LoadURL("https://example.com");
    main_resource.Complete(R"(
      <iframe
        src="https://example.com/foo.html"
        policy="lossless-images-max-bpp=1.0">
      </iframe>
    )");
  
    iframe_resource.Finish();
    auto* child_frame = To<WebLocalFrameImpl>(MainFrame().FirstChild());
    auto* child_window = child_frame->GetFrame()->DomWindow();
    auto& console_messages = static_cast<frame_test_helpers::TestWebFrameClient*>(
                                 child_frame->Client())
                                 ->ConsoleMessages();
  
    // Should not receive a console error message caused by document policy
    // violation blocking document load.
    EXPECT_TRUE(console_messages.IsEmpty());
  
    EXPECT_EQ(child_window->Url(), KURL("https://example.com/foo.html"));
  
    EXPECT_FALSE(child_window->document()->IsUseCounted(
        mojom::WebFeature::kDocumentPolicyCausedPageUnload));
  
    // lossless-images-max-bpp should be set to inf in main document, i.e. allow
    // all values.
    EXPECT_TRUE(Window().IsFeatureEnabled(
        mojom::blink::DocumentPolicyFeature::kLosslessImagesMaxBpp,
        PolicyValue::CreateDecDouble(2.0)));
    EXPECT_TRUE(Window().IsFeatureEnabled(
        mojom::blink::DocumentPolicyFeature::kLosslessImagesMaxBpp,
        PolicyValue::CreateDecDouble(1.0)));
  
    // lossless-images-max-bpp should be set to inf in child document, i.e. allow
    // all values.
    EXPECT_TRUE(child_window->IsFeatureEnabled(
        mojom::blink::DocumentPolicyFeature::kLosslessImagesMaxBpp,
        PolicyValue::CreateDecDouble(2.0)));
    EXPECT_TRUE(child_window->IsFeatureEnabled(
        mojom::blink::DocumentPolicyFeature::kLosslessImagesMaxBpp,
        PolicyValue::CreateDecDouble(1.0)));
  }
  
  // When runtime feature DocumentPolicyNegotiation is not enabled, specifying
  // Require-Document-Policy HTTP header and policy attribute on iframe should
  // have no effect, i.e. document load should not be blocked even if the required
  // policy and incoming policy are incompatible. Document-Policy header should
  // function as normal.
  TEST_F(DocumentPolicySimTest, DocumentPolicyNegotiationNoEffectWhenFlagNotSet) {
    ScopedDocumentPolicyNegotiationForTest sdpn(false);
    ResetAvailableDocumentPolicyFeaturesForTest();
  
    SimRequest::Params main_params;
    main_params.response_http_headers = {
        {"Require-Document-Policy", "lossless-images-max-bpp=1.0"}};
  
    SimRequest::Params iframe_params;
    iframe_params.response_http_headers = {
        {"Document-Policy", "lossless-images-max-bpp=1.1"}};
  
    SimRequest main_resource("https://example.com", "text/html", main_params);
    SimRequest iframe_resource("https://example.com/foo.html", "text/html",
                               iframe_params);
  
    LoadURL("https://example.com");
    main_resource.Complete(R"(
      <iframe
        src="https://example.com/foo.html"
        policy="lossless-images-max-bpp=1.0">
      </iframe>
    )");
  
    iframe_resource.Finish();
    auto* child_frame = To<WebLocalFrameImpl>(MainFrame().FirstChild());
    auto* child_window = child_frame->GetFrame()->DomWindow();
    auto& console_messages = static_cast<frame_test_helpers::TestWebFrameClient*>(
                                 child_frame->Client())
                                 ->ConsoleMessages();
  
    // Should not receive a console error message caused by document policy
    // violation blocking document load.
    EXPECT_TRUE(console_messages.IsEmpty());
  
    EXPECT_EQ(child_window->Url(), KURL("https://example.com/foo.html"));
  
    EXPECT_FALSE(child_window->document()->IsUseCounted(
        mojom::WebFeature::kDocumentPolicyCausedPageUnload));
  
    // lossless-images-max-bpp should be set to inf in main document, i.e. allow
    // all values.
    EXPECT_TRUE(Window().IsFeatureEnabled(
        mojom::blink::DocumentPolicyFeature::kLosslessImagesMaxBpp,
        PolicyValue::CreateDecDouble(2.0)));
    EXPECT_TRUE(Window().IsFeatureEnabled(
        mojom::blink::DocumentPolicyFeature::kLosslessImagesMaxBpp,
        PolicyValue::CreateDecDouble(1.0)));
  
    // lossless-images-max-bpp should be set to 1.1 in child document.
    EXPECT_FALSE(child_window->IsFeatureEnabled(
        mojom::blink::DocumentPolicyFeature::kLosslessImagesMaxBpp,
        PolicyValue::CreateDecDouble(2.0)));
    EXPECT_TRUE(child_window->IsFeatureEnabled(
        mojom::blink::DocumentPolicyFeature::kLosslessImagesMaxBpp,
        PolicyValue::CreateDecDouble(1.0)));
  }
  
  TEST_F(DocumentPolicySimTest, ReportDocumentPolicyHeaderParsingError) {
    SimRequest::Params params;
    params.response_http_headers = {{"Document-Policy", "bad-feature-name"}};
    SimRequest main_resource("https://example.com", "text/html", params);
    LoadURL("https://example.com");
    main_resource.Finish();
  
    EXPECT_EQ(ConsoleMessages().size(), 1u);
    EXPECT_TRUE(
        ConsoleMessages().front().StartsWith("Document-Policy HTTP header:"));
  }
  
  TEST_F(DocumentPolicySimTest, ReportRequireDocumentPolicyHeaderParsingError) {
    SimRequest::Params params;
    params.response_http_headers = {
        {"Require-Document-Policy", "bad-feature-name"}};
    SimRequest main_resource("https://example.com", "text/html", params);
    LoadURL("https://example.com");
    main_resource.Finish();
  
    EXPECT_EQ(ConsoleMessages().size(), 1u);
    EXPECT_TRUE(ConsoleMessages().front().StartsWith(
        "Require-Document-Policy HTTP header:"));
  }
  
  TEST_F(DocumentPolicySimTest, ReportErrorWhenDocumentPolicyIncompatible) {
    SimRequest::Params params;
    params.response_http_headers = {
        {"Document-Policy", "lossless-images-max-bpp=1.1"}};
  
    SimRequest main_resource("https://example.com", "text/html");
    SimRequest iframe_resource("https://example.com/foo.html", "text/html",
                               params);
  
    LoadURL("https://example.com");
    main_resource.Complete(R"(
      <iframe
        src="https://example.com/foo.html"
        policy="lossless-images-max-bpp=1.0">
      </iframe>
    )");
  
    // When blocked by document policy, the document should be filled in with an
    // empty response, with Finish called on |navigation_body_loader| already.
    // If Finish was not called on the loader, because the document was not
    // blocked, this test will fail by crashing here.
    iframe_resource.Finish(true /* body_loader_finished */);
  
    auto* child_frame = To<WebLocalFrameImpl>(MainFrame().FirstChild());
    auto* child_document = child_frame->GetFrame()->GetDocument();
  
    // Should console log a error message.
    auto& console_messages = static_cast<frame_test_helpers::TestWebFrameClient*>(
                                 child_frame->Client())
                                 ->ConsoleMessages();
  
    ASSERT_EQ(console_messages.size(), 1u);
    EXPECT_TRUE(console_messages.front().Contains("document policy"));
  
    // Should replace the document's origin with an opaque origin.
    EXPECT_EQ(child_document->Url(), SecurityOrigin::UrlWithUniqueOpaqueOrigin());
  
    EXPECT_TRUE(child_document->IsUseCounted(
        mojom::WebFeature::kDocumentPolicyCausedPageUnload));
  }
  
  // HTTP header Require-Document-Policy should only take effect on subtree of
  // current document, but not on current document.
  TEST_F(DocumentPolicySimTest,
         RequireDocumentPolicyHeaderShouldNotAffectCurrentDocument) {
    SimRequest::Params params;
    params.response_http_headers = {
        {"Require-Document-Policy", "lossless-images-max-bpp=1.0"},
        {"Document-Policy", "lossless-images-max-bpp=1.1"}};
  
    SimRequest main_resource("https://example.com", "text/html", params);
    LoadURL("https://example.com");
    // If document is blocked by document policy because of incompatible document
    // policy, this test will fail by crashing here.
    main_resource.Finish();
  }
  
  TEST_F(DocumentPolicySimTest, DocumentPolicyHeaderHistogramTest) {
    HistogramTester histogram_tester;
  
    SimRequest::Params params;
    params.response_http_headers = {
        {"Document-Policy",
         "font-display-late-swap, lossless-images-max-bpp=1.1"}};
  
    SimRequest main_resource("https://example.com", "text/html", params);
    LoadURL("https://example.com");
    main_resource.Finish();
  
    histogram_tester.ExpectTotalCount("Blink.UseCounter.DocumentPolicy.Header",
                                      2);
    histogram_tester.ExpectBucketCount("Blink.UseCounter.DocumentPolicy.Header",
                                       1 /* kFontDisplay */, 1);
    histogram_tester.ExpectBucketCount("Blink.UseCounter.DocumentPolicy.Header",
                                       2 /* kUnoptimizedLosslessImages */, 1);
  }
  
  TEST_F(DocumentPolicySimTest, DocumentPolicyPolicyAttributeHistogramTest) {
    HistogramTester histogram_tester;
  
    SimRequest main_resource("https://example.com", "text/html");
    LoadURL("https://example.com");
  
    // Same feature should only be reported once in a document despite its
    // occurrence.
    main_resource.Complete(R"(
      <iframe policy="font-display-late-swap"></iframe>
      <iframe policy="font-display-late-swap=?0"></iframe>
      <iframe
        policy="font-display-late-swap, lossless-images-max-bpp=1.1">
      </iframe>
    )");
  
    histogram_tester.ExpectTotalCount(
        "Blink.UseCounter.DocumentPolicy.PolicyAttribute", 2);
    histogram_tester.ExpectBucketCount(
        "Blink.UseCounter.DocumentPolicy.PolicyAttribute", 1 /* kFontDisplay */,
        1);
    histogram_tester.ExpectBucketCount(
        "Blink.UseCounter.DocumentPolicy.PolicyAttribute",
        2 /* kUnoptimizedLosslessImages */, 1);
  }
  
  TEST_F(DocumentPolicySimTest, DocumentPolicyEnforcedReportHistogramTest) {
    HistogramTester histogram_tester;
  
    SimRequest main_resource("https://example.com", "text/html");
    LoadURL("https://example.com");
    main_resource.Finish();
  
    Window().ReportDocumentPolicyViolation(
        mojom::blink::DocumentPolicyFeature::kFontDisplay,
        mojom::blink::PolicyDisposition::kEnforce,
        "first font display violation");
  
    histogram_tester.ExpectTotalCount("Blink.UseCounter.DocumentPolicy.Enforced",
                                      1);
    histogram_tester.ExpectBucketCount("Blink.UseCounter.DocumentPolicy.Enforced",
                                       1 /* kFontDisplay */, 1);
  
    // Multiple reports should be recorded multiple times.
    Window().ReportDocumentPolicyViolation(
        mojom::blink::DocumentPolicyFeature::kFontDisplay,
        mojom::blink::PolicyDisposition::kEnforce,
        "second font display violation");
  
    histogram_tester.ExpectTotalCount("Blink.UseCounter.DocumentPolicy.Enforced",
                                      2);
    histogram_tester.ExpectBucketCount("Blink.UseCounter.DocumentPolicy.Enforced",
                                       1 /* kFontDisplay */, 2);
  }
  
  TEST_F(DocumentPolicySimTest, DocumentPolicyReportOnlyReportHistogramTest) {
    HistogramTester histogram_tester;
  
    SimRequest::Params params;
    params.response_http_headers = {
        {"Document-Policy-Report-Only", "font-display-late-swap"}};
    SimRequest main_resource("https://example.com", "text/html", params);
  
    LoadURL("https://example.com");
    main_resource.Finish();
  
    Window().ReportDocumentPolicyViolation(
        mojom::blink::DocumentPolicyFeature::kFontDisplay,
        mojom::blink::PolicyDisposition::kReport, "first font display violation");
  
    histogram_tester.ExpectTotalCount(
        "Blink.UseCounter.DocumentPolicy.ReportOnly", 1);
    histogram_tester.ExpectBucketCount(
        "Blink.UseCounter.DocumentPolicy.ReportOnly", 1 /* kFontDisplay */, 1);
  
    // Multiple reports should be recorded multiple times.
    Window().ReportDocumentPolicyViolation(
        mojom::blink::DocumentPolicyFeature::kFontDisplay,
        mojom::blink::PolicyDisposition::kReport,
        "second font display violation");
  
    histogram_tester.ExpectTotalCount(
        "Blink.UseCounter.DocumentPolicy.ReportOnly", 2);
    histogram_tester.ExpectBucketCount(
        "Blink.UseCounter.DocumentPolicy.ReportOnly", 1 /* kFontDisplay */, 2);
  }
  
  class DocumentPolicyHeaderUseCounterTest
      : public DocumentPolicySimTest,
        public testing::WithParamInterface<std::tuple<bool, bool, bool>> {};
  
  TEST_P(DocumentPolicyHeaderUseCounterTest, ShouldObserveUseCounterUpdate) {
    bool has_document_policy_header, has_report_only_header, has_require_header;
    std::tie(has_document_policy_header, has_report_only_header,
             has_require_header) = GetParam();
  
    SimRequest::Params params;
    if (has_document_policy_header) {
      params.response_http_headers.insert("Document-Policy",
                                          "lossless-images-max-bpp=1.0");
    }
    if (has_report_only_header) {
      params.response_http_headers.insert("Document-Policy-Report-Only",
                                          "lossless-images-max-bpp=1.0");
    }
    if (has_require_header) {
      params.response_http_headers.insert("Require-Document-Policy",
                                          "lossless-images-max-bpp=1.0");
    }
    SimRequest main_resource("https://example.com", "text/html", params);
    LoadURL("https://example.com");
    main_resource.Complete();
  
    EXPECT_EQ(
        GetDocument().IsUseCounted(mojom::WebFeature::kDocumentPolicyHeader),
        has_document_policy_header);
    EXPECT_EQ(GetDocument().IsUseCounted(
                  mojom::WebFeature::kDocumentPolicyReportOnlyHeader),
              has_report_only_header);
    EXPECT_EQ(GetDocument().IsUseCounted(
                  mojom::WebFeature::kRequireDocumentPolicyHeader),
              has_require_header);
  }
  
  INSTANTIATE_TEST_SUITE_P(DocumentPolicyHeaderValues,
                           DocumentPolicyHeaderUseCounterTest,
                           ::testing::Combine(::testing::Bool(),
                                              ::testing::Bool(),
                                              ::testing::Bool()));
  
  TEST_F(DocumentPolicySimTest,
         DocumentPolicyIframePolicyAttributeUseCounterTest) {
    SimRequest main_resource("https://example.com", "text/html");
    SimRequest::Params iframe_params;
    iframe_params.response_http_headers = {
        {"Document-Policy", "lossless-images-max-bpp=1.0"}};
    SimRequest iframe_resource("https://example.com/foo.html", "text/html",
                               iframe_params);
    LoadURL("https://example.com");
    main_resource.Complete(R"(
      <iframe
        src="https://example.com/foo.html"
        policy="lossless-images-max-bpp=1.0"
      ></iframe>
    )");
    iframe_resource.Finish();
  
    EXPECT_TRUE(GetDocument().IsUseCounted(
        mojom::WebFeature::kDocumentPolicyIframePolicyAttribute));
    EXPECT_FALSE(
        GetDocument().IsUseCounted(mojom::WebFeature::kRequiredDocumentPolicy));
  
    auto* child_frame = To<WebLocalFrameImpl>(MainFrame().FirstChild());
    auto* child_document = child_frame->GetFrame()->GetDocument();
  
    EXPECT_FALSE(child_document->IsUseCounted(
        mojom::WebFeature::kDocumentPolicyIframePolicyAttribute));
    EXPECT_TRUE(
        child_document->IsUseCounted(mojom::WebFeature::kRequiredDocumentPolicy));
  }
  
  TEST_F(DocumentPolicySimTest, RequiredDocumentPolicyUseCounterTest) {
    SimRequest::Params main_frame_params;
    main_frame_params.response_http_headers = {
        {"Require-Document-Policy", "lossless-images-max-bpp=1.0"}};
    SimRequest main_resource("https://example.com", "text/html",
                             main_frame_params);
  
    SimRequest::Params iframe_params;
    iframe_params.response_http_headers = {
        {"Document-Policy", "lossless-images-max-bpp=1.0"}};
    SimRequest iframe_resource("https://example.com/foo.html", "text/html",
                               iframe_params);
  
    LoadURL("https://example.com");
    main_resource.Complete(R"(
      <iframe src="https://example.com/foo.html"></iframe>
    )");
    iframe_resource.Finish();
  
    EXPECT_FALSE(GetDocument().IsUseCounted(
        mojom::WebFeature::kDocumentPolicyIframePolicyAttribute));
    EXPECT_FALSE(
        GetDocument().IsUseCounted(mojom::WebFeature::kRequiredDocumentPolicy));
  
    auto* child_frame = To<WebLocalFrameImpl>(MainFrame().FirstChild());
    auto* child_document = child_frame->GetFrame()->GetDocument();
  
    EXPECT_FALSE(child_document->IsUseCounted(
        mojom::WebFeature::kDocumentPolicyIframePolicyAttribute));
    EXPECT_TRUE(
        child_document->IsUseCounted(mojom::WebFeature::kRequiredDocumentPolicy));
  }
  
  }  // namespace blink
  