<commit_before>// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "google/cloud/storage/client.h"
#include "google/cloud/storage/list_objects_reader.h"
#include "google/cloud/storage/testing/remove_stale_buckets.h"
#include "google/cloud/storage/testing/storage_integration_test.h"
#include "google/cloud/internal/algorithm.h"
#include "google/cloud/internal/getenv.h"
#include "google/cloud/testing_util/contains_once.h"
#include "google/cloud/testing_util/status_matchers.h"
#include <gmock/gmock.h>
#include <chrono>
#include <thread>
// TODO(#5929) - remove once deprecated functions are removed
#include "google/cloud/internal/disable_deprecation_warnings.inc"

namespace google {
namespace cloud {
namespace storage {
inline namespace STORAGE_CLIENT_NS {
namespace {

using ::google::cloud::storage::testing::AclEntityNames;
using ::google::cloud::testing_util::ContainsOnce;
using ::google::cloud::testing_util::IsOk;
using ::testing::Contains;
using ::testing::HasSubstr;
using ::testing::Not;
using ::testing::UnorderedElementsAreArray;

class BucketIntegrationTest
    : public google::cloud::storage::testing::StorageIntegrationTest {
 protected:
  void SetUp() override {
    project_id_ =
        google::cloud::internal::GetEnv("GOOGLE_CLOUD_PROJECT").value_or("");
    ASSERT_FALSE(project_id_.empty());
    bucket_name_ = google::cloud::internal::GetEnv(
                       "GOOGLE_CLOUD_CPP_STORAGE_TEST_BUCKET_NAME")
                       .value_or("");
    ASSERT_FALSE(bucket_name_.empty());
    topic_name_ = google::cloud::internal::GetEnv(
                      "GOOGLE_CLOUD_CPP_STORAGE_TEST_TOPIC_NAME")
                      .value_or("");
    ASSERT_FALSE(topic_name_.empty());
    service_account_ = google::cloud::internal::GetEnv(
                           "GOOGLE_CLOUD_CPP_STORAGE_TEST_SERVICE_ACCOUNT")
                           .value_or("");
    ASSERT_FALSE(service_account_.empty());
  }

  std::string MakeEntityName() {
    // We always use the viewers for the project because it is known to exist.
    return "project-viewers-" + project_id_;
  }

  std::string project_id_;
  std::string bucket_name_;
  std::string topic_name_;
  std::string service_account_;
};

TEST_F(BucketIntegrationTest, BasicCRUD) {
  std::string bucket_name = MakeRandomBucketName();
  StatusOr<Client> client = MakeBucketIntegrationTestClient();
  ASSERT_STATUS_OK(client);

  // We use this test to remove any buckets created by the integration tests
  // more than 48 hours ago.
  testing::RemoveStaleBuckets(
      *client, RandomBucketNamePrefix(),
      std::chrono::system_clock::now() - std::chrono::hours(48));

  auto list_bucket_names = [&client, this] {
    std::vector<std::string> names;
    for (auto b : client->ListBucketsForProject(project_id_)) {
      EXPECT_STATUS_OK(b);
      if (!b) break;
      names.push_back(b->name());
    }
    return names;
  };
  ASSERT_THAT(list_bucket_names(), Not(Contains(bucket_name)))
      << "Test aborted. The bucket <" << bucket_name << "> already exists."
      << " This is unexpected as the test generates a random bucket name.";

  auto insert_meta = client->CreateBucketForProject(bucket_name, project_id_,
                                                    BucketMetadata());
  ASSERT_STATUS_OK(insert_meta);
  EXPECT_EQ(bucket_name, insert_meta->name());
  EXPECT_THAT(list_bucket_names(), ContainsOnce(bucket_name));

  StatusOr<BucketMetadata> get_meta = client->GetBucketMetadata(bucket_name);
  ASSERT_STATUS_OK(get_meta);
  EXPECT_EQ(*insert_meta, *get_meta);

  // Create a request to update the metadata, change the storage class because
  // it is easy. And use either COLDLINE or NEARLINE depending on the existing
  // value.
  std::string desired_storage_class = storage_class::Coldline();
  if (get_meta->storage_class() == storage_class::Coldline()) {
    desired_storage_class = storage_class::Nearline();
  }
  BucketMetadata update = *get_meta;
  update.set_storage_class(desired_storage_class);
  StatusOr<BucketMetadata> updated_meta =
      client->UpdateBucket(bucket_name, update);
  ASSERT_STATUS_OK(updated_meta);
  EXPECT_EQ(desired_storage_class, updated_meta->storage_class());

  // Patch the metadata to change the storage class, add some lifecycle
  // rules, and the website settings.
  BucketMetadata desired_state = *updated_meta;
  LifecycleRule rule(LifecycleRule::ConditionConjunction(
                         LifecycleRule::MaxAge(30),
                         LifecycleRule::MatchesStorageClassStandard()),
                     LifecycleRule::Delete());
  desired_state.set_storage_class(storage_class::Standard())
      .set_lifecycle(BucketLifecycle{{rule}})
      .set_website(BucketWebsite{"index.html", "404.html"});

  StatusOr<BucketMetadata> patched =
      client->PatchBucket(bucket_name, *updated_meta, desired_state);
  ASSERT_STATUS_OK(patched);
  EXPECT_EQ(storage_class::Standard(), patched->storage_class());
  EXPECT_EQ(1, patched->lifecycle().rule.size());

  // Patch the metadata again, this time remove billing and website settings.
  patched = client->PatchBucket(
      bucket_name, BucketMetadataPatchBuilder().ResetWebsite().ResetBilling());
  ASSERT_STATUS_OK(patched);
  EXPECT_FALSE(patched->has_billing());
  EXPECT_FALSE(patched->has_website());

  auto status = client->DeleteBucket(bucket_name);
  ASSERT_STATUS_OK(status);
  EXPECT_THAT(list_bucket_names(), Not(Contains(bucket_name)));
}

TEST_F(BucketIntegrationTest, CreatePredefinedAcl) {
  std::vector<PredefinedAcl> test_values{
      PredefinedAcl::AuthenticatedRead(), PredefinedAcl::Private(),
      PredefinedAcl::ProjectPrivate(),    PredefinedAcl::PublicRead(),
      PredefinedAcl::PublicReadWrite(),
  };

  StatusOr<Client> client = MakeBucketIntegrationTestClient();
  ASSERT_STATUS_OK(client);
  for (auto const& acl : test_values) {
    SCOPED_TRACE(std::string("Testing with ") +
                 acl.well_known_parameter_name() + "=" + acl.value());
    std::string bucket_name = MakeRandomBucketName();

    auto metadata = client->CreateBucketForProject(
        bucket_name, project_id_, BucketMetadata(), PredefinedAcl(acl));
    ASSERT_STATUS_OK(metadata);
    EXPECT_EQ(bucket_name, metadata->name());

    // Wait at least 2 seconds before trying to create / delete another bucket.
    if (!UsingEmulator()) std::this_thread::sleep_for(std::chrono::seconds(2));

    auto status = client->DeleteBucket(bucket_name);
    ASSERT_STATUS_OK(status);

    // Wait at least 2 seconds before trying to create / delete another bucket.
    if (!UsingEmulator()) std::this_thread::sleep_for(std::chrono::seconds(2));
  }
}

TEST_F(BucketIntegrationTest, CreatePredefinedDefaultObjectAcl) {
  std::vector<PredefinedDefaultObjectAcl> test_values{
      PredefinedDefaultObjectAcl::AuthenticatedRead(),
      PredefinedDefaultObjectAcl::BucketOwnerFullControl(),
      PredefinedDefaultObjectAcl::BucketOwnerRead(),
      PredefinedDefaultObjectAcl::Private(),
      PredefinedDefaultObjectAcl::ProjectPrivate(),
      PredefinedDefaultObjectAcl::PublicRead(),
  };

  StatusOr<Client> client = MakeBucketIntegrationTestClient();
  ASSERT_STATUS_OK(client);
  for (auto const& acl : test_values) {
    SCOPED_TRACE(std::string("Testing with ") +
                 acl.well_known_parameter_name() + "=" + acl.value());
    std::string bucket_name = MakeRandomBucketName();

    auto metadata = client->CreateBucketForProject(
        bucket_name, project_id_, BucketMetadata(),
        PredefinedDefaultObjectAcl(acl));
    ASSERT_STATUS_OK(metadata);
    EXPECT_EQ(bucket_name, metadata->name());

    // Wait at least 2 seconds before trying to create / delete another bucket.
    if (!UsingEmulator()) std::this_thread::sleep_for(std::chrono::seconds(2));

    auto status = client->DeleteBucket(bucket_name);
    ASSERT_STATUS_OK(status);

    // Wait at least 2 seconds before trying to create / delete another bucket.
    if (!UsingEmulator()) std::this_thread::sleep_for(std::chrono::seconds(2));
  }
}

TEST_F(BucketIntegrationTest, PatchLifecycleConditions) {
  std::vector<LifecycleRuleCondition> test_values{
      LifecycleRule::MaxAge(30),
      LifecycleRule::CreatedBefore(absl::CivilDay(2020, 7, 26)),
      LifecycleRule::IsLive(false),
      LifecycleRule::MatchesStorageClassArchive(),
      LifecycleRule::MatchesStorageClasses(
          {storage_class::Standard(), storage_class::Nearline()}),
      LifecycleRule::MatchesStorageClassStandard(),
      // Skip this one because it requires creating a regional bucket (the
      // default is multi-regional US), and that felt like too much of a hassle
      //   LifecycleRule::MatchesStorageClassRegional(),
      LifecycleRule::MatchesStorageClassMultiRegional(),
      LifecycleRule::MatchesStorageClassNearline(),
      LifecycleRule::MatchesStorageClassColdline(),
      LifecycleRule::MatchesStorageClassDurableReducedAvailability(),
  };

  StatusOr<Client> client = MakeBucketIntegrationTestClient();
  ASSERT_STATUS_OK(client);
  std::string bucket_name = MakeRandomBucketName();

  auto original = client->CreateBucketForProject(bucket_name, project_id_,
                                                 BucketMetadata{});
  ASSERT_STATUS_OK(original);
  EXPECT_EQ(bucket_name, original->name());

  for (auto const& condition : test_values) {
    std::ostringstream os;
    os << "testing with " << condition;
    auto const str = std::move(os).str();
    SCOPED_TRACE(str);

    auto updated = client->PatchBucket(
        bucket_name, BucketMetadataPatchBuilder{}.SetLifecycle({{LifecycleRule{
                         condition, LifecycleRule::Delete()}}}));
    // We do not use an ASSERT_STATUS_OK() here because we want to continue and
    // delete the temporary bucket.
    EXPECT_STATUS_OK(updated);
    if (updated) {
      EXPECT_TRUE(updated->has_lifecycle()) << "updated = " << *updated;
    }
  }

  auto status = client->DeleteBucket(bucket_name);
  ASSERT_STATUS_OK(status);
}

TEST_F(BucketIntegrationTest, FullPatch) {
  std::string bucket_name = MakeRandomBucketName();
  StatusOr<Client> client = MakeBucketIntegrationTestClient();
  ASSERT_STATUS_OK(client);

  // We need to have an available bucket for logging ...
  std::string logging_name = MakeRandomBucketName();
  StatusOr<BucketMetadata> const logging_meta = client->CreateBucketForProject(
      logging_name, project_id_, BucketMetadata(), PredefinedAcl("private"),
      PredefinedDefaultObjectAcl("projectPrivate"), Projection("noAcl"));
  ASSERT_STATUS_OK(logging_meta);
  EXPECT_EQ(logging_name, logging_meta->name());

  // Wait at least 2 seconds before trying to create / delete another bucket.
  if (!UsingEmulator()) std::this_thread::sleep_for(std::chrono::seconds(2));
  // Create a Bucket, use the default settings for most fields, except the
  // storage class and location. Fetch the full attributes of the bucket.
  StatusOr<BucketMetadata> const insert_meta = client->CreateBucketForProject(
      bucket_name, project_id_,
      BucketMetadata().set_location("US").set_storage_class(
          storage_class::Standard()),
      PredefinedAcl("private"), PredefinedDefaultObjectAcl("projectPrivate"),
      Projection("full"));
  ASSERT_STATUS_OK(insert_meta);
  EXPECT_EQ(bucket_name, insert_meta->name());

  // Patch every possible field in the metadata, to verify they work.
  BucketMetadata desired_state = *insert_meta;
  // acl()
  desired_state.mutable_acl().push_back(BucketAccessControl()
                                            .set_entity("allAuthenticatedUsers")
                                            .set_role("READER"));

  // billing()
  if (!desired_state.has_billing()) {
    desired_state.set_billing(BucketBilling(false));
  } else {
    desired_state.set_billing(
        BucketBilling(!desired_state.billing().requester_pays));
  }

  // cors()
  desired_state.mutable_cors().push_back(CorsEntry{86400, {"GET"}, {}, {}});

  // default_acl()
  desired_state.mutable_default_acl().push_back(
      ObjectAccessControl()
          .set_entity("allAuthenticatedUsers")
          .set_role("READER"));

  // encryption()
  // TODO(#1003) - need a valid KMS entry to set the encryption.

  // iam_configuration() - skipped, cannot set both ACL and iam_configuration in
  // the same bucket.

  // labels()
  desired_state.mutable_labels().emplace("test-label", "testing-full-patch");

  // lifecycle()
  LifecycleRule rule(LifecycleRule::ConditionConjunction(
                         LifecycleRule::MaxAge(30),
                         LifecycleRule::MatchesStorageClassStandard()),
                     LifecycleRule::Delete());
  desired_state.set_lifecycle(BucketLifecycle{{rule}});

  // logging()
  if (desired_state.has_logging()) {
    desired_state.reset_logging();
  } else {
    desired_state.set_logging(BucketLogging{logging_name, "test-log"});
  }

  // storage_class()
  desired_state.set_storage_class(storage_class::Coldline());

  // versioning()
  if (!desired_state.has_versioning()) {
    desired_state.enable_versioning();
  } else {
    desired_state.reset_versioning();
  }

  // website()
  if (desired_state.has_website()) {
    desired_state.reset_website();
  } else {
    desired_state.set_website(BucketWebsite{"index.html", "404.html"});
  }

  StatusOr<BucketMetadata> patched =
      client->PatchBucket(bucket_name, *insert_meta, desired_state);
  ASSERT_STATUS_OK(patched);
  // acl() - cannot compare for equality because many fields are updated with
  // unknown values (entity_id, etag, etc)
  EXPECT_THAT(AclEntityNames(patched->acl()),
              ContainsOnce("allAuthenticatedUsers"));

  // billing()
  EXPECT_EQ(desired_state.billing_as_optional(),
            patched->billing_as_optional());

  // cors()
  EXPECT_EQ(desired_state.cors(), patched->cors());

  // default_acl() - cannot compare for equality because many fields are updated
  // with unknown values (entity_id, etag, etc)
  EXPECT_THAT(AclEntityNames(patched->default_acl()),
              ContainsOnce("allAuthenticatedUsers"));

  // encryption() - TODO(#1003) - verify the key was correctly used.

  // lifecycle()
  EXPECT_EQ(desired_state.lifecycle_as_optional(),
            patched->lifecycle_as_optional());

  // location()
  EXPECT_EQ(desired_state.location(), patched->location());

  // logging()
  EXPECT_EQ(desired_state.logging_as_optional(),
            patched->logging_as_optional());

  // storage_class()
  EXPECT_EQ(desired_state.storage_class(), patched->storage_class());

  // versioning()
  EXPECT_EQ(desired_state.versioning(), patched->versioning());

  // website()
  EXPECT_EQ(desired_state.website_as_optional(),
            patched->website_as_optional());

  auto status = client->DeleteBucket(bucket_name);
  ASSERT_STATUS_OK(status);
  // Wait at least 2 seconds before trying to create / delete another bucket.
  if (!UsingEmulator()) std::this_thread::sleep_for(std::chrono::seconds(2));
  status = client->DeleteBucket(logging_name);
  ASSERT_STATUS_OK(status);
}

// @test Verify that we can set the iam_configuration() in a Bucket.
TEST_F(BucketIntegrationTest, UniformBucketLevelAccessPatch) {
  std::string bucket_name = MakeRandomBucketName();
  StatusOr<Client> client = MakeIntegrationTestClient();
  ASSERT_STATUS_OK(client);

  // Create a Bucket, use the default settings for all fields. Fetch the full
  // attributes of the bucket.
  StatusOr<BucketMetadata> const insert_meta = client->CreateBucketForProject(
      bucket_name, project_id_, BucketMetadata(), PredefinedAcl("private"),
      PredefinedDefaultObjectAcl("projectPrivate"), Projection("full"));
  ASSERT_STATUS_OK(insert_meta);
  EXPECT_EQ(bucket_name, insert_meta->name());

  // Patch the iam_configuration().
  BucketMetadata desired_state = *insert_meta;
  BucketIamConfiguration iam_configuration;
  iam_configuration.uniform_bucket_level_access =
      UniformBucketLevelAccess{true, {}};
  desired_state.set_iam_configuration(std::move(iam_configuration));

  StatusOr<BucketMetadata> patched =
      client->PatchBucket(bucket_name, *insert_meta, desired_state);
  ASSERT_STATUS_OK(patched);

  ASSERT_TRUE(patched->has_iam_configuration()) << "patched=" << *patched;
  ASSERT_TRUE(patched->iam_configuration().uniform_bucket_level_access)
      << "patched=" << *patched;

  auto status = client->DeleteBucket(bucket_name);
  ASSERT_STATUS_OK(status);
}

// @test Verify that we can set the iam_configuration() in a Bucket.
TEST_F(BucketIntegrationTest, PublicAccessPreventionPatch) {
  if (!UsingEmulator()) GTEST_SKIP();
  std::string bucket_name = MakeRandomBucketName();
  StatusOr<Client> client = MakeIntegrationTestClient();
  ASSERT_STATUS_OK(client);

  // Create a Bucket, use the default settings for all fields. Fetch the full
  // attributes of the bucket.
  StatusOr<BucketMetadata> const insert_meta = client->CreateBucketForProject(
      bucket_name, project_id_, BucketMetadata(), PredefinedAcl("private"),
      PredefinedDefaultObjectAcl("projectPrivate"), Projection("full"));
  ASSERT_STATUS_OK(insert_meta);
  EXPECT_EQ(bucket_name, insert_meta->name());<commit_msg>test(storage): public access prevention in prod (#6914)<commit_after>// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "google/cloud/storage/client.h"
#include "google/cloud/storage/list_objects_reader.h"
#include "google/cloud/storage/testing/remove_stale_buckets.h"
#include "google/cloud/storage/testing/storage_integration_test.h"
#include "google/cloud/internal/algorithm.h"
#include "google/cloud/internal/getenv.h"
#include "google/cloud/testing_util/contains_once.h"
#include "google/cloud/testing_util/status_matchers.h"
#include <gmock/gmock.h>
#include <chrono>
#include <thread>
// TODO(#5929) - remove once deprecated functions are removed
#include "google/cloud/internal/disable_deprecation_warnings.inc"

namespace google {
namespace cloud {
namespace storage {
inline namespace STORAGE_CLIENT_NS {
namespace {

using ::google::cloud::storage::testing::AclEntityNames;
using ::google::cloud::testing_util::ContainsOnce;
using ::google::cloud::testing_util::IsOk;
using ::testing::Contains;
using ::testing::HasSubstr;
using ::testing::Not;
using ::testing::UnorderedElementsAreArray;

class BucketIntegrationTest
    : public google::cloud::storage::testing::StorageIntegrationTest {
 protected:
  void SetUp() override {
    project_id_ =
        google::cloud::internal::GetEnv("GOOGLE_CLOUD_PROJECT").value_or("");
    ASSERT_FALSE(project_id_.empty());
    bucket_name_ = google::cloud::internal::GetEnv(
                       "GOOGLE_CLOUD_CPP_STORAGE_TEST_BUCKET_NAME")
                       .value_or("");
    ASSERT_FALSE(bucket_name_.empty());
    topic_name_ = google::cloud::internal::GetEnv(
                      "GOOGLE_CLOUD_CPP_STORAGE_TEST_TOPIC_NAME")
                      .value_or("");
    ASSERT_FALSE(topic_name_.empty());
    service_account_ = google::cloud::internal::GetEnv(
                           "GOOGLE_CLOUD_CPP_STORAGE_TEST_SERVICE_ACCOUNT")
                           .value_or("");
    ASSERT_FALSE(service_account_.empty());
  }

  std::string MakeEntityName() {
    // We always use the viewers for the project because it is known to exist.
    return "project-viewers-" + project_id_;
  }

  std::string project_id_;
  std::string bucket_name_;
  std::string topic_name_;
  std::string service_account_;
};

TEST_F(BucketIntegrationTest, BasicCRUD) {
  std::string bucket_name = MakeRandomBucketName();
  StatusOr<Client> client = MakeBucketIntegrationTestClient();
  ASSERT_STATUS_OK(client);

  // We use this test to remove any buckets created by the integration tests
  // more than 48 hours ago.
  testing::RemoveStaleBuckets(
      *client, RandomBucketNamePrefix(),
      std::chrono::system_clock::now() - std::chrono::hours(48));

  auto list_bucket_names = [&client, this] {
    std::vector<std::string> names;
    for (auto b : client->ListBucketsForProject(project_id_)) {
      EXPECT_STATUS_OK(b);
      if (!b) break;
      names.push_back(b->name());
    }
    return names;
  };
  ASSERT_THAT(list_bucket_names(), Not(Contains(bucket_name)))
      << "Test aborted. The bucket <" << bucket_name << "> already exists."
      << " This is unexpected as the test generates a random bucket name.";

  auto insert_meta = client->CreateBucketForProject(bucket_name, project_id_,
                                                    BucketMetadata());
  ASSERT_STATUS_OK(insert_meta);
  EXPECT_EQ(bucket_name, insert_meta->name());
  EXPECT_THAT(list_bucket_names(), ContainsOnce(bucket_name));

  StatusOr<BucketMetadata> get_meta = client->GetBucketMetadata(bucket_name);
  ASSERT_STATUS_OK(get_meta);
  EXPECT_EQ(*insert_meta, *get_meta);

  // Create a request to update the metadata, change the storage class because
  // it is easy. And use either COLDLINE or NEARLINE depending on the existing
  // value.
  std::string desired_storage_class = storage_class::Coldline();
  if (get_meta->storage_class() == storage_class::Coldline()) {
    desired_storage_class = storage_class::Nearline();
  }
  BucketMetadata update = *get_meta;
  update.set_storage_class(desired_storage_class);
  StatusOr<BucketMetadata> updated_meta =
      client->UpdateBucket(bucket_name, update);
  ASSERT_STATUS_OK(updated_meta);
  EXPECT_EQ(desired_storage_class, updated_meta->storage_class());

  // Patch the metadata to change the storage class, add some lifecycle
  // rules, and the website settings.
  BucketMetadata desired_state = *updated_meta;
  LifecycleRule rule(LifecycleRule::ConditionConjunction(
                         LifecycleRule::MaxAge(30),
                         LifecycleRule::MatchesStorageClassStandard()),
                     LifecycleRule::Delete());
  desired_state.set_storage_class(storage_class::Standard())
      .set_lifecycle(BucketLifecycle{{rule}})
      .set_website(BucketWebsite{"index.html", "404.html"});

  StatusOr<BucketMetadata> patched =
      client->PatchBucket(bucket_name, *updated_meta, desired_state);
  ASSERT_STATUS_OK(patched);
  EXPECT_EQ(storage_class::Standard(), patched->storage_class());
  EXPECT_EQ(1, patched->lifecycle().rule.size());

  // Patch the metadata again, this time remove billing and website settings.
  patched = client->PatchBucket(
      bucket_name, BucketMetadataPatchBuilder().ResetWebsite().ResetBilling());
  ASSERT_STATUS_OK(patched);
  EXPECT_FALSE(patched->has_billing());
  EXPECT_FALSE(patched->has_website());

  auto status = client->DeleteBucket(bucket_name);
  ASSERT_STATUS_OK(status);
  EXPECT_THAT(list_bucket_names(), Not(Contains(bucket_name)));
}

TEST_F(BucketIntegrationTest, CreatePredefinedAcl) {
  std::vector<PredefinedAcl> test_values{
      PredefinedAcl::AuthenticatedRead(), PredefinedAcl::Private(),
      PredefinedAcl::ProjectPrivate(),    PredefinedAcl::PublicRead(),
      PredefinedAcl::PublicReadWrite(),
  };

  StatusOr<Client> client = MakeBucketIntegrationTestClient();
  ASSERT_STATUS_OK(client);
  for (auto const& acl : test_values) {
    SCOPED_TRACE(std::string("Testing with ") +
                 acl.well_known_parameter_name() + "=" + acl.value());
    std::string bucket_name = MakeRandomBucketName();

    auto metadata = client->CreateBucketForProject(
        bucket_name, project_id_, BucketMetadata(), PredefinedAcl(acl));
    ASSERT_STATUS_OK(metadata);
    EXPECT_EQ(bucket_name, metadata->name());

    // Wait at least 2 seconds before trying to create / delete another bucket.
    if (!UsingEmulator()) std::this_thread::sleep_for(std::chrono::seconds(2));

    auto status = client->DeleteBucket(bucket_name);
    ASSERT_STATUS_OK(status);

    // Wait at least 2 seconds before trying to create / delete another bucket.
    if (!UsingEmulator()) std::this_thread::sleep_for(std::chrono::seconds(2));
  }
}

TEST_F(BucketIntegrationTest, CreatePredefinedDefaultObjectAcl) {
  std::vector<PredefinedDefaultObjectAcl> test_values{
      PredefinedDefaultObjectAcl::AuthenticatedRead(),
      PredefinedDefaultObjectAcl::BucketOwnerFullControl(),
      PredefinedDefaultObjectAcl::BucketOwnerRead(),
      PredefinedDefaultObjectAcl::Private(),
      PredefinedDefaultObjectAcl::ProjectPrivate(),
      PredefinedDefaultObjectAcl::PublicRead(),
  };

  StatusOr<Client> client = MakeBucketIntegrationTestClient();
  ASSERT_STATUS_OK(client);
  for (auto const& acl : test_values) {
    SCOPED_TRACE(std::string("Testing with ") +
                 acl.well_known_parameter_name() + "=" + acl.value());
    std::string bucket_name = MakeRandomBucketName();

    auto metadata = client->CreateBucketForProject(
        bucket_name, project_id_, BucketMetadata(),
        PredefinedDefaultObjectAcl(acl));
    ASSERT_STATUS_OK(metadata);
    EXPECT_EQ(bucket_name, metadata->name());

    // Wait at least 2 seconds before trying to create / delete another bucket.
    if (!UsingEmulator()) std::this_thread::sleep_for(std::chrono::seconds(2));

    auto status = client->DeleteBucket(bucket_name);
    ASSERT_STATUS_OK(status);

    // Wait at least 2 seconds before trying to create / delete another bucket.
    if (!UsingEmulator()) std::this_thread::sleep_for(std::chrono::seconds(2));
  }
}

TEST_F(BucketIntegrationTest, PatchLifecycleConditions) {
  std::vector<LifecycleRuleCondition> test_values{
      LifecycleRule::MaxAge(30),
      LifecycleRule::CreatedBefore(absl::CivilDay(2020, 7, 26)),
      LifecycleRule::IsLive(false),
      LifecycleRule::MatchesStorageClassArchive(),
      LifecycleRule::MatchesStorageClasses(
          {storage_class::Standard(), storage_class::Nearline()}),
      LifecycleRule::MatchesStorageClassStandard(),
      // Skip this one because it requires creating a regional bucket (the
      // default is multi-regional US), and that felt like too much of a hassle
      //   LifecycleRule::MatchesStorageClassRegional(),
      LifecycleRule::MatchesStorageClassMultiRegional(),
      LifecycleRule::MatchesStorageClassNearline(),
      LifecycleRule::MatchesStorageClassColdline(),
      LifecycleRule::MatchesStorageClassDurableReducedAvailability(),
  };

  StatusOr<Client> client = MakeBucketIntegrationTestClient();
  ASSERT_STATUS_OK(client);
  std::string bucket_name = MakeRandomBucketName();

  auto original = client->CreateBucketForProject(bucket_name, project_id_,
                                                 BucketMetadata{});
  ASSERT_STATUS_OK(original);
  EXPECT_EQ(bucket_name, original->name());

  for (auto const& condition : test_values) {
    std::ostringstream os;
    os << "testing with " << condition;
    auto const str = std::move(os).str();
    SCOPED_TRACE(str);

    auto updated = client->PatchBucket(
        bucket_name, BucketMetadataPatchBuilder{}.SetLifecycle({{LifecycleRule{
                         condition, LifecycleRule::Delete()}}}));
    // We do not use an ASSERT_STATUS_OK() here because we want to continue and
    // delete the temporary bucket.
    EXPECT_STATUS_OK(updated);
    if (updated) {
      EXPECT_TRUE(updated->has_lifecycle()) << "updated = " << *updated;
    }
  }

  auto status = client->DeleteBucket(bucket_name);
  ASSERT_STATUS_OK(status);
}

TEST_F(BucketIntegrationTest, FullPatch) {
  std::string bucket_name = MakeRandomBucketName();
  StatusOr<Client> client = MakeBucketIntegrationTestClient();
  ASSERT_STATUS_OK(client);

  // We need to have an available bucket for logging ...
  std::string logging_name = MakeRandomBucketName();
  StatusOr<BucketMetadata> const logging_meta = client->CreateBucketForProject(
      logging_name, project_id_, BucketMetadata(), PredefinedAcl("private"),
      PredefinedDefaultObjectAcl("projectPrivate"), Projection("noAcl"));
  ASSERT_STATUS_OK(logging_meta);
  EXPECT_EQ(logging_name, logging_meta->name());

  // Wait at least 2 seconds before trying to create / delete another bucket.
  if (!UsingEmulator()) std::this_thread::sleep_for(std::chrono::seconds(2));
  // Create a Bucket, use the default settings for most fields, except the
  // storage class and location. Fetch the full attributes of the bucket.
  StatusOr<BucketMetadata> const insert_meta = client->CreateBucketForProject(
      bucket_name, project_id_,
      BucketMetadata().set_location("US").set_storage_class(
          storage_class::Standard()),
      PredefinedAcl("private"), PredefinedDefaultObjectAcl("projectPrivate"),
      Projection("full"));
  ASSERT_STATUS_OK(insert_meta);
  EXPECT_EQ(bucket_name, insert_meta->name());

  // Patch every possible field in the metadata, to verify they work.
  BucketMetadata desired_state = *insert_meta;
  // acl()
  desired_state.mutable_acl().push_back(BucketAccessControl()
                                            .set_entity("allAuthenticatedUsers")
                                            .set_role("READER"));

  // billing()
  if (!desired_state.has_billing()) {
    desired_state.set_billing(BucketBilling(false));
  } else {
    desired_state.set_billing(
        BucketBilling(!desired_state.billing().requester_pays));
  }

  // cors()
  desired_state.mutable_cors().push_back(CorsEntry{86400, {"GET"}, {}, {}});

  // default_acl()
  desired_state.mutable_default_acl().push_back(
      ObjectAccessControl()
          .set_entity("allAuthenticatedUsers")
          .set_role("READER"));

  // encryption()
  // TODO(#1003) - need a valid KMS entry to set the encryption.

  // iam_configuration() - skipped, cannot set both ACL and iam_configuration in
  // the same bucket.

  // labels()
  desired_state.mutable_labels().emplace("test-label", "testing-full-patch");

  // lifecycle()
  LifecycleRule rule(LifecycleRule::ConditionConjunction(
                         LifecycleRule::MaxAge(30),
                         LifecycleRule::MatchesStorageClassStandard()),
                     LifecycleRule::Delete());
  desired_state.set_lifecycle(BucketLifecycle{{rule}});

  // logging()
  if (desired_state.has_logging()) {
    desired_state.reset_logging();
  } else {
    desired_state.set_logging(BucketLogging{logging_name, "test-log"});
  }

  // storage_class()
  desired_state.set_storage_class(storage_class::Coldline());

  // versioning()
  if (!desired_state.has_versioning()) {
    desired_state.enable_versioning();
  } else {
    desired_state.reset_versioning();
  }

  // website()
  if (desired_state.has_website()) {
    desired_state.reset_website();
  } else {
    desired_state.set_website(BucketWebsite{"index.html", "404.html"});
  }

  StatusOr<BucketMetadata> patched =
      client->PatchBucket(bucket_name, *insert_meta, desired_state);
  ASSERT_STATUS_OK(patched);
  // acl() - cannot compare for equality because many fields are updated with
  // unknown values (entity_id, etag, etc)
  EXPECT_THAT(AclEntityNames(patched->acl()),
              ContainsOnce("allAuthenticatedUsers"));

  // billing()
  EXPECT_EQ(desired_state.billing_as_optional(),
            patched->billing_as_optional());

  // cors()
  EXPECT_EQ(desired_state.cors(), patched->cors());

  // default_acl() - cannot compare for equality because many fields are updated
  // with unknown values (entity_id, etag, etc)
  EXPECT_THAT(AclEntityNames(patched->default_acl()),
              ContainsOnce("allAuthenticatedUsers"));

  // encryption() - TODO(#1003) - verify the key was correctly used.

  // lifecycle()
  EXPECT_EQ(desired_state.lifecycle_as_optional(),
            patched->lifecycle_as_optional());

  // location()
  EXPECT_EQ(desired_state.location(), patched->location());

  // logging()
  EXPECT_EQ(desired_state.logging_as_optional(),
            patched->logging_as_optional());

  // storage_class()
  EXPECT_EQ(desired_state.storage_class(), patched->storage_class());

  // versioning()
  EXPECT_EQ(desired_state.versioning(), patched->versioning());

  // website()
  EXPECT_EQ(desired_state.website_as_optional(),
            patched->website_as_optional());

  auto status = client->DeleteBucket(bucket_name);
  ASSERT_STATUS_OK(status);
  // Wait at least 2 seconds before trying to create / delete another bucket.
  if (!UsingEmulator()) std::this_thread::sleep_for(std::chrono::seconds(2));
  status = client->DeleteBucket(logging_name);
  ASSERT_STATUS_OK(status);
}

// @test Verify that we can set the iam_configuration() in a Bucket.
TEST_F(BucketIntegrationTest, UniformBucketLevelAccessPatch) {
  std::string bucket_name = MakeRandomBucketName();
  StatusOr<Client> client = MakeIntegrationTestClient();
  ASSERT_STATUS_OK(client);

  // Create a Bucket, use the default settings for all fields. Fetch the full
  // attributes of the bucket.
  StatusOr<BucketMetadata> const insert_meta = client->CreateBucketForProject(
      bucket_name, project_id_, BucketMetadata(), PredefinedAcl("private"),
      PredefinedDefaultObjectAcl("projectPrivate"), Projection("full"));
  ASSERT_STATUS_OK(insert_meta);
  EXPECT_EQ(bucket_name, insert_meta->name());

  // Patch the iam_configuration().
  BucketMetadata desired_state = *insert_meta;
  BucketIamConfiguration iam_configuration;
  iam_configuration.uniform_bucket_level_access =
      UniformBucketLevelAccess{true, {}};
  desired_state.set_iam_configuration(std::move(iam_configuration));

  StatusOr<BucketMetadata> patched =
      client->PatchBucket(bucket_name, *insert_meta, desired_state);
  ASSERT_STATUS_OK(patched);

  ASSERT_TRUE(patched->has_iam_configuration()) << "patched=" << *patched;
  ASSERT_TRUE(patched->iam_configuration().uniform_bucket_level_access)
      << "patched=" << *patched;

  auto status = client->DeleteBucket(bucket_name);
  ASSERT_STATUS_OK(status);
}

// @test Verify that we can set the iam_configuration() in a Bucket.
TEST_F(BucketIntegrationTest, PublicAccessPreventionPatch) {
  std::string bucket_name = MakeRandomBucketName();
  StatusOr<Client> client = MakeIntegrationTestClient();
  ASSERT_STATUS_OK(client);

  // Create a Bucket, use the default settings for all fields. Fetch the full
  // attributes of the bucket.
  StatusOr<BucketMetadata> const insert_meta = client->CreateBucketForProject(
      bucket_name, project_id_, BucketMetadata(), PredefinedAcl("private"),
      PredefinedDefaultObjectAcl("projectPrivate"), Projection("full"));
  ASSERT_STATUS_OK(insert_meta);
  EXPECT_EQ(bucket_name, insert_meta->name());
{'fraction_non_alphanumeric': 0.11985991866244916, 'fraction_numerical': 0.0035585178490736558, 'mean_word_length': 4.365, 'pattern_counts': {'":': 0, '<': 99, '<?xml version=': 0, '>': 153, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5826674', 'n_tokens_mistral': 10486, 'n_tokens_neox': 10203, 'n_words': 2068}