blob: 095c4945bfea45c45350456b50cf10ebb269eb5f [file]
// 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