blob: 09b1bb4ba47459087b63b2dd2971a5b091ee9869 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 "paimon/fs/s3/s3_file_system.h"
#include <gtest/gtest.h>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <optional>
#include <utility>
#include <vector>
#include "paimon/common/utils/string_utils.h"
#include "paimon/fs/s3/s3_file_system_factory.h"
#include "paimon/testing/utils/testharness.h"
namespace paimon::s3 {
namespace {
class MockHttpClient : public HttpClient {
public:
Result<HttpResponse> Execute(const HttpRequest& request,
const HttpBodyConsumer& consumer) const override {
request_ = request;
HttpResponse response;
response.status_code = status_code_;
response.headers = response_headers_;
if (!body_.empty()) {
PAIMON_RETURN_NOT_OK(consumer(body_.data(), body_.size()));
response.body_size = body_.size();
}
return response;
}
mutable HttpRequest request_;
int32_t status_code_ = 200;
HttpHeaders response_headers_;
std::string body_;
};
class ScopedEnvironmentVariable {
public:
ScopedEnvironmentVariable(const char* name, std::optional<std::string> value) : name_(name) {
const char* previous = std::getenv(name);
if (previous != nullptr) {
previous_ = previous;
}
if (value) {
setenv(name, value->c_str(), 1);
} else {
unsetenv(name);
}
}
~ScopedEnvironmentVariable() {
if (previous_) {
setenv(name_.c_str(), previous_->c_str(), 1);
} else {
unsetenv(name_.c_str());
}
}
private:
std::string name_;
std::optional<std::string> previous_;
};
std::map<std::string, std::string> StaticOptions() {
return {{kS3AccessKeyOption, "access"},
{kS3SecretKeyOption, "secret"},
{kS3SessionTokenOption, "token"},
{kS3RegionOption, "ap-northeast-2"}};
}
const std::string* FindHeader(const HttpHeaders& headers, const std::string& name) {
std::string normalized_name = StringUtils::ToLowerCase(name);
for (const auto& [header_name, value] : headers) {
if (StringUtils::ToLowerCase(header_name) == normalized_name) {
return &value;
}
}
return nullptr;
}
TEST(S3ObjectStoreClientTest, TestHeadAndSigning) {
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(StaticOptions(), http));
ASSERT_OK_AND_ASSIGN(auto metadata, client->HeadObject({"bucket", "a b/file"}));
ASSERT_EQ(metadata.size, 12);
ASSERT_EQ(http->request_.url, "https://bucket.s3.ap-northeast-2.amazonaws.com/a%20b/file");
const std::string* authorization = FindHeader(http->request_.headers, "authorization");
ASSERT_NE(authorization, nullptr);
ASSERT_NE(authorization->find("/ap-northeast-2/s3/aws4_request"), std::string::npos);
ASSERT_NE(authorization->find(
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token"),
std::string::npos);
ASSERT_EQ(http->request_.headers["host"], "bucket.s3.ap-northeast-2.amazonaws.com");
const std::string* session_token = FindHeader(http->request_.headers, "x-amz-security-token");
ASSERT_NE(session_token, nullptr);
ASSERT_EQ(*session_token, "token");
}
TEST(S3ObjectStoreClientTest, TestDottedBucketUsesPathStyleForDefaultEndpoint) {
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(StaticOptions(), http));
ASSERT_OK(client->HeadObject({"paimon.prod.data", "file"}));
ASSERT_EQ(http->request_.url, "https://s3.ap-northeast-2.amazonaws.com/paimon.prod.data/file");
ASSERT_EQ(http->request_.headers["host"], "s3.ap-northeast-2.amazonaws.com");
auto path_style_options = StaticOptions();
path_style_options[kS3PathStyleAccessOption] = "false";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> path_style_client,
MakeS3ObjectStoreClient(path_style_options, http));
ASSERT_OK(path_style_client->HeadObject({"paimon.prod.data", "file"}));
ASSERT_EQ(http->request_.url, "https://s3.ap-northeast-2.amazonaws.com/paimon.prod.data/file");
}
TEST(S3ObjectStoreClientTest, TestCustomEndpointAddressing) {
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
auto https_options = StaticOptions();
https_options[kS3EndpointOption] = "https://s3.example.com";
https_options[kS3PathStyleAccessOption] = "false";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> https_client,
MakeS3ObjectStoreClient(https_options, http));
ASSERT_OK(https_client->HeadObject({"bucket", "file"}));
ASSERT_EQ(http->request_.url, "https://s3.example.com/bucket/file");
ASSERT_OK(https_client->HeadObject({"paimon.prod.data", "file"}));
ASSERT_EQ(http->request_.url, "https://s3.example.com/paimon.prod.data/file");
auto http_options = StaticOptions();
http_options[kS3EndpointOption] = "http://s3.example.com";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> http_client,
MakeS3ObjectStoreClient(http_options, http));
ASSERT_OK(http_client->HeadObject({"paimon.prod.data", "file"}));
ASSERT_EQ(http->request_.url, "http://paimon.prod.data.s3.example.com/file");
auto ip_options = StaticOptions();
ip_options[kS3EndpointOption] = "http://127.0.0.1:9000";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> ip_client,
MakeS3ObjectStoreClient(ip_options, http));
ASSERT_OK(ip_client->HeadObject({"bucket", "file"}));
ASSERT_EQ(http->request_.url, "http://127.0.0.1:9000/bucket/file");
auto base_path_options = StaticOptions();
base_path_options[kS3EndpointOption] = "HTTPS://s3.example.com/storage";
base_path_options[kS3PathStyleAccessOption] = "true";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> base_path_client,
MakeS3ObjectStoreClient(base_path_options, http));
ASSERT_OK(base_path_client->HeadObject({"bucket", "file"}));
ASSERT_EQ(http->request_.url, "https://s3.example.com/storage/bucket/file");
}
TEST(S3ObjectStoreClientTest, TestInvalidCustomEndpoint) {
for (const char* endpoint :
{"ftp://s3.example.com", "https://user@s3.example.com", "https://s3.example.com?query",
"https://s3.example.com#fragment"}) {
auto options = StaticOptions();
options[kS3EndpointOption] = endpoint;
ASSERT_NOK(MakeS3ObjectStoreClient(options, std::make_shared<MockHttpClient>()));
}
}
TEST(S3ObjectStoreClientTest, TestNonVirtualHostableBucketUsesPathStyle) {
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(StaticOptions(), http));
ASSERT_OK(client->HeadObject({"aa", "file"}));
ASSERT_EQ(http->request_.url, "https://s3.ap-northeast-2.amazonaws.com/aa/file");
auto options = StaticOptions();
options[kS3EndpointOption] = "http://s3.example.com";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> custom_client,
MakeS3ObjectStoreClient(options, http));
ASSERT_OK(custom_client->HeadObject({"BucketName", "file"}));
ASSERT_EQ(http->request_.url, "http://s3.example.com/BucketName/file");
}
TEST(S3ObjectStoreClientTest, TestPathStyleOptionUsesCommonBooleanParser) {
for (const auto& [value, path_style] :
std::vector<std::pair<std::string, bool>>{{"t", true},
{"y", true},
{"yes", true},
{"1", true},
{"f", false},
{"n", false},
{"no", false},
{"0", false}}) {
auto options = StaticOptions();
options[kS3PathStyleAccessOption] = value;
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(options, http));
ASSERT_OK(client->HeadObject({"bucket", "file"}));
ASSERT_EQ(http->request_.url, path_style
? "https://s3.ap-northeast-2.amazonaws.com/bucket/file"
: "https://bucket.s3.ap-northeast-2.amazonaws.com/file");
}
}
TEST(S3ObjectStoreClientTest, TestOptionAliases) {
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
std::map<std::string, std::string> python_options{
{"fs.s3.accessKeyId", "python-access"}, {"fs.s3.accessKeySecret", "python-secret"},
{"fs.s3.securityToken", "python-token"}, {"fs.s3.endpoint", "http://s3.example.com"},
{"fs.s3.region", "us-west-2"}, {"fs.s3.path.style.access", "true"}};
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> python_client,
MakeS3ObjectStoreClient(python_options, http));
ASSERT_OK(python_client->HeadObject({"bucket", "file"}));
ASSERT_EQ(http->request_.url, "http://s3.example.com/bucket/file");
const std::string* authorization = FindHeader(http->request_.headers, "authorization");
ASSERT_NE(authorization, nullptr);
ASSERT_NE(authorization->find("Credential=python-access/"), std::string::npos);
const std::string* token = FindHeader(http->request_.headers, "x-amz-security-token");
ASSERT_NE(token, nullptr);
ASSERT_EQ(*token, "python-token");
std::map<std::string, std::string> java_options{{"s3a.access.key", "java-access"},
{"s3a.secret.key", "java-secret"},
{"s3a.session.token", "java-token"},
{"s3a.region", "eu-west-1"}};
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> java_client,
MakeS3ObjectStoreClient(java_options, http));
ASSERT_OK(java_client->HeadObject({"bucket", "file"}));
ASSERT_EQ(http->request_.url, "https://bucket.s3.eu-west-1.amazonaws.com/file");
authorization = FindHeader(http->request_.headers, "authorization");
ASSERT_NE(authorization, nullptr);
ASSERT_NE(authorization->find("Credential=java-access/"), std::string::npos);
token = FindHeader(http->request_.headers, "x-amz-security-token");
ASSERT_NE(token, nullptr);
ASSERT_EQ(*token, "java-token");
}
TEST(S3ObjectStoreClientTest, TestCanonicalOptionsTakePrecedenceOverAliases) {
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
std::map<std::string, std::string> options{{kS3AccessKeyOption, "canonical-access"},
{kS3SecretKeyOption, "canonical-secret"},
{"s3a.access.key", "alias-access"},
{"s3a.secret.key", "alias-secret"},
{kS3RegionOption, "ap-northeast-2"}};
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(options, http));
ASSERT_OK(client->HeadObject({"bucket", "file"}));
const std::string* authorization = FindHeader(http->request_.headers, "authorization");
ASSERT_NE(authorization, nullptr);
ASSERT_NE(authorization->find("Credential=canonical-access/"), std::string::npos);
}
TEST(S3ObjectStoreClientTest, TestInvalidContentLength) {
for (const std::string content_length :
{"", "invalid", "-1", "12abc", "999999999999999999999999"}) {
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = content_length;
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(StaticOptions(), http));
ASSERT_TRUE(client->HeadObject({"bucket", "file"}).status().IsIOError());
}
}
TEST(S3ObjectStoreClientTest, TestInvalidModificationTime) {
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
http->response_headers_["last-modified"] = "invalid";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(StaticOptions(), http));
ASSERT_OK_AND_ASSIGN(auto metadata, client->HeadObject({"bucket", "file"}));
ASSERT_EQ(metadata.modification_time, 0);
http->body_ =
"<ListBucketResult><IsTruncated>false</IsTruncated><Contents><Key>file</Key>"
"<LastModified>invalid</LastModified><Size>12</Size></Contents></ListBucketResult>";
ASSERT_OK_AND_ASSIGN(auto result, client->ListObjects({"bucket", ""}, "", 0));
ASSERT_EQ(result.objects[0].modification_time, 0);
}
TEST(S3ObjectStoreClientTest, TestRegionFromEnvironment) {
ScopedEnvironmentVariable region("AWS_REGION", "eu-west-1");
auto options = StaticOptions();
options.erase(kS3RegionOption);
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(options, http));
ASSERT_OK(client->HeadObject({"bucket", "file"}));
ASSERT_EQ(http->request_.url, "https://bucket.s3.eu-west-1.amazonaws.com/file");
}
TEST(S3ObjectStoreClientTest, TestDefaultEndpointsForAwsPartitions) {
for (const auto& [region, endpoint] : std::vector<std::pair<std::string, std::string>>{
{"ap-northeast-2", "https://bucket.s3.ap-northeast-2.amazonaws.com/file"},
{"cn-north-1", "https://bucket.s3.cn-north-1.amazonaws.com.cn/file"},
{"eusc-de-east-1", "https://bucket.s3.eusc-de-east-1.amazonaws.eu/file"},
{"us-iso-east-1", "https://bucket.s3.us-iso-east-1.c2s.ic.gov/file"},
{"us-isob-east-1", "https://bucket.s3.us-isob-east-1.sc2s.sgov.gov/file"},
{"eu-isoe-west-1", "https://bucket.s3.eu-isoe-west-1.cloud.adc-e.uk/file"},
{"us-isof-south-1", "https://bucket.s3.us-isof-south-1.csp.hci.ic.gov/file"},
{"us-gov-west-1", "https://bucket.s3.us-gov-west-1.amazonaws.com/file"}}) {
auto options = StaticOptions();
options[kS3RegionOption] = region;
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(options, http));
ASSERT_OK(client->HeadObject({"bucket", "file"}));
ASSERT_EQ(http->request_.url, endpoint);
}
}
TEST(S3ObjectStoreClientTest, TestRegionFromProfile) {
auto test_dir = paimon::test::UniqueTestDirectory::Create();
ASSERT_TRUE(test_dir);
std::filesystem::path config_path = std::filesystem::path(test_dir->Str()) / "region-config";
{
std::ofstream config(config_path);
config << "[profile test-profile]\nregion = ap-south-1\n";
}
ScopedEnvironmentVariable region("AWS_REGION", std::nullopt);
ScopedEnvironmentVariable default_region("AWS_DEFAULT_REGION", std::nullopt);
ScopedEnvironmentVariable config_file("AWS_CONFIG_FILE", config_path.string());
auto options = StaticOptions();
options.erase(kS3RegionOption);
options[kS3ProfileOption] = "test-profile";
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(options, http));
ASSERT_OK(client->HeadObject({"bucket", "file"}));
ASSERT_EQ(http->request_.url, "https://bucket.s3.ap-south-1.amazonaws.com/file");
}
TEST(S3ObjectStoreClientTest, TestCredentialsFromEnvironmentProfile) {
auto test_dir = paimon::test::UniqueTestDirectory::Create();
ASSERT_TRUE(test_dir);
std::filesystem::path credentials_path =
std::filesystem::path(test_dir->Str()) / "credentials-config";
{
std::ofstream credentials(credentials_path);
credentials << "[environment-profile]\n"
"aws_access_key_id = profile-access\n"
"aws_secret_access_key = profile-secret\n"
"aws_session_token = profile-token\n"
"[default]\n"
"aws_access_key_id = default-access\n"
"aws_secret_access_key = default-secret\n"
"aws_session_token = default-token\n";
}
ScopedEnvironmentVariable credentials_file("AWS_SHARED_CREDENTIALS_FILE",
credentials_path.string());
ScopedEnvironmentVariable access_key("AWS_ACCESS_KEY_ID", std::nullopt);
ScopedEnvironmentVariable secret_key("AWS_SECRET_ACCESS_KEY", std::nullopt);
ScopedEnvironmentVariable session_token("AWS_SESSION_TOKEN", std::nullopt);
{
ScopedEnvironmentVariable profile("AWS_PROFILE", "environment-profile");
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient({{kS3RegionOption, "ap-northeast-2"}}, http));
ASSERT_OK(client->HeadObject({"bucket", "file"}));
const std::string* authorization = FindHeader(http->request_.headers, "authorization");
ASSERT_NE(authorization, nullptr);
ASSERT_NE(authorization->find("Credential=profile-access/"), std::string::npos);
const std::string* token = FindHeader(http->request_.headers, "x-amz-security-token");
ASSERT_NE(token, nullptr);
ASSERT_EQ(*token, "profile-token");
}
{
ScopedEnvironmentVariable profile("AWS_PROFILE", std::nullopt);
auto http = std::make_shared<MockHttpClient>();
http->response_headers_["content-length"] = "12";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient({{kS3RegionOption, "ap-northeast-2"}}, http));
ASSERT_OK(client->HeadObject({"bucket", "file"}));
const std::string* authorization = FindHeader(http->request_.headers, "authorization");
ASSERT_NE(authorization, nullptr);
ASSERT_NE(authorization->find("Credential=default-access/"), std::string::npos);
const std::string* token = FindHeader(http->request_.headers, "x-amz-security-token");
ASSERT_NE(token, nullptr);
ASSERT_EQ(*token, "default-token");
}
}
TEST(S3ObjectStoreClientTest, TestRangeAndListObjects) {
auto http = std::make_shared<MockHttpClient>();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(StaticOptions(), http));
http->body_ = "data";
char buffer[4];
ASSERT_OK_AND_ASSIGN(auto size, client->GetObjectRange({"bucket", "key"}, 2, 4, buffer));
ASSERT_EQ(size, 4);
ASSERT_EQ(std::string(buffer, sizeof(buffer)), "data");
ASSERT_EQ(http->request_.headers["range"], "bytes=2-5");
http->body_ =
"<ListBucketResult><IsTruncated>true</IsTruncated>"
"<Contents><Key>dir/a&amp;b</Key><LastModified>2026-01-01T00:00:00Z</LastModified>"
"<Size>7</Size></Contents><Contents><Key>dir/a&amp;lt;b</Key><Size>8</Size></Contents>"
"<CommonPrefixes><Prefix>dir/sub/</Prefix></CommonPrefixes>"
"<NextContinuationToken>next token</NextContinuationToken></ListBucketResult>";
ASSERT_OK_AND_ASSIGN(auto result, client->ListObjects({"bucket", "dir/"}, "old token", 10));
ASSERT_TRUE(result.is_truncated);
ASSERT_EQ(result.continuation_token, "next token");
ASSERT_EQ(result.objects[0].key, "dir/a&b");
ASSERT_EQ(result.objects[1].key, "dir/a&lt;b");
ASSERT_EQ(result.common_prefixes[0], "dir/sub/");
ASSERT_NE(http->request_.url.find("amazonaws.com/?list-type=2"), std::string::npos);
ASSERT_NE(http->request_.url.find("encoding-type=url"), std::string::npos);
ASSERT_NE(http->request_.url.find("continuation-token=old%20token"), std::string::npos);
}
TEST(S3ObjectStoreClientTest, TestUrlEncodedListObjects) {
auto http = std::make_shared<MockHttpClient>();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(StaticOptions(), http));
http->body_ =
"<ListBucketResult><IsTruncated>true</IsTruncated>"
"<Contents><Key>dir/a%26b%0D</Key><Size>7</Size></Contents>"
"<CommonPrefixes><Prefix>dir/sub%25/</Prefix></CommonPrefixes>"
"<NextContinuationToken>next%2Ftoken</NextContinuationToken></ListBucketResult>";
ASSERT_OK_AND_ASSIGN(auto result, client->ListObjects({"bucket", "dir/"}, "", 0));
ASSERT_EQ(result.objects[0].key, "dir/a&b\r");
ASSERT_EQ(result.common_prefixes[0], "dir/sub%/");
ASSERT_EQ(result.continuation_token, "next%2Ftoken");
http->body_ = "<ListBucketResult><IsTruncated>false</IsTruncated></ListBucketResult>";
ASSERT_OK(client->ListObjects({"bucket", "dir/"}, result.continuation_token, 0));
ASSERT_NE(http->request_.url.find("continuation-token=next%252Ftoken"), std::string::npos);
http->body_ =
"<ListBucketResult><IsTruncated>false</IsTruncated>"
"<Contents><Key>dir/invalid%2</Key><Size>7</Size></Contents></ListBucketResult>";
ASSERT_TRUE(client->ListObjects({"bucket", "dir/"}, "", 0).status().IsIOError());
}
TEST(S3ObjectStoreClientTest, TestInvalidListObjectsResponse) {
auto http = std::make_shared<MockHttpClient>();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<ObjectStoreClient> client,
MakeS3ObjectStoreClient(StaticOptions(), http));
for (const std::string size : {"", "abc", "-1", "12abc", "9223372036854775808"}) {
http->body_ =
"<ListBucketResult><IsTruncated>false</IsTruncated><Contents><Key>key</Key>"
"<Size>" +
size + "</Size></Contents></ListBucketResult>";
ASSERT_TRUE(client->ListObjects({"bucket", ""}, "", 0).status().IsIOError());
}
http->body_ =
"<ListBucketResult><IsTruncated>false</IsTruncated><Contents><Key>key</Key>"
"<Size>1</Size></ListBucketResult>";
ASSERT_TRUE(client->ListObjects({"bucket", ""}, "", 0).status().IsIOError());
http->body_ =
"<ListBucketResult><Contents><Key>key</Key><Size>1</Size></Contents></ListBucketResult>";
ASSERT_TRUE(client->ListObjects({"bucket", ""}, "", 0).status().IsIOError());
}
TEST(S3FileSystemFactoryTest, TestOptionValidation) {
S3FileSystemFactory factory;
ASSERT_TRUE(
factory.Create("s3://bucket", {{kS3AccessKeyOption, ""}, {kS3SecretKeyOption, "secret"}})
.status()
.IsInvalid());
ASSERT_TRUE(
factory.Create("s3://bucket", {{kS3PathStyleAccessOption, "treu"}}).status().IsInvalid());
ASSERT_TRUE(
factory.Create("s3://bucket", {{kS3PathStyleAccessOption, "on"}}).status().IsInvalid());
ASSERT_TRUE(
factory.Create("s3://bucket", {{kS3SessionTokenOption, "token"}}).status().IsInvalid());
auto http = std::make_shared<MockHttpClient>();
ASSERT_TRUE(
MakeS3ObjectStoreClient({{kS3AccessKeyOption, "access"}}, http).status().IsInvalid());
ASSERT_TRUE(
MakeS3ObjectStoreClient({{kS3PathStyleAccessOption, "treu"}}, http).status().IsInvalid());
}
} // namespace
} // namespace paimon::s3