yams/tests/unit/cli/data_dir_resolution_catch2_test.cpp

587 lines
22 KiB
C++

// CLI Data Directory Resolution tests
// Catch2 migration from GTest (yams-3s4 / yams-cli)
//
// Tests for data directory resolution precedence.
#include <catch2/catch_test_macros.hpp>
#include <yams/cli/yams_cli.h>
#include <yams/config/config_helpers.h>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include "../../common/test_helpers_catch2.h"
#include <yams/compat/unistd.h>
namespace fs = std::filesystem;
namespace {
using ScopedEnv = yams::test::ScopedEnvVar;
/**
* RAII helper to create a temporary config file.
*/
class TempConfig {
public:
explicit TempConfig(const std::string& content) {
auto pid = getpid();
tempDir_ = fs::temp_directory_path() / ("yams_test_" + std::to_string(pid));
fs::create_directories(tempDir_);
configPath_ = tempDir_ / "config.toml";
std::ofstream f(configPath_);
f << content;
}
~TempConfig() {
std::error_code ec;
fs::remove_all(tempDir_, ec);
}
fs::path path() const { return configPath_; }
std::string pathStr() const { return configPath_.string(); }
TempConfig(const TempConfig&) = delete;
TempConfig& operator=(const TempConfig&) = delete;
private:
fs::path tempDir_;
fs::path configPath_;
};
} // namespace
// ============================================================================
// get_data_dir() - Platform default tests
// ============================================================================
TEST_CASE("DataDirResolution - get_data_dir returns non-empty path", "[cli][data_dir][catch2]") {
auto dataDir = yams::config::get_data_dir();
CHECK_FALSE(dataDir.empty());
}
TEST_CASE("DataDirResolution - get_data_dir contains yams", "[cli][data_dir][catch2]") {
auto dataDir = yams::config::get_data_dir();
CHECK((dataDir.string().find("yams") != std::string::npos));
}
#ifndef _WIN32
TEST_CASE("DataDirResolution - get_data_dir respects XDG_DATA_HOME", "[cli][data_dir][catch2]") {
ScopedEnv clearDataDir("YAMS_DATA_DIR", std::nullopt);
ScopedEnv clearStorage("YAMS_STORAGE", std::nullopt);
ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml");
ScopedEnv xdg("XDG_DATA_HOME", "/custom/data");
auto dataDir = yams::config::get_data_dir();
CHECK((dataDir == fs::path("/custom/data/yams")));
}
#else
TEST_CASE("DataDirResolution - get_data_dir respects LOCALAPPDATA", "[cli][data_dir][catch2]") {
const char* localAppData = std::getenv("LOCALAPPDATA");
if (localAppData) {
auto dataDir = yams::config::get_data_dir();
CHECK((dataDir.string().find("yams") != std::string::npos));
}
}
#endif
// ============================================================================
// get_config_dir() - Platform default tests
// ============================================================================
TEST_CASE("DataDirResolution - get_config_dir returns non-empty path", "[cli][data_dir][catch2]") {
auto configDir = yams::config::get_config_dir();
CHECK_FALSE(configDir.empty());
}
TEST_CASE("DataDirResolution - get_config_dir contains yams", "[cli][data_dir][catch2]") {
auto configDir = yams::config::get_config_dir();
CHECK((configDir.string().find("yams") != std::string::npos));
}
// ============================================================================
// resolve_data_dir_from_config() - Precedence tests
// ============================================================================
TEST_CASE("DataDirResolution - legacy YAMS_STORAGE remains supported", "[cli][data_dir][catch2]") {
ScopedEnv storage("YAMS_STORAGE", "/env/storage/path");
ScopedEnv dataDir("YAMS_DATA_DIR", std::nullopt);
ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml");
REQUIRE((std::getenv("YAMS_STORAGE") != nullptr));
CHECK((std::string(std::getenv("YAMS_STORAGE")) == "/env/storage/path"));
auto resolved = yams::config::resolve_data_dir_from_config();
CHECK((resolved == fs::path("/env/storage/path")));
}
TEST_CASE("DataDirResolution - env YAMS_DATA_DIR used if no storage", "[cli][data_dir][catch2]") {
ScopedEnv storage("YAMS_STORAGE", "");
ScopedEnv dataDir("YAMS_DATA_DIR", "/env/data/path");
ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml");
REQUIRE((std::getenv("YAMS_DATA_DIR") != nullptr));
CHECK((std::string(std::getenv("YAMS_DATA_DIR")) == "/env/data/path"));
auto resolved = yams::config::resolve_data_dir_from_config();
CHECK((resolved == fs::path("/env/data/path")));
}
TEST_CASE("DataDirResolution - config data_dir used with YAMS_CONFIG env",
"[cli][data_dir][catch2]") {
TempConfig config(R"(
[core]
data_dir = "/from/config/file"
)");
ScopedEnv storage("YAMS_STORAGE", "");
ScopedEnv dataDir("YAMS_DATA_DIR", "");
ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str());
auto resolved = yams::config::resolve_data_dir_from_config();
CHECK((resolved == fs::path("/from/config/file")));
}
TEST_CASE("DataDirResolution - config takes precedence over env", "[cli][data_dir][catch2]") {
TempConfig config(R"(
[core]
data_dir = "/from/config/file"
)");
ScopedEnv storage("YAMS_STORAGE", "/env/wins");
ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str());
auto resolved = yams::config::resolve_data_dir_from_config();
CHECK((resolved == fs::path("/from/config/file")));
}
TEST_CASE("DataDirResolution - falls back to platform default", "[cli][data_dir][catch2]") {
ScopedEnv storage("YAMS_STORAGE", "");
ScopedEnv dataDir("YAMS_DATA_DIR", "");
ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml");
auto resolved = yams::config::resolve_data_dir_from_config();
CHECK_FALSE(resolved.empty());
CHECK((resolved.string().find("yams") != std::string::npos));
}
TEST_CASE("DataDirResolution - explicit CLI data dir overrides config and env",
"[cli][data_dir][catch2]") {
TempConfig config(R"(
[core]
data_dir = "/from/config/file"
)");
ScopedEnv storage("YAMS_STORAGE", "/env/storage/path");
ScopedEnv dataDir("YAMS_DATA_DIR", "/env/data/path");
ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str());
yams::cli::YamsCLI cli;
std::string arg0 = "yams";
std::string arg1 = "--data-dir";
std::string arg2 = "/from/cli/flag";
std::string arg3 = "completion";
std::string arg4 = "bash";
char* argv[] = {arg0.data(), arg1.data(), arg2.data(), arg3.data(), arg4.data()};
const int rc = cli.run(5, argv);
REQUIRE((rc == 0));
CHECK(cli.hasExplicitDataDir());
CHECK((cli.getDataPath() == fs::path("/from/cli/flag")));
}
TEST_CASE("DataDirResolution - explicit CLI data dir disambiguates conflicting aliases",
"[cli][data_dir][catch2]") {
ScopedEnv storage("YAMS_STORAGE", "/legacy/conflict");
ScopedEnv dataDir("YAMS_DATA_DIR", "/canonical/conflict");
ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml");
yams::cli::YamsCLI cli;
std::string arg0 = "yams";
std::string arg1 = "--data-dir";
std::string arg2 = "/from/explicit/option";
std::string arg3 = "completion";
std::string arg4 = "bash";
char* argv[] = {arg0.data(), arg1.data(), arg2.data(), arg3.data(), arg4.data()};
const int rc = cli.run(5, argv);
REQUIRE((rc == 0));
CHECK(cli.hasExplicitDataDir());
CHECK((cli.getDataPath() == fs::path("/from/explicit/option")));
}
TEST_CASE("DataDirResolution - CLI uses config before env when no explicit override",
"[cli][data_dir][catch2]") {
TempConfig config(R"(
[core]
data_dir = "/from/config/file"
)");
ScopedEnv storage("YAMS_STORAGE", "/env/storage/path");
ScopedEnv dataDir("YAMS_DATA_DIR", "/env/data/path");
ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str());
yams::cli::YamsCLI cli;
std::string arg0 = "yams";
std::string arg1 = "completion";
std::string arg2 = "bash";
char* argv[] = {arg0.data(), arg1.data(), arg2.data()};
const int rc = cli.run(3, argv);
REQUIRE((rc == 0));
CHECK_FALSE(cli.hasExplicitDataDir());
CHECK((cli.getDataPath() == fs::path("/from/config/file")));
}
TEST_CASE("DataDirResolution - daemon pid file uses config when present",
"[cli][data_dir][catch2]") {
TempConfig config(R"(
[daemon]
pid_file = "/tmp/from-config.pid"
)");
ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str());
auto resolved = yams::cli::YamsCLI::resolveConfiguredDaemonPidFilePath();
CHECK((resolved == fs::path("/tmp/from-config.pid")));
}
TEST_CASE("DataDirResolution - daemon pid file falls back when config missing",
"[cli][data_dir][catch2]") {
ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml");
auto resolved = yams::cli::YamsCLI::resolveConfiguredDaemonPidFilePath();
CHECK_FALSE(resolved.empty());
CHECK((resolved.filename().string().find("yams-daemon") != std::string::npos));
}
// ============================================================================
// resolve_runtime_paths() - Typed cross-component policy tests
// ============================================================================
TEST_CASE("RuntimePaths - conflicting data aliases fail without a higher-precedence source",
"[config][runtime_paths][catch2]") {
ScopedEnv storage("YAMS_STORAGE", "/legacy/data");
ScopedEnv dataDir("YAMS_DATA_DIR", "/canonical/data");
ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml");
const auto resolved = yams::config::resolve_runtime_paths();
REQUIRE_FALSE(resolved);
CHECK((resolved.error().code == yams::ErrorCode::InvalidData));
CHECK((resolved.error().message.find("YAMS_STORAGE") != std::string::npos));
CHECK((resolved.error().message.find("YAMS_DATA_DIR") != std::string::npos));
CHECK_THROWS_AS(yams::config::get_data_dir(), std::invalid_argument);
CHECK_THROWS_AS(yams::config::resolve_data_dir_from_config(), std::invalid_argument);
}
TEST_CASE("RuntimePaths - explicit and configured data paths preserve documented precedence",
"[config][runtime_paths][catch2]") {
TempConfig config(R"(
[core]
data_dir = "/from/config/file"
)");
ScopedEnv storage("YAMS_STORAGE", "/legacy/data");
ScopedEnv dataDir("YAMS_DATA_DIR", "/canonical/data");
ScopedEnv configEnv("YAMS_CONFIG", config.pathStr());
yams::config::RuntimePathOverrides overrides;
overrides.dataDir = "/from/explicit/option";
const auto explicitPaths = yams::config::resolve_runtime_paths(overrides);
REQUIRE(explicitPaths);
CHECK((explicitPaths.value().dataDir.value == fs::path("/from/explicit/option")));
CHECK((explicitPaths.value().dataDir.source == yams::config::RuntimePathSource::Explicit));
CHECK_FALSE(explicitPaths.value().diagnostics.empty());
overrides.dataDir.reset();
const auto configuredPaths = yams::config::resolve_runtime_paths(overrides);
REQUIRE(configuredPaths);
CHECK((configuredPaths.value().dataDir.value == fs::path("/from/config/file")));
CHECK((configuredPaths.value().dataDir.source == yams::config::RuntimePathSource::ConfigFile));
CHECK_FALSE(configuredPaths.value().diagnostics.empty());
}
TEST_CASE("RuntimePaths - socket and pid resolve in one config snapshot",
"[config][runtime_paths][catch2]") {
TempConfig config(R"(
[daemon]
socket_path = "/from/config/yams.sock" # inline comment
pid_file = "/from/config/yams.pid" # inline comment
)");
ScopedEnv configEnv("YAMS_CONFIG", config.pathStr());
ScopedEnv socketEnv("YAMS_DAEMON_SOCKET", std::nullopt);
ScopedEnv socketPathEnv("YAMS_DAEMON_SOCKET_PATH", std::nullopt);
ScopedEnv storage("YAMS_STORAGE", std::nullopt);
ScopedEnv dataDir("YAMS_DATA_DIR", std::nullopt);
const auto resolved = yams::config::resolve_runtime_paths();
REQUIRE(resolved);
CHECK((resolved.value().configFile.value == config.path()));
CHECK((resolved.value().socketPath.value == fs::path("/from/config/yams.sock")));
CHECK((resolved.value().socketPath.source == yams::config::RuntimePathSource::ConfigFile));
CHECK((resolved.value().pidFile.value == fs::path("/from/config/yams.pid")));
CHECK((resolved.value().pidFile.source == yams::config::RuntimePathSource::ConfigFile));
}
TEST_CASE("RuntimePaths - explicit runtime root derives socket and PID defaults",
"[config][runtime_paths][catch2]") {
ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml");
ScopedEnv socketEnv("YAMS_DAEMON_SOCKET", std::nullopt);
ScopedEnv socketPathEnv("YAMS_DAEMON_SOCKET_PATH", std::nullopt);
ScopedEnv storage("YAMS_STORAGE", std::nullopt);
ScopedEnv dataDir("YAMS_DATA_DIR", std::nullopt);
ScopedEnv home("HOME", "/home/runtime-user");
yams::config::RuntimePathOverrides overrides;
overrides.runtimeDir = "~/isolated/runtime";
const auto resolved = yams::config::resolve_runtime_paths(overrides);
REQUIRE(resolved);
CHECK((resolved.value().runtimeDir.value == fs::path("/home/runtime-user/isolated/runtime")));
CHECK((resolved.value().socketPath.value ==
fs::path("/home/runtime-user/isolated/runtime/yams-daemon.sock")));
CHECK((resolved.value().pidFile.value ==
fs::path("/home/runtime-user/isolated/runtime/yams-daemon.pid")));
}
TEST_CASE("RuntimePaths - conflicting socket aliases fail closed",
"[config][runtime_paths][catch2]") {
ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml");
ScopedEnv socketEnv("YAMS_DAEMON_SOCKET", "/tmp/canonical.sock");
ScopedEnv socketPathEnv("YAMS_DAEMON_SOCKET_PATH", "/tmp/compat.sock");
ScopedEnv storage("YAMS_STORAGE", std::nullopt);
ScopedEnv dataDir("YAMS_DATA_DIR", std::nullopt);
const auto resolved = yams::config::resolve_runtime_paths();
REQUIRE_FALSE(resolved);
CHECK((resolved.error().code == yams::ErrorCode::InvalidData));
CHECK((resolved.error().message.find("YAMS_DAEMON_SOCKET") != std::string::npos));
CHECK((resolved.error().message.find("YAMS_DAEMON_SOCKET_PATH") != std::string::npos));
}
// ============================================================================
// parse_config_value() - Config file parsing tests
// ============================================================================
TEST_CASE("DataDirResolution - parse_config_value finds key", "[cli][data_dir][catch2]") {
TempConfig config(R"(
[core]
data_dir = "/test/path"
)");
auto value = yams::config::parse_config_value(config.path(), "core", "data_dir");
CHECK((value == "/test/path"));
}
TEST_CASE("DataDirResolution - parse_config_value handles quoted values",
"[cli][data_dir][catch2]") {
TempConfig config(R"(
[core]
data_dir = "/path/with spaces"
)");
auto value = yams::config::parse_config_value(config.path(), "core", "data_dir");
CHECK((value == "/path/with spaces"));
}
TEST_CASE("DataDirResolution - parse_config_value handles single quotes",
"[cli][data_dir][catch2]") {
TempConfig config(R"(
[core]
data_dir = '/single/quoted'
)");
auto value = yams::config::parse_config_value(config.path(), "core", "data_dir");
CHECK((value == "/single/quoted"));
}
TEST_CASE("DataDirResolution - parse_config_value ignores comments", "[cli][data_dir][catch2]") {
TempConfig config(R"(
[core]
# This is a comment
data_dir = "/actual/value" # inline comment
)");
auto value = yams::config::parse_config_value(config.path(), "core", "data_dir");
CHECK((value == "/actual/value"));
}
TEST_CASE("DataDirResolution - parse_config_value returns empty for missing key",
"[cli][data_dir][catch2]") {
TempConfig config(R"(
[core]
other_key = "value"
)");
auto value = yams::config::parse_config_value(config.path(), "core", "data_dir");
CHECK(value.empty());
}
TEST_CASE("DataDirResolution - parse_config_value returns empty for missing section",
"[cli][data_dir][catch2]") {
TempConfig config(R"(
[other]
data_dir = "/wrong/section"
)");
auto value = yams::config::parse_config_value(config.path(), "core", "data_dir");
CHECK(value.empty());
}
TEST_CASE("DataDirResolution - parse_config_value handles empty file", "[cli][data_dir][catch2]") {
TempConfig config("");
auto value = yams::config::parse_config_value(config.path(), "core", "data_dir");
CHECK(value.empty());
}
TEST_CASE("DataDirResolution - parse_config_value returns empty for nonexistent file",
"[cli][data_dir][catch2]") {
auto value = yams::config::parse_config_value("/nonexistent/config.toml", "core", "data_dir");
CHECK(value.empty());
}
// ============================================================================
// Tilde expansion tests
// ============================================================================
#ifndef _WIN32
TEST_CASE("DataDirResolution - expand_tilde expands home", "[cli][data_dir][catch2]") {
ScopedEnv home("HOME", "/home/testuser");
auto expanded = yams::config::expand_tilde("~/some/path");
CHECK((expanded == fs::path("/home/testuser/some/path")));
}
TEST_CASE("DataDirResolution - expand_tilde leaves absolute paths unchanged",
"[cli][data_dir][catch2]") {
auto expanded = yams::config::expand_tilde("/absolute/path");
CHECK((expanded == fs::path("/absolute/path")));
}
TEST_CASE("DataDirResolution - expand_tilde leaves relative paths unchanged",
"[cli][data_dir][catch2]") {
auto expanded = yams::config::expand_tilde("relative/path");
CHECK((expanded == fs::path("relative/path")));
}
#endif
// ============================================================================
// Values must NOT contain description strings
// ============================================================================
TEST_CASE("DataDirResolution - resolved path does not contain descriptions",
"[cli][data_dir][catch2]") {
std::vector<std::string> badPatterns = {"[Deprecated]", "Single path to file",
"use '-' for stdin", "Data directory for storage",
"especially useful for", "File or directory paths"};
auto dataDir = yams::config::get_data_dir();
auto configDir = yams::config::get_config_dir();
auto cacheDir = yams::config::get_cache_dir();
auto runtimeDir = yams::config::get_runtime_dir();
for (const auto& pattern : badPatterns) {
INFO("Checking pattern: " << pattern);
CHECK((dataDir.string().find(pattern) == std::string::npos));
CHECK((configDir.string().find(pattern) == std::string::npos));
CHECK((cacheDir.string().find(pattern) == std::string::npos));
CHECK((runtimeDir.string().find(pattern) == std::string::npos));
}
}
TEST_CASE("DataDirResolution - config value does not contain descriptions",
"[cli][data_dir][catch2]") {
TempConfig config(R"(
[core]
data_dir = "/proper/path"
)");
auto value = yams::config::parse_config_value(config.path(), "core", "data_dir");
CHECK((value == "/proper/path"));
CHECK((value.find("[Deprecated]") == std::string::npos));
CHECK((value.find("file/directory") == std::string::npos));
CHECK((value.find("Data directory") == std::string::npos));
}
// ============================================================================
// socket_path resolution tests
// ============================================================================
TEST_CASE("DataDirResolution - socket_path env takes precedence", "[cli][data_dir][catch2]") {
ScopedEnv socketEnv("YAMS_DAEMON_SOCKET", "/env/yams.sock");
ScopedEnv socketPathEnv("YAMS_DAEMON_SOCKET_PATH", std::nullopt);
auto resolved = yams::config::resolve_socket_path_from_config();
CHECK((resolved == fs::path("/env/yams.sock")));
}
TEST_CASE("DataDirResolution - socket_path from config file", "[cli][data_dir][catch2]") {
TempConfig config(R"(
[daemon]
socket_path = "/from/config/yams.sock"
)");
ScopedEnv socketEnv("YAMS_DAEMON_SOCKET", "");
ScopedEnv socketPathEnv("YAMS_DAEMON_SOCKET_PATH", std::nullopt);
ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str());
auto resolved = yams::config::resolve_socket_path_from_config();
CHECK((resolved == fs::path("/from/config/yams.sock")));
}
TEST_CASE("DataDirResolution - socket_path env overrides config", "[cli][data_dir][catch2]") {
TempConfig config(R"(
[daemon]
socket_path = "/from/config/yams.sock"
)");
ScopedEnv socketEnv("YAMS_DAEMON_SOCKET", "/env/wins.sock");
ScopedEnv socketPathEnv("YAMS_DAEMON_SOCKET_PATH", std::nullopt);
ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str());
auto resolved = yams::config::resolve_socket_path_from_config();
CHECK((resolved == fs::path("/env/wins.sock")));
}
// ============================================================================
// parse_path_list() tests
// ============================================================================
TEST_CASE("DataDirResolution - parse_path_list handles comma-separated",
"[cli][data_dir][catch2]") {
auto paths = yams::config::parse_path_list("/path/a,/path/b,/path/c");
REQUIRE((paths.size() == 3));
CHECK((paths[0] == fs::path("/path/a")));
CHECK((paths[1] == fs::path("/path/b")));
CHECK((paths[2] == fs::path("/path/c")));
}
TEST_CASE("DataDirResolution - parse_path_list handles TOML array", "[cli][data_dir][catch2]") {
auto paths = yams::config::parse_path_list(R"(["/path/a", "/path/b"])");
REQUIRE((paths.size() == 2));
CHECK((paths[0] == fs::path("/path/a")));
CHECK((paths[1] == fs::path("/path/b")));
}
TEST_CASE("DataDirResolution - parse_path_list handles empty string", "[cli][data_dir][catch2]") {
auto paths = yams::config::parse_path_list("");
CHECK(paths.empty());
}
TEST_CASE("DataDirResolution - parse_path_list handles whitespace", "[cli][data_dir][catch2]") {
auto paths = yams::config::parse_path_list(" /path/a , /path/b ");
REQUIRE((paths.size() == 2));
CHECK((paths[0] == fs::path("/path/a")));
CHECK((paths[1] == fs::path("/path/b")));
}
#ifndef _WIN32
TEST_CASE("DataDirResolution - parse_path_list expands tilde", "[cli][data_dir][catch2]") {
ScopedEnv home("HOME", "/home/testuser");
auto paths = yams::config::parse_path_list("~/path/a,~/path/b");
REQUIRE((paths.size() == 2));
CHECK((paths[0] == fs::path("/home/testuser/path/a")));
CHECK((paths[1] == fs::path("/home/testuser/path/b")));
}
#endif