| // Copyright 2026 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 | |
| // | |
| // https://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 "jumbf/uri.h" | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/base/no_destructor.h" | |
| #include "absl/container/flat_hash_map.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "jumbf/box.h" | |
| #include "uuid/uuid.h" | |
| namespace jumbf { | |
| namespace { | |
| using ::absl_testing::IsOkAndHolds; | |
| using ::absl_testing::StatusIs; | |
| using ::testing::Eq; | |
| constexpr absl::string_view kUuid1 = "DEADBEEF-F00D-BABE-B4DF-000000000001"; | |
| DescriptionBox Desc(std::optional<absl::string_view> label) { | |
| return DescriptionBox{ | |
| .type_uuid = credentio::Uuid::FromStringOrDie(kUuid1), | |
| .label = std::move(label), | |
| }; | |
| } | |
| // Concise representation of a SuperBox tree for testing. | |
| struct TestSuperBox { | |
| std::optional<absl::string_view> label; | |
| std::vector<TestSuperBox> children; | |
| }; | |
| // Creates a payload based on the provided label, backed by a singleton map. | |
| // Not thread-safe. | |
| absl::string_view TestPayload(std::optional<absl::string_view> label) { | |
| static absl::NoDestructor< | |
| absl::flat_hash_map<std::string, std::unique_ptr<std::string>>> | |
| payloads; | |
| std::string label_str = std::string(label.value_or("")); | |
| if (auto it = payloads->find(label_str); it != payloads->end()) { | |
| return *it->second; | |
| } | |
| std::string payload = absl::StrCat(label_str, "-payload"); | |
| auto [it, _] = payloads->insert( | |
| std::make_pair(label_str, std::make_unique<std::string>(payload))); | |
| return *it->second; | |
| } | |
| SuperBox MakeSuperBox(const TestSuperBox& test_box) { | |
| SuperBox super_box = {.description = Desc(test_box.label)}; | |
| if (test_box.children.empty()) { | |
| super_box.contents.push_back( | |
| ContentBox{.payload = CborBox{.payload = TestPayload(test_box.label)}}); | |
| } | |
| for (const TestSuperBox& child : test_box.children) { | |
| super_box.contents.push_back(ContentBox{.payload = MakeSuperBox(child)}); | |
| } | |
| return super_box; | |
| } | |
| absl::StatusOr<const SuperBox*> Get(const UriResolver& resolver, | |
| absl::string_view uri, | |
| absl::string_view current_path) { | |
| ABSL_ASSIGN_OR_RETURN(auto absolute_path, | |
| resolver.GetAbsolutePathFromUri(uri, current_path)); | |
| return resolver.ResolvePath(absolute_path); | |
| } | |
| // Predicate matcher for a SuperBox* with a single CBOR content box. | |
| MATCHER_P(HasCborPayload, value, "") { | |
| return arg->contents.size() == 1 && | |
| std::holds_alternative<CborBox>(arg->contents[0].payload) && | |
| std::get<CborBox>(arg->contents[0].payload).payload == value; | |
| } | |
| TEST(UriTest, RelativePaths) { | |
| SuperBox root_box = MakeSuperBox(TestSuperBox{ | |
| .label = "label1", | |
| .children = {TestSuperBox{ | |
| .label = "label2", | |
| .children = {TestSuperBox{.label = std::nullopt}, | |
| TestSuperBox{.label = "label3"}, | |
| TestSuperBox{.label = "label4"}, | |
| TestSuperBox{ | |
| .label = "label5", | |
| .children = {TestSuperBox{.label = "label6"}}, | |
| }}}}}); | |
| UriResolver resolver = UriResolver::WithSingleRootChild(&root_box); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=label3", "/label1/label2"), | |
| IsOkAndHolds(HasCborPayload("label3-payload"))); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=label4", "/label1/label2"), | |
| IsOkAndHolds(HasCborPayload("label4-payload"))); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=label5/label6", "/label1/label2"), | |
| IsOkAndHolds(HasCborPayload("label6-payload"))); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=label2", "/label1/label2"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=label4/label5", "/label1/label2"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=label5/label6/", "/label1/label2"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| EXPECT_THAT( | |
| Get(resolver, "self#jumbf=label5/label6/label7", "/label1/label2"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| } | |
| TEST(UriTest, AbsolutePaths) { | |
| SuperBox root_box = MakeSuperBox( | |
| TestSuperBox{.label = "label1", | |
| .children = {TestSuperBox{ | |
| .label = "label2", | |
| .children = {TestSuperBox{.label = std::nullopt}, | |
| TestSuperBox{.label = "label3"}, | |
| TestSuperBox{.label = "label4"}}}}}); | |
| UriResolver resolver = UriResolver::WithSingleRootChild(&root_box); | |
| EXPECT_THAT( | |
| Get(resolver, "self#jumbf=/label1/label2/label3", "/label1/label2"), | |
| IsOkAndHolds(HasCborPayload("label3-payload"))); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=/label1/label2/label4", | |
| "/label1/label2/label3"), | |
| IsOkAndHolds(HasCborPayload("label4-payload"))); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=/", "/label1/label2"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=/label3", "/label1/label2"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=/label1/label2/label3/label5", | |
| "/label1/label2"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| } | |
| TEST(UriTest, EmptyPathNotFound) { | |
| SuperBox root_box = MakeSuperBox( | |
| TestSuperBox{.label = "label1", | |
| .children = {TestSuperBox{ | |
| .label = "label2", | |
| .children = {TestSuperBox{.label = "label3"}, | |
| TestSuperBox{.label = "label4"}}}}}); | |
| UriResolver resolver = UriResolver::WithSingleRootChild(&root_box); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=", "/label1/label2"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| } | |
| TEST(UriTest, AbsolutePathWrongRootPrefix) { | |
| SuperBox root_box = MakeSuperBox( | |
| TestSuperBox{.label = "label1", | |
| .children = {TestSuperBox{ | |
| .label = "label2", | |
| .children = {TestSuperBox{.label = "label3"}, | |
| TestSuperBox{.label = "label4"}}}}}); | |
| UriResolver resolver = UriResolver::WithSingleRootChild(&root_box); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=/somethingelse/label2/label3", | |
| "/label1/label2"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| } | |
| // No self#jumbf= prefix. | |
| TEST(UriTest, NoSelfPrefix) { | |
| SuperBox root_box = MakeSuperBox( | |
| TestSuperBox{.label = "label1", | |
| .children = {TestSuperBox{ | |
| .label = "label2", | |
| .children = {TestSuperBox{.label = "label3"}, | |
| TestSuperBox{.label = "label4"}}}}}); | |
| UriResolver resolver = UriResolver::WithSingleRootChild(&root_box); | |
| EXPECT_THAT(Get(resolver, "/label1/label2/label3", "/label1/label2"), | |
| StatusIs(absl::StatusCode::kInvalidArgument)); | |
| } | |
| TEST(UriTest, DotPathComponentsNotSupported) { | |
| SuperBox root_box = MakeSuperBox( | |
| TestSuperBox{.label = "label1", | |
| .children = {TestSuperBox{ | |
| .label = "label2", | |
| .children = {TestSuperBox{.label = "label3"}, | |
| TestSuperBox{.label = "label4"}}}}}); | |
| UriResolver resolver = UriResolver::WithSingleRootChild(&root_box); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=/label1/label2/label3/../label4", | |
| "/label1/label2"), | |
| StatusIs(absl::StatusCode::kInvalidArgument)); | |
| EXPECT_THAT(Get(resolver, "self#jumbf=../label1/label2/label3/label4", | |
| "/label1/label2"), | |
| StatusIs(absl::StatusCode::kInvalidArgument)); | |
| EXPECT_THAT( | |
| Get(resolver, "self#jumbf=/label1/label2/./label3", "/label1/label2"), | |
| StatusIs(absl::StatusCode::kInvalidArgument)); | |
| } | |
| TEST(ResolvePathTest, AbsolutePaths) { | |
| SuperBox root_box = MakeSuperBox( | |
| TestSuperBox{.label = "label1", | |
| .children = {TestSuperBox{ | |
| .label = "label2", | |
| .children = {TestSuperBox{.label = std::nullopt}, | |
| TestSuperBox{.label = "label3"}, | |
| TestSuperBox{.label = "label4"}}}}}); | |
| UriResolver resolver = UriResolver::WithSingleRootChild(&root_box); | |
| EXPECT_THAT(resolver.ResolvePath("/label1/label2/label3"), | |
| IsOkAndHolds(HasCborPayload("label3-payload"))); | |
| EXPECT_THAT(resolver.ResolvePath("/label1/label2/label4"), | |
| IsOkAndHolds(HasCborPayload("label4-payload"))); | |
| EXPECT_THAT(resolver.ResolvePath("/"), StatusIs(absl::StatusCode::kNotFound)); | |
| EXPECT_THAT(resolver.ResolvePath("/label3"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| EXPECT_THAT(resolver.ResolvePath("/label1/label2/label3/label5"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| } | |
| TEST(ResolvePathTest, EmptyPathNotFound) { | |
| SuperBox root_box = MakeSuperBox( | |
| TestSuperBox{.label = "label1", | |
| .children = {TestSuperBox{ | |
| .label = "label2", | |
| .children = {TestSuperBox{.label = "label3"}, | |
| TestSuperBox{.label = "label4"}}}}}); | |
| UriResolver resolver = UriResolver::WithSingleRootChild(&root_box); | |
| EXPECT_THAT(resolver.ResolvePath(""), StatusIs(absl::StatusCode::kNotFound)); | |
| } | |
| TEST(ResolvePathTest, AbsolutePathWrongRootPrefix) { | |
| SuperBox root_box = MakeSuperBox( | |
| TestSuperBox{.label = "label1", | |
| .children = {TestSuperBox{ | |
| .label = "label2", | |
| .children = {TestSuperBox{.label = "label3"}, | |
| TestSuperBox{.label = "label4"}}}}}); | |
| UriResolver resolver = UriResolver::WithSingleRootChild(&root_box); | |
| EXPECT_THAT(resolver.ResolvePath("/somethingelse/label2/label3"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| EXPECT_THAT(resolver.ResolvePath("self#jumbf=/label1/label2/label3"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| EXPECT_THAT(resolver.ResolvePath("label1/label2/label3"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| } | |
| TEST(ResolvePathTest, DotPathComponentsNotSupported) { | |
| SuperBox root_box = MakeSuperBox( | |
| TestSuperBox{.label = "label1", | |
| .children = {TestSuperBox{ | |
| .label = "label2", | |
| .children = {TestSuperBox{.label = "label3"}, | |
| TestSuperBox{.label = "label4"}}}}}); | |
| UriResolver resolver = UriResolver::WithSingleRootChild(&root_box); | |
| EXPECT_THAT(resolver.ResolvePath("/label1/label2/label3/../label4"), | |
| StatusIs(absl::StatusCode::kInvalidArgument)); | |
| EXPECT_THAT(resolver.ResolvePath("../label1/label2/label3/label4"), | |
| StatusIs(absl::StatusCode::kNotFound)); | |
| EXPECT_THAT(resolver.ResolvePath("/label1/label2/./label3"), | |
| StatusIs(absl::StatusCode::kInvalidArgument)); | |
| } | |
| TEST(GetAbsolutePath, RelativePaths) { | |
| EXPECT_THAT(UriResolver::GetAbsolutePath("label3", "/label1/label2"), | |
| Eq("/label1/label2/label3")); | |
| EXPECT_THAT(UriResolver::GetAbsolutePath("label3/label4", "/label1/label2"), | |
| Eq("/label1/label2/label3/label4")); | |
| } | |
| TEST(GetAbsolutePath, AbsolutePaths) { | |
| EXPECT_THAT(UriResolver::GetAbsolutePath("/label3", "/label1/label2"), | |
| Eq("/label3")); | |
| EXPECT_THAT(UriResolver::GetAbsolutePath("/label3/label4", "/label1/label2"), | |
| Eq("/label3/label4")); | |
| } | |
| TEST(GetAbsolutePathFromUri, RelativePaths) { | |
| EXPECT_THAT(UriResolver::GetAbsolutePathFromUri("self#jumbf=label3", | |
| "/label1/label2"), | |
| IsOkAndHolds("/label1/label2/label3")); | |
| EXPECT_THAT(UriResolver::GetAbsolutePathFromUri("self#jumbf=label3/label4", | |
| "/label1/label2"), | |
| IsOkAndHolds("/label1/label2/label3/label4")); | |
| } | |
| TEST(GetAbsolutePathFromUri, AbsolutePaths) { | |
| EXPECT_THAT(UriResolver::GetAbsolutePathFromUri("self#jumbf=/label3", | |
| "/label1/label2"), | |
| IsOkAndHolds("/label3")); | |
| EXPECT_THAT(UriResolver::GetAbsolutePathFromUri("self#jumbf=/label3/label4", | |
| "/label1/label2"), | |
| IsOkAndHolds("/label3/label4")); | |
| } | |
| TEST(GetAbsolutePathFromUri, WrongPrefix) { | |
| EXPECT_THAT(UriResolver::GetAbsolutePathFromUri("/label3", "/label1/label2"), | |
| StatusIs(absl::StatusCode::kInvalidArgument)); | |
| EXPECT_THAT(UriResolver::GetAbsolutePathFromUri("https://google.com/label3", | |
| "/label1/label2"), | |
| StatusIs(absl::StatusCode::kInvalidArgument)); | |
| } | |
| } // namespace | |
| } // namespace jumbf |