blob: 0035afca08edbb23b382afeb5f0c8aa47c570f05 [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 "iceberg/catalog/rest/rest_util.h"
#include <string>
#include <gtest/gtest.h>
#include "iceberg/catalog/rest/catalog_properties.h"
#include "iceberg/catalog/rest/endpoint.h"
#include "iceberg/catalog/rest/resource_paths.h"
#include "iceberg/table_identifier.h"
#include "iceberg/test/matchers.h"
namespace iceberg::rest {
TEST(RestUtilTest, TrimTrailingSlash) {
EXPECT_EQ(TrimTrailingSlash("https://foo"), "https://foo");
EXPECT_EQ(TrimTrailingSlash("https://foo/"), "https://foo");
EXPECT_EQ(TrimTrailingSlash("https://foo////"), "https://foo");
}
TEST(RestUtilTest, RoundTripUrlEncodeDecodeNamespace) {
// {"dogs"}
EXPECT_THAT(EncodeNamespace(Namespace{.levels = {"dogs"}}),
HasValue(::testing::Eq("dogs")));
EXPECT_THAT(DecodeNamespace("dogs"),
HasValue(::testing::Eq(Namespace{.levels = {"dogs"}})));
// {"dogs.named.hank"}
EXPECT_THAT(EncodeNamespace(Namespace{.levels = {"dogs.named.hank"}}),
HasValue(::testing::Eq("dogs.named.hank")));
EXPECT_THAT(DecodeNamespace("dogs.named.hank"),
HasValue(::testing::Eq(Namespace{.levels = {"dogs.named.hank"}})));
// {"dogs/named/hank"}
EXPECT_THAT(EncodeNamespace(Namespace{.levels = {"dogs/named/hank"}}),
HasValue(::testing::Eq("dogs%2Fnamed%2Fhank")));
EXPECT_THAT(DecodeNamespace("dogs%2Fnamed%2Fhank"),
HasValue(::testing::Eq(Namespace{.levels = {"dogs/named/hank"}})));
// {"dogs", "named", "hank"}
EXPECT_THAT(EncodeNamespace(Namespace{.levels = {"dogs", "named", "hank"}}),
HasValue(::testing::Eq("dogs%1Fnamed%1Fhank")));
EXPECT_THAT(DecodeNamespace("dogs%1Fnamed%1Fhank"),
HasValue(::testing::Eq(Namespace{.levels = {"dogs", "named", "hank"}})));
// {"dogs.and.cats", "named", "hank.or.james-westfall"}
EXPECT_THAT(EncodeNamespace(Namespace{
.levels = {"dogs.and.cats", "named", "hank.or.james-westfall"}}),
HasValue(::testing::Eq("dogs.and.cats%1Fnamed%1Fhank.or.james-westfall")));
EXPECT_THAT(DecodeNamespace("dogs.and.cats%1Fnamed%1Fhank.or.james-westfall"),
HasValue(::testing::Eq(Namespace{
.levels = {"dogs.and.cats", "named", "hank.or.james-westfall"}})));
// empty namespace
EXPECT_THAT(EncodeNamespace(Namespace{.levels = {}}), HasValue(::testing::Eq("")));
EXPECT_THAT(DecodeNamespace(""), HasValue(::testing::Eq(Namespace{.levels = {}})));
}
TEST(RestUtilTest, RoundTripNamespaceWithCustomSeparator) {
Namespace ns{.levels = {"dogs.and.cats", "named", "hank.or.james-westfall"}};
EXPECT_THAT(EncodeNamespace(ns, "%2E"),
HasValue(::testing::Eq("dogs.and.cats%2Enamed%2Ehank.or.james-westfall")));
EXPECT_THAT(DecodeNamespace("dogs.and.cats%2Enamed%2Ehank.or.james-westfall", "%2E"),
HasValue(::testing::Eq(ns)));
}
TEST(RestUtilTest, NamespaceRejectsEmptySeparator) {
auto expect_empty_separator_error = [](const auto& result) {
EXPECT_THAT(result, IsError(ErrorKind::kInvalidArgument));
EXPECT_THAT(result, HasErrorMessage("REST namespace separator cannot be empty"));
};
expect_empty_separator_error(EncodeNamespace(Namespace{.levels = {"dogs"}}, ""));
expect_empty_separator_error(EncodeNamespace(Namespace{.levels = {}}, ""));
expect_empty_separator_error(DecodeNamespace("dogs", ""));
expect_empty_separator_error(DecodeNamespace("", ""));
}
TEST(RestUtilTest, ResourcePathsRejectsEmptyNamespaceSeparator) {
auto result = ResourcePaths::Make("https://catalog.example.com", "", "");
EXPECT_THAT(result, IsError(ErrorKind::kInvalidArgument));
EXPECT_THAT(result, HasErrorMessage("REST namespace separator cannot be empty"));
}
TEST(RestUtilTest, EncodeString) {
// RFC 3986 unreserved characters should not be encoded
EXPECT_THAT(EncodeString("abc123XYZ"), HasValue(::testing::Eq("abc123XYZ")));
EXPECT_THAT(EncodeString("test-file_name.txt~backup"),
HasValue(::testing::Eq("test-file_name.txt~backup")));
// Spaces and special characters should be encoded
EXPECT_THAT(EncodeString("hello world"), HasValue(::testing::Eq("hello%20world")));
EXPECT_THAT(EncodeString("test@example.com"),
HasValue(::testing::Eq("test%40example.com")));
EXPECT_THAT(EncodeString("path/to/file"), HasValue(::testing::Eq("path%2Fto%2Ffile")));
EXPECT_THAT(EncodeString("key=value&foo=bar"),
HasValue(::testing::Eq("key%3Dvalue%26foo%3Dbar")));
EXPECT_THAT(EncodeString("100%"), HasValue(::testing::Eq("100%25")));
EXPECT_THAT(EncodeString("hello\x1Fworld"), HasValue(::testing::Eq("hello%1Fworld")));
EXPECT_THAT(EncodeString(""), HasValue(::testing::Eq("")));
}
TEST(RestUtilTest, DecodeString) {
// Decode percent-encoded strings
EXPECT_THAT(DecodeString("hello%20world"), HasValue(::testing::Eq("hello world")));
EXPECT_THAT(DecodeString("test%40example.com"),
HasValue(::testing::Eq("test@example.com")));
EXPECT_THAT(DecodeString("path%2Fto%2Ffile"), HasValue(::testing::Eq("path/to/file")));
EXPECT_THAT(DecodeString("key%3Dvalue%26foo%3Dbar"),
HasValue(::testing::Eq("key=value&foo=bar")));
EXPECT_THAT(DecodeString("100%25"), HasValue(::testing::Eq("100%")));
// ASCII Unit Separator (0x1F)
EXPECT_THAT(DecodeString("hello%1Fworld"), HasValue(::testing::Eq("hello\x1Fworld")));
// Unreserved characters remain unchanged
EXPECT_THAT(DecodeString("test-file_name.txt~backup"),
HasValue(::testing::Eq("test-file_name.txt~backup")));
EXPECT_THAT(DecodeString(""), HasValue(::testing::Eq("")));
}
TEST(RestUtilTest, EncodeDecodeStringRoundTrip) {
std::vector<std::string> test_cases = {"hello world",
"test@example.com",
"path/to/file",
"key=value&foo=bar",
"100%",
"hello\x1Fworld",
"special!@#$%^&*()chars",
"mixed-123_test.file~ok",
""};
for (const auto& test : test_cases) {
ICEBERG_UNWRAP_OR_FAIL(std::string encoded, EncodeString(test));
ICEBERG_UNWRAP_OR_FAIL(std::string decoded, DecodeString(encoded));
EXPECT_EQ(decoded, test) << "Round-trip failed for: " << test;
}
}
TEST(RestUtilTest, MergeConfigs) {
std::unordered_map<std::string, std::string> server_defaults = {
{"default1", "value1"}, {"default2", "value2"}, {"common", "default_value"}};
std::unordered_map<std::string, std::string> client_configs = {
{"client1", "value1"}, {"common", "client_value"}, {"extra", "client_value"}};
std::unordered_map<std::string, std::string> server_overrides = {
{"override1", "value1"}, {"common", "override_value"}};
auto merged = MergeConfigs(server_defaults, client_configs, server_overrides);
EXPECT_EQ(merged.size(), 6);
// Check precedence: server_overrides > client_configs > server_defaults
EXPECT_EQ(merged["default1"], "value1");
EXPECT_EQ(merged["default2"], "value2");
EXPECT_EQ(merged["client1"], "value1");
EXPECT_EQ(merged["override1"], "value1");
EXPECT_EQ(merged["common"], "override_value");
EXPECT_EQ(merged["extra"], "client_value");
// Test with empty maps
auto merged_empty = MergeConfigs({}, {{"key", "value"}}, {});
EXPECT_EQ(merged_empty.size(), 1);
EXPECT_EQ(merged_empty["key"], "value");
}
struct SnapshotModeValidCase {
std::string input;
SnapshotMode expected;
};
class SnapshotLoadingModeValidTest
: public ::testing::TestWithParam<SnapshotModeValidCase> {};
TEST_P(SnapshotLoadingModeValidTest, ParsesCorrectly) {
auto config = RestCatalogProperties::default_properties();
config.Set(RestCatalogProperties::kSnapshotLoadingMode, GetParam().input);
auto result = config.SnapshotLoadingMode();
ASSERT_THAT(result, IsOk());
EXPECT_EQ(result.value(), GetParam().expected);
}
INSTANTIATE_TEST_SUITE_P(RestCatalogProperties, SnapshotLoadingModeValidTest,
::testing::Values(
// Exact uppercase
SnapshotModeValidCase{"ALL", SnapshotMode::kAll},
SnapshotModeValidCase{"REFS", SnapshotMode::kRefs},
// Lowercase (Java parity: toUpperCase before parsing)
SnapshotModeValidCase{"all", SnapshotMode::kAll},
SnapshotModeValidCase{"refs", SnapshotMode::kRefs},
// Mixed case
SnapshotModeValidCase{"All", SnapshotMode::kAll},
SnapshotModeValidCase{"Refs", SnapshotMode::kRefs}));
class SnapshotLoadingModeInvalidTest : public ::testing::TestWithParam<std::string> {};
TEST_P(SnapshotLoadingModeInvalidTest, ReturnsError) {
auto config = RestCatalogProperties::default_properties();
config.Set(RestCatalogProperties::kSnapshotLoadingMode, GetParam());
EXPECT_THAT(config.SnapshotLoadingMode(), IsError(ErrorKind::kInvalidArgument));
}
INSTANTIATE_TEST_SUITE_P(RestCatalogProperties, SnapshotLoadingModeInvalidTest,
::testing::Values("INVALID", "none", ""));
TEST(RestCatalogPropertiesTest, SnapshotLoadingModeDefaultIsAll) {
auto config = RestCatalogProperties::default_properties();
auto result = config.SnapshotLoadingMode();
ASSERT_THAT(result, IsOk());
EXPECT_EQ(result.value(), SnapshotMode::kAll);
}
} // namespace iceberg::rest