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

#include "net/base/network_isolation_key.h"

#include <array>
#include <optional>

#include "base/test/scoped_feature_list.h"
#include "base/unguessable_token.h"
#include "base/values.h"
#include "net/base/features.h"
#include "net/base/network_isolation_partition.h"
#include "net/base/schemeful_site.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/hash/hash_testing.h"
#include "url/gurl.h"
#include "url/url_util.h"

namespace net {

namespace {
const char kDataUrl[] = "data:text/html,<body>Hello World</body>";

TEST(NetworkIsolationKeyTest, EmptyKey) {
  NetworkIsolationKey key;
  EXPECT_TRUE(key.IsEmpty());
  EXPECT_EQ(std::nullopt, key.ToCacheKeyString());
  EXPECT_TRUE(key.IsTransient());
  EXPECT_EQ("null null", key.ToDebugString());
}

TEST(NetworkIsolationKeyTest, NonEmptySameSiteKey) {
  SchemefulSite site1 = SchemefulSite(GURL("http://a.test/"));
  NetworkIsolationKey key(site1, site1);
  EXPECT_FALSE(key.IsEmpty());
  EXPECT_EQ(site1.Serialize() + " " + site1.Serialize(),
            key.ToCacheKeyString());
  EXPECT_EQ(site1.GetDebugString() + " " + site1.GetDebugString(),
            key.ToDebugString());
  EXPECT_FALSE(key.IsTransient());
}

TEST(NetworkIsolationKeyTest, NonEmptyCrossSiteKey) {
  SchemefulSite site1 = SchemefulSite(GURL("http://a.test/"));
  SchemefulSite site2 = SchemefulSite(GURL("http://b.test/"));
  NetworkIsolationKey key(site1, site2);
  EXPECT_FALSE(key.IsEmpty());
  EXPECT_EQ(site1.Serialize() + " " + site2.Serialize(),
            key.ToCacheKeyString());
  EXPECT_EQ(site1.GetDebugString() + " " + site2.GetDebugString(),
            key.ToDebugString());
  EXPECT_FALSE(key.IsTransient());
}

TEST(NetworkIsolationKeyTest, KeyWithNonce) {
  SchemefulSite site1 = SchemefulSite(GURL("http://a.test/"));
  SchemefulSite site2 = SchemefulSite(GURL("http://b.test/"));
  base::UnguessableToken nonce = base::UnguessableToken::Create();
  NetworkIsolationKey key(site1, site2, nonce);
  EXPECT_FALSE(key.IsEmpty());
  EXPECT_EQ(std::nullopt, key.ToCacheKeyString());
  EXPECT_TRUE(key.IsTransient());
  EXPECT_EQ(site1.GetDebugString() + " " + site2.GetDebugString() +
                " (with nonce " + nonce.ToString() + ")",
            key.ToDebugString());

  // Create another NetworkIsolationKey with the same input parameters, and
  // check that it is equal.
  NetworkIsolationKey same_key(site1, site2, nonce);
  EXPECT_EQ(key, same_key);

  // Create another NetworkIsolationKey with a different nonce and check that
  // it's different.
  base::UnguessableToken nonce2 = base::UnguessableToken::Create();
  NetworkIsolationKey key2(site1, site2, nonce2);
  EXPECT_NE(key, key2);
  EXPECT_NE(key.ToDebugString(), key2.ToDebugString());
}

TEST(NetworkIsolationKeyTest, KeyWithNonGeneralNetworkPartition) {
  SchemefulSite site1 = SchemefulSite(GURL("http://a.test/"));
  SchemefulSite site2 = SchemefulSite(GURL("http://b.test/"));
  NetworkIsolationKey key(
      site1, site2, /*nonce=*/std::nullopt,
      NetworkIsolationPartition::kProtectedAudienceSellerWorklet);
  EXPECT_FALSE(key.IsEmpty());
  EXPECT_EQ(NetworkIsolationPartition::kProtectedAudienceSellerWorklet,
            key.GetNetworkIsolationPartition());
  EXPECT_EQ(site1.Serialize() + " " + site2.Serialize() + " 1",
            key.ToCacheKeyString());
  EXPECT_FALSE(key.IsTransient());
  EXPECT_EQ(site1.GetDebugString() + " " + site2.GetDebugString() +
                " (protected audience seller worklet partition)",
            key.ToDebugString());
  EXPECT_EQ(site1.Serialize() + " " + site2.Serialize() + " 1",
            key.ToCacheKeyString());

  // Create another NetworkIsolationKey with the same input parameters, and
  // check that it is equal.
  NetworkIsolationKey same_key(
      site1, site2, /*nonce=*/std::nullopt,
      NetworkIsolationPartition::kProtectedAudienceSellerWorklet);
  EXPECT_EQ(NetworkIsolationPartition::kProtectedAudienceSellerWorklet,
            same_key.GetNetworkIsolationPartition());
  EXPECT_EQ(key, same_key);
  EXPECT_EQ(key.ToCacheKeyString(), same_key.ToCacheKeyString());
  EXPECT_EQ(key.ToDebugString(), same_key.ToDebugString());

  // Create another NetworkIsolationKey with a different
  // NetworkIsolationPartition and check that it's different.
  NetworkIsolationKey key2(site1, site2, /*nonce=*/std::nullopt,
                           NetworkIsolationPartition::kGeneral);
  EXPECT_EQ(NetworkIsolationPartition::kGeneral,
            key2.GetNetworkIsolationPartition());
  EXPECT_NE(key, key2);
  EXPECT_NE(key.ToCacheKeyString(), key2.ToCacheKeyString());
  EXPECT_NE(key.ToDebugString(), key2.ToDebugString());

  // Make sure if a nonce is included in addition to a
  // non-general NetworkPartition, the NIK is transient.
  auto nonce = base::UnguessableToken::Create();
  NetworkIsolationKey key3(
      site1, site2, /*nonce=*/nonce,
      NetworkIsolationPartition::kProtectedAudienceSellerWorklet);
  EXPECT_TRUE(key3.IsTransient());

  // Make sure if there's an opaque origin, the NIK is still transient.
  NetworkIsolationKey key4(
      site1, SchemefulSite(GURL(kDataUrl)), /*nonce=*/std::nullopt,
      NetworkIsolationPartition::kProtectedAudienceSellerWorklet);
  EXPECT_TRUE(key4.IsTransient());
}

TEST(NetworkIsolationKeyTest, CreateEmptyWithPartition) {
  NetworkIsolationKey key = NetworkIsolationKey::CreateEmptyWithPartition(
      NetworkIsolationPartition::kDnsOverHttps);
  EXPECT_TRUE(key.IsEmpty());
  EXPECT_TRUE(key.IsTransient());
  EXPECT_EQ(NetworkIsolationPartition::kDnsOverHttps,
            key.GetNetworkIsolationPartition());
  EXPECT_EQ(std::nullopt, key.ToCacheKeyString());
  EXPECT_EQ("null null (dns over https)", key.ToDebugString());

  // Create another NetworkIsolationKey with the same partition, and check that
  // they're equal.
  NetworkIsolationKey same_key = NetworkIsolationKey::CreateEmptyWithPartition(
      NetworkIsolationPartition::kDnsOverHttps);
  EXPECT_EQ(key, same_key);

  // Create another NetworkIsolationKey with a different partition, and check
  // that they're different.
  NetworkIsolationKey other_key = NetworkIsolationKey::CreateEmptyWithPartition(
      NetworkIsolationPartition::kFedCmUncredentialedRequests);
  EXPECT_NE(key, other_key);

  // Check that it's also different from the general case empty
  // NetworkIsolationKey.
  NetworkIsolationKey empty_key;
  EXPECT_NE(key, empty_key);
}

TEST(NetworkIsolationKeyTest, OpaqueOriginKey) {
  SchemefulSite site_data = SchemefulSite(GURL(kDataUrl));
  NetworkIsolationKey key(site_data, site_data);
  EXPECT_FALSE(key.IsEmpty());
  EXPECT_EQ(std::nullopt, key.ToCacheKeyString());
  EXPECT_TRUE(key.IsTransient());
  EXPECT_EQ(site_data.GetDebugString() + " " + site_data.GetDebugString(),
            key.ToDebugString());

  // Create another site with an opaque origin, and make sure it's different and
  // has a different debug string.
  SchemefulSite other_site = SchemefulSite(GURL(kDataUrl));
  NetworkIsolationKey other_key(other_site, other_site);
  EXPECT_NE(key, other_key);
  EXPECT_NE(key.ToDebugString(), other_key.ToDebugString());
  EXPECT_EQ(other_site.GetDebugString() + " " + other_site.GetDebugString(),
            other_key.ToDebugString());
}

TEST(NetworkIsolationKeyTest, OpaqueOriginTopLevelSiteKey) {
  SchemefulSite site1 = SchemefulSite(GURL("http://a.test/"));
  SchemefulSite site_data = SchemefulSite(GURL(kDataUrl));
  NetworkIsolationKey key(site_data, site1);
  EXPECT_FALSE(key.IsEmpty());
  EXPECT_EQ(std::nullopt, key.ToCacheKeyString());
  EXPECT_TRUE(key.IsTransient());
  EXPECT_EQ(site_data.GetDebugString() + " " + site1.GetDebugString(),
            key.ToDebugString());

  // Create another site with an opaque origin, and make sure it's different and
  // has a different debug string.
  SchemefulSite other_site = SchemefulSite(GURL(kDataUrl));
  NetworkIsolationKey other_key(other_site, site1);
  EXPECT_NE(key, other_key);
  EXPECT_NE(key.ToDebugString(), other_key.ToDebugString());
  EXPECT_EQ(other_site.GetDebugString() + " " + site1.GetDebugString(),
            other_key.ToDebugString());
}

TEST(NetworkIsolationKeyTest, OpaqueOriginIframeKey) {
  SchemefulSite site1 = SchemefulSite(GURL("http://a.test/"));
  SchemefulSite site_data = SchemefulSite(GURL(kDataUrl));
  NetworkIsolationKey key(site1, site_data);
  EXPECT_FALSE(key.IsEmpty());
  EXPECT_EQ(std::nullopt, key.ToCacheKeyString());
  EXPECT_TRUE(key.IsTransient());
  EXPECT_EQ(site1.GetDebugString() + " " + site_data.GetDebugString(),
            key.ToDebugString());

  // Create another site with an opaque origin iframe, and make sure it's
  // different and has a different debug string when the frame site is in use.
  SchemefulSite other_site = SchemefulSite(GURL(kDataUrl));
  NetworkIsolationKey other_key(site1, other_site);
  EXPECT_NE(key, other_key);
  EXPECT_NE(key.ToDebugString(), other_key.ToDebugString());
  EXPECT_EQ(site1.GetDebugString() + " " + other_site.GetDebugString(),
            other_key.ToDebugString());
}

TEST(NetworkIsolationKeyTest, Operators) {
  base::UnguessableToken nonce1 = base::UnguessableToken::Create();
  base::UnguessableToken nonce2 = base::UnguessableToken::Create();
  if (nonce2 < nonce1)
    std::swap(nonce1, nonce2);
  // These are in ascending order.
  const auto kKeys = std::to_array<NetworkIsolationKey>({
      NetworkIsolationKey(),
      // Site with unique origins are still sorted by scheme, so data is before
      // file, and file before http.
      NetworkIsolationKey(SchemefulSite(GURL(kDataUrl)),
                          SchemefulSite(GURL(kDataUrl))),
      NetworkIsolationKey(SchemefulSite(GURL("file:///foo")),
                          SchemefulSite(GURL("file:///foo"))),
      NetworkIsolationKey(SchemefulSite(GURL("http://a.test/")),
                          SchemefulSite(GURL("http://a.test/"))),
      NetworkIsolationKey(SchemefulSite(GURL("http://b.test/")),
                          SchemefulSite(GURL("http://b.test/"))),
      NetworkIsolationKey(SchemefulSite(GURL("https://a.test/")),
                          SchemefulSite(GURL("https://a.test/")),
                          /*nonce=*/std::nullopt,
                          NetworkIsolationPartition::kGeneral),
      NetworkIsolationKey(
          SchemefulSite(GURL("https://a.test/")),
          SchemefulSite(GURL("https://a.test/")), /*nonce=*/std::nullopt,
          NetworkIsolationPartition::kProtectedAudienceSellerWorklet),
      NetworkIsolationKey(SchemefulSite(GURL("https://a.test/")),
                          SchemefulSite(GURL("https://a.test/")), nonce1),
      NetworkIsolationKey(SchemefulSite(GURL("https://a.test/")),
                          SchemefulSite(GURL("https://a.test/")), nonce2),
  });

  for (size_t first = 0; first < std::size(kKeys); ++first) {
    NetworkIsolationKey key1 = kKeys[first];
    SCOPED_TRACE(key1.ToDebugString());

    EXPECT_TRUE(key1 == key1);
    EXPECT_FALSE(key1 != key1);
    EXPECT_FALSE(key1 < key1);

    // Make sure that copying a key doesn't change the results of any operation.
    // This check is a bit more interesting with unique origins.
    NetworkIsolationKey key1_copy = key1;
    EXPECT_TRUE(key1 == key1_copy);
    EXPECT_FALSE(key1 < key1_copy);
    EXPECT_FALSE(key1_copy < key1);

    for (size_t second = first + 1; second < std::size(kKeys); ++second) {
      NetworkIsolationKey key2 = kKeys[second];
      SCOPED_TRACE(key2.ToDebugString());

      EXPECT_TRUE(key1 < key2);
      EXPECT_FALSE(key2 < key1);
      EXPECT_FALSE(key1 == key2);
      EXPECT_FALSE(key2 == key1);
    }
  }
}

TEST(NetworkIsolationKeyTest, UniqueOriginOperators) {
  const auto kSite1 = SchemefulSite(GURL(kDataUrl));
  const auto kSite2 = SchemefulSite(GURL(kDataUrl));
  NetworkIsolationKey key1(kSite1, kSite1);
  NetworkIsolationKey key2(kSite2, kSite2);

  EXPECT_TRUE(key1 == key1);
  EXPECT_TRUE(key2 == key2);

  // Creating copies shouldn't affect comparison result.
  EXPECT_TRUE(NetworkIsolationKey(key1) == NetworkIsolationKey(key1));
  EXPECT_TRUE(NetworkIsolationKey(key2) == NetworkIsolationKey(key2));

  EXPECT_FALSE(key1 == key2);
  EXPECT_FALSE(key2 == key1);

  // Order of Nonces isn't predictable, but they should have an ordering.
  EXPECT_TRUE(key1 < key2 || key2 < key1);
  EXPECT_TRUE(!(key1 < key2) || !(key2 < key1));
}

TEST(NetworkIsolationKeyTest, OpaqueSiteKeyBoth) {
  SchemefulSite site_data_1 = SchemefulSite(GURL(kDataUrl));
  SchemefulSite site_data_2 = SchemefulSite(GURL(kDataUrl));
  SchemefulSite site_data_3 = SchemefulSite(GURL(kDataUrl));

  NetworkIsolationKey key1(site_data_1, site_data_2);
  NetworkIsolationKey key2(site_data_1, site_data_2);
  NetworkIsolationKey key3(site_data_1, site_data_3);

  // All the keys should be fully populated and transient.
  EXPECT_FALSE(key1.IsEmpty());
  EXPECT_FALSE(key2.IsEmpty());
  EXPECT_FALSE(key3.IsEmpty());
  EXPECT_TRUE(key1.IsTransient());
  EXPECT_TRUE(key2.IsTransient());
  EXPECT_TRUE(key3.IsTransient());

  // Test the equality/comparisons of the various keys
  EXPECT_TRUE(key1 == key2);
  EXPECT_FALSE(key1 < key2 || key2 < key1);
  EXPECT_FALSE(key1 == key3);
  EXPECT_TRUE(key1 < key3 || key3 < key1);
  EXPECT_NE(key1.ToDebugString(), key3.ToDebugString());

  // Test the ToString and ToDebugString
  EXPECT_EQ(key1.ToDebugString(), key2.ToDebugString());
  EXPECT_EQ(std::nullopt, key1.ToCacheKeyString());
  EXPECT_EQ(std::nullopt, key2.ToCacheKeyString());
  EXPECT_EQ(std::nullopt, key3.ToCacheKeyString());
}

// Make sure that the logic to extract the registerable domain from an origin
// does not affect the host when using a non-standard scheme.
TEST(NetworkIsolationKeyTest, NonStandardScheme) {
  // Have to register the scheme, or SchemefulSite() will return an opaque
  // origin.
  url::ScopedSchemeRegistryForTests scoped_registry;
  url::AddStandardScheme("foo", url::SCHEME_WITH_HOST);

  SchemefulSite site = SchemefulSite(GURL("foo://a.foo.com"));
  NetworkIsolationKey key(site, site);
  EXPECT_FALSE(key.GetTopFrameSite()->opaque());
  EXPECT_EQ("foo://a.foo.com foo://a.foo.com", key.ToCacheKeyString());
}

TEST(NetworkIsolationKeyTest, CreateWithNewFrameSite) {
  SchemefulSite site_a = SchemefulSite(GURL("http://a.com"));
  SchemefulSite site_b = SchemefulSite(GURL("http://b.com"));
  SchemefulSite site_c = SchemefulSite(GURL("http://c.com"));

  NetworkIsolationKey key(site_a, site_b);
  NetworkIsolationKey key_c = key.CreateWithNewFrameSite(site_c);
  EXPECT_EQ(site_c, key_c.GetFrameSiteForTesting());
  EXPECT_NE(key_c, key);
  EXPECT_EQ(site_a, key_c.GetTopFrameSite());

  // Ensure that `CreateWithNewFrameSite()` preserves the nonce if one exists.
  base::UnguessableToken nonce = base::UnguessableToken::Create();
  NetworkIsolationKey key_with_nonce(site_a, site_b, nonce);
  NetworkIsolationKey key_with_nonce_c =
      key_with_nonce.CreateWithNewFrameSite(site_c);
  EXPECT_EQ(key_with_nonce.GetNonce(), key_with_nonce_c.GetNonce());
  EXPECT_TRUE(key_with_nonce_c.IsTransient());

  // Ensure that `CreateWithNewFrameSite()` preserves the
  // NetworkIsolationPartition.
  NetworkIsolationKey key_with_partition(
      site_a, site_b, /*nonce=*/std::nullopt,
      NetworkIsolationPartition::kProtectedAudienceSellerWorklet);
  NetworkIsolationKey key_with_partition_c =
      key_with_partition.CreateWithNewFrameSite(site_c);
  EXPECT_EQ(key_with_partition.GetNetworkIsolationPartition(),
            NetworkIsolationPartition::kProtectedAudienceSellerWorklet);
  EXPECT_EQ(key_with_partition.GetNetworkIsolationPartition(),
            key_with_partition_c.GetNetworkIsolationPartition());
}

TEST(NetworkIsolationKeyTest, CreateTransientForTesting) {
  NetworkIsolationKey transient_key =
      NetworkIsolationKey::CreateTransientForTesting();
  EXPECT_FALSE(transient_key.IsEmpty());
  EXPECT_TRUE(transient_key.IsTransient());
  EXPECT_FALSE(transient_key.IsEmpty());
  EXPECT_EQ(transient_key, transient_key);

  // Make sure that subsequent calls don't return the same NIK.
  for (int i = 0; i < 1000; ++i) {
    EXPECT_NE(transient_key, NetworkIsolationKey::CreateTransientForTesting());
  }
}

TEST(NetworkIsolationKeyTest, SupportsAbslHash) {
  SchemefulSite site_a = SchemefulSite(GURL("http://a.test/"));
  SchemefulSite site_b = SchemefulSite(GURL("http://b.test/"));
  // These are different even though they are constructed from the same URL,
  // because they are opaque origins.
  SchemefulSite data_site_1 = SchemefulSite(GURL("data:foo"));
  SchemefulSite data_site_2 = SchemefulSite(GURL("data:foo"));
  base::UnguessableToken nonce = base::UnguessableToken::Create();
  base::UnguessableToken different_nonce = base::UnguessableToken::Create();

  EXPECT_TRUE(absl::VerifyTypeImplementsAbslHashCorrectly({
      NetworkIsolationKey(),
      NetworkIsolationKey(site_a, site_a),
      NetworkIsolationKey(site_a, site_b),
      NetworkIsolationKey(site_b, site_a),
      NetworkIsolationKey(site_b, site_b),
      NetworkIsolationKey(data_site_1, data_site_1),
      NetworkIsolationKey(data_site_2, data_site_2),
      NetworkIsolationKey(data_site_1, data_site_2),
      NetworkIsolationKey(site_a, data_site_1),
      NetworkIsolationKey(data_site_1, site_a),
      NetworkIsolationKey(site_a, site_a, nonce),
      NetworkIsolationKey(site_a, site_b, nonce),
      NetworkIsolationKey(site_a, site_b, different_nonce),
      NetworkIsolationKey(
          site_a, site_a, std::nullopt,
          NetworkIsolationPartition::kProtectedAudienceSellerWorklet),
      NetworkIsolationKey(
          site_a, site_a, std::nullopt,
          NetworkIsolationPartition::kFedCmUncredentialedRequests),
  }));
}

}  // namespace

}  // namespace net
