2026-03-24 17:28:59 -04:00
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
#include <cstdlib>
|
2026-05-21 14:05:18 -04:00
|
|
|
#include <filesystem>
|
|
|
|
|
#include <string>
|
2026-03-24 17:28:59 -04:00
|
|
|
#include "config.h"
|
|
|
|
|
|
2026-05-21 14:05:18 -04:00
|
|
|
static std::string getTempDir()
|
|
|
|
|
{
|
|
|
|
|
const char* tmp = std::getenv("TMPDIR");
|
|
|
|
|
if (!tmp || !*tmp) tmp = "/tmp";
|
|
|
|
|
return std::string(tmp);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-24 17:28:59 -04:00
|
|
|
class ConfigTest : public ::testing::Test {
|
|
|
|
|
protected:
|
2026-05-21 14:05:18 -04:00
|
|
|
std::string origHome;
|
|
|
|
|
std::string testDir;
|
2026-03-24 17:28:59 -04:00
|
|
|
|
|
|
|
|
void SetUp() override {
|
2026-08-03 21:49:15 -03:00
|
|
|
// These tests describe the logosctl surface. The flavor defaults to
|
|
|
|
|
// Legacy so that anything which forgets to set it behaves like the
|
|
|
|
|
// tool that exists today, so say which one we mean.
|
|
|
|
|
Config::setFlavor(Config::Flavor::Modern);
|
|
|
|
|
testDir = getTempDir() + "/logosctl_test_config_" + std::to_string(getpid());
|
|
|
|
|
std::filesystem::create_directories(testDir + "/.logosctl");
|
2026-03-24 17:28:59 -04:00
|
|
|
|
2026-05-21 14:05:18 -04:00
|
|
|
const char* h = std::getenv("HOME");
|
|
|
|
|
origHome = h ? std::string(h) : "";
|
|
|
|
|
setenv("HOME", testDir.c_str(), 1);
|
2026-03-24 17:28:59 -04:00
|
|
|
|
2026-08-03 21:49:15 -03:00
|
|
|
unsetenv("LOGOSCTL_TOKEN");
|
|
|
|
|
unsetenv("LOGOSCTL_CONFIG_DIR");
|
2026-05-21 14:05:18 -04:00
|
|
|
Config::setConfigDir("");
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TearDown() override {
|
2026-05-21 14:05:18 -04:00
|
|
|
setenv("HOME", origHome.c_str(), 1);
|
2026-08-03 21:49:15 -03:00
|
|
|
unsetenv("LOGOSCTL_CONFIG_DIR");
|
2026-05-21 14:05:18 -04:00
|
|
|
Config::setConfigDir("");
|
|
|
|
|
std::filesystem::remove_all(testDir);
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-03 21:49:15 -03:00
|
|
|
TEST_F(ConfigTest, ConfigDir_ReturnsHomeLogosctl)
|
2026-03-24 17:28:59 -04:00
|
|
|
{
|
2026-05-21 14:05:18 -04:00
|
|
|
std::string dir = Config::configDir();
|
2026-08-03 21:49:15 -03:00
|
|
|
const std::string suffix = "/.logosctl";
|
2026-05-21 14:05:18 -04:00
|
|
|
EXPECT_TRUE(dir.size() > suffix.size() &&
|
|
|
|
|
dir.substr(dir.size() - suffix.size()) == suffix);
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
2026-05-07 13:36:07 -03:00
|
|
|
// ----------------------------------------------------------------------
|
|
|
|
|
// Token resolution
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
TEST_F(ConfigTest, GetToken_EnvVarReturned)
|
2026-03-24 17:28:59 -04:00
|
|
|
{
|
2026-08-03 21:49:15 -03:00
|
|
|
setenv("LOGOSCTL_TOKEN", "env-token", 1);
|
2026-03-24 17:28:59 -04:00
|
|
|
EXPECT_EQ(Config::getToken(), "env-token");
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 13:36:07 -03:00
|
|
|
TEST_F(ConfigTest, GetToken_EmptyWhenNoEnv)
|
2026-03-24 17:28:59 -04:00
|
|
|
{
|
2026-05-21 14:05:18 -04:00
|
|
|
EXPECT_TRUE(Config::getToken().empty());
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
2026-05-07 13:36:07 -03:00
|
|
|
// ----------------------------------------------------------------------
|
|
|
|
|
// Path helpers — verify the daemon/ and client/ subdir layout is honored.
|
|
|
|
|
// ----------------------------------------------------------------------
|
2026-03-24 17:28:59 -04:00
|
|
|
|
2026-05-07 13:36:07 -03:00
|
|
|
TEST_F(ConfigTest, Paths_DaemonAndClientUnderConfigDir)
|
|
|
|
|
{
|
2026-05-21 14:05:18 -04:00
|
|
|
const std::string cfg = Config::configDir();
|
|
|
|
|
EXPECT_EQ(Config::daemonDir(), cfg + "/daemon");
|
2026-08-03 21:49:15 -03:00
|
|
|
EXPECT_EQ(Config::daemonConfigPath(), cfg + "/daemon/config.yaml");
|
2026-05-21 14:05:18 -04:00
|
|
|
EXPECT_EQ(Config::daemonStatePath(), cfg + "/daemon/state.json");
|
|
|
|
|
EXPECT_EQ(Config::daemonTokensPath(), cfg + "/daemon/tokens.json");
|
|
|
|
|
EXPECT_EQ(Config::daemonTokensDir(), cfg + "/daemon/tokens");
|
|
|
|
|
EXPECT_EQ(Config::clientDir(), cfg + "/client");
|
2026-08-03 21:49:15 -03:00
|
|
|
EXPECT_EQ(Config::clientConfigPath(), cfg + "/client/config.yaml");
|
2026-05-21 14:05:18 -04:00
|
|
|
EXPECT_EQ(Config::clientTokenPath("auto.json"), cfg + "/client/auto.json");
|
2026-03-24 17:28:59 -04:00
|
|
|
}
|
|
|
|
|
|
2026-06-15 10:52:45 -04:00
|
|
|
// ----------------------------------------------------------------------
|
|
|
|
|
// BUG-019: clientTokenPath() must not let a token_file value escape the
|
|
|
|
|
// client/ dir. The filename comes from --token-file, the
|
|
|
|
|
// LOGOSCORE_CLIENT_TOKEN_FILE env var, or client/config.json's token_file
|
|
|
|
|
// field; a value like "../daemon/tokens.json" or "../../etc/passwd" would
|
|
|
|
|
// otherwise resolve outside client/ and be read as a credential file.
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
TEST_F(ConfigTest, ClientTokenPath_AcceptsSimpleFilenames)
|
|
|
|
|
{
|
|
|
|
|
const std::string cfg = Config::configDir();
|
|
|
|
|
EXPECT_EQ(Config::clientTokenPath("auto.json"), cfg + "/client/auto.json");
|
|
|
|
|
EXPECT_EQ(Config::clientTokenPath("alice.json"), cfg + "/client/alice.json");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(ConfigTest, ClientTokenPath_RejectsTraversal)
|
|
|
|
|
{
|
|
|
|
|
const std::string clientDir = Config::clientDir();
|
|
|
|
|
// Every traversal/absolute attempt must stay strictly inside client/:
|
|
|
|
|
// the resolved path must begin with "<clientDir>/" and must not contain
|
|
|
|
|
// a ".." component that climbs out.
|
|
|
|
|
for (const std::string bad : {
|
|
|
|
|
std::string("../daemon/tokens.json"),
|
|
|
|
|
std::string("../../etc/passwd"),
|
|
|
|
|
std::string("sub/dir/token.json"),
|
|
|
|
|
std::string("/etc/passwd"),
|
|
|
|
|
}) {
|
|
|
|
|
const std::string got = Config::clientTokenPath(bad);
|
|
|
|
|
EXPECT_EQ(got.rfind(clientDir + "/", 0), 0u)
|
|
|
|
|
<< "clientTokenPath('" << bad << "') escaped client/: " << got;
|
|
|
|
|
EXPECT_EQ(got.find(".."), std::string::npos)
|
|
|
|
|
<< "clientTokenPath('" << bad << "') still contains '..': " << got;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 13:36:07 -03:00
|
|
|
// ----------------------------------------------------------------------
|
|
|
|
|
// Config-dir override precedence
|
|
|
|
|
// ----------------------------------------------------------------------
|
2026-04-22 12:38:11 -03:00
|
|
|
|
|
|
|
|
TEST_F(ConfigTest, ConfigDir_EnvVarOverridesHome)
|
|
|
|
|
{
|
2026-05-21 14:05:18 -04:00
|
|
|
const std::string alt = testDir + "/alt-config";
|
|
|
|
|
std::filesystem::create_directories(alt);
|
2026-08-03 21:49:15 -03:00
|
|
|
setenv("LOGOSCTL_CONFIG_DIR", alt.c_str(), 1);
|
2026-04-22 12:38:11 -03:00
|
|
|
|
|
|
|
|
EXPECT_EQ(Config::configDir(), alt);
|
2026-05-21 14:05:18 -04:00
|
|
|
EXPECT_EQ(Config::daemonConfigPath().substr(0, alt.size()), alt);
|
|
|
|
|
EXPECT_EQ(Config::clientConfigPath().substr(0, alt.size()), alt);
|
2026-04-22 12:38:11 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(ConfigTest, ConfigDir_SetterOverridesEnvVar)
|
|
|
|
|
{
|
2026-05-21 14:05:18 -04:00
|
|
|
const std::string envDir = testDir + "/env-config";
|
|
|
|
|
const std::string setterDir = testDir + "/setter-config";
|
|
|
|
|
std::filesystem::create_directories(envDir);
|
|
|
|
|
std::filesystem::create_directories(setterDir);
|
2026-04-22 12:38:11 -03:00
|
|
|
|
2026-08-03 21:49:15 -03:00
|
|
|
setenv("LOGOSCTL_CONFIG_DIR", envDir.c_str(), 1);
|
2026-04-22 12:38:11 -03:00
|
|
|
Config::setConfigDir(setterDir);
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(Config::configDir(), setterDir)
|
|
|
|
|
<< "explicit setter (from --config-dir) must win over env var";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(ConfigTest, ConfigDir_ClearingSetterFallsBackToEnv)
|
|
|
|
|
{
|
2026-05-21 14:05:18 -04:00
|
|
|
const std::string envDir = testDir + "/env-config";
|
|
|
|
|
const std::string setterDir = testDir + "/setter-config";
|
|
|
|
|
std::filesystem::create_directories(envDir);
|
|
|
|
|
std::filesystem::create_directories(setterDir);
|
2026-04-22 12:38:11 -03:00
|
|
|
|
2026-08-03 21:49:15 -03:00
|
|
|
setenv("LOGOSCTL_CONFIG_DIR", envDir.c_str(), 1);
|
2026-04-22 12:38:11 -03:00
|
|
|
Config::setConfigDir(setterDir);
|
|
|
|
|
ASSERT_EQ(Config::configDir(), setterDir);
|
|
|
|
|
|
2026-05-21 14:05:18 -04:00
|
|
|
Config::setConfigDir("");
|
2026-04-22 12:38:11 -03:00
|
|
|
EXPECT_EQ(Config::configDir(), envDir);
|
|
|
|
|
}
|
2026-08-03 21:49:15 -03:00
|
|
|
|
|
|
|
|
// The two binaries must not share a byte of state: a bad logosctl session
|
|
|
|
|
// cannot be allowed to disturb a working logoscore deployment. That isolation
|
|
|
|
|
// is the whole reason `logoscore` can keep shipping unchanged while logosctl
|
|
|
|
|
// is validated, so pin it down.
|
|
|
|
|
TEST_F(ConfigTest, LegacyAndModernFlavorsShareNothing)
|
|
|
|
|
{
|
|
|
|
|
Config::setConfigDir(""); // fall back to the per-flavor default
|
|
|
|
|
|
|
|
|
|
Config::setFlavor(Config::Flavor::Legacy);
|
|
|
|
|
const std::string legacyDir = Config::configDir();
|
|
|
|
|
const std::string legacyCfg = Config::daemonConfigPath();
|
|
|
|
|
const std::string legacyClnt = Config::clientConfigPath();
|
|
|
|
|
|
|
|
|
|
Config::setFlavor(Config::Flavor::Modern);
|
|
|
|
|
const std::string modernDir = Config::configDir();
|
|
|
|
|
const std::string modernCfg = Config::daemonConfigPath();
|
|
|
|
|
const std::string modernClnt = Config::clientConfigPath();
|
|
|
|
|
|
|
|
|
|
EXPECT_NE(legacyDir, modernDir);
|
|
|
|
|
EXPECT_NE(std::string::npos, legacyDir.find(".logoscore"));
|
|
|
|
|
EXPECT_NE(std::string::npos, modernDir.find(".logosctl"));
|
|
|
|
|
|
|
|
|
|
// logoscore keeps writing JSON so an existing deployment's config stays
|
|
|
|
|
// readable by the tool that wrote it; logosctl writes YAML.
|
|
|
|
|
EXPECT_NE(std::string::npos, legacyCfg.find("config.json"));
|
|
|
|
|
EXPECT_NE(std::string::npos, modernCfg.find("config.yaml"));
|
|
|
|
|
EXPECT_NE(std::string::npos, legacyClnt.find("config.json"));
|
|
|
|
|
EXPECT_NE(std::string::npos, modernClnt.find("config.yaml"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The session is portable by default -- every subdirectory lives inside the
|
|
|
|
|
// config dir -- but each one can be redirected, so a shared keyring or a cache
|
|
|
|
|
// on a bigger disk doesn't force you to give that up wholesale.
|
|
|
|
|
TEST_F(ConfigTest, SessionDirsDefaultInsideTheSession)
|
|
|
|
|
{
|
|
|
|
|
const std::string cfg = testDir;
|
|
|
|
|
Config::setConfigDir(cfg);
|
|
|
|
|
for (auto which : {Config::SessionDir::Modules, Config::SessionDir::Plugins,
|
|
|
|
|
Config::SessionDir::Keyring, Config::SessionDir::Data,
|
|
|
|
|
Config::SessionDir::Cache})
|
|
|
|
|
Config::setSessionDirOverride(which, "");
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(Config::modulesDir(), cfg + "/modules");
|
|
|
|
|
EXPECT_EQ(Config::pluginsDir(), cfg + "/plugins");
|
|
|
|
|
EXPECT_EQ(Config::keyringDir(), cfg + "/keyring");
|
|
|
|
|
EXPECT_EQ(Config::dataDir(), cfg + "/data");
|
|
|
|
|
EXPECT_EQ(Config::cacheDir(), cfg + "/cache");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(ConfigTest, SessionDirOverrideResolvesByForm)
|
|
|
|
|
{
|
|
|
|
|
const std::string cfg = testDir;
|
|
|
|
|
Config::setConfigDir(cfg);
|
|
|
|
|
|
|
|
|
|
// Absolute: deliberately outside the session.
|
|
|
|
|
Config::setSessionDirOverride(Config::SessionDir::Keyring, "/opt/shared/keys");
|
|
|
|
|
EXPECT_EQ(Config::keyringDir(), "/opt/shared/keys");
|
|
|
|
|
|
|
|
|
|
// Relative: still inside the session, so it stays portable.
|
|
|
|
|
Config::setSessionDirOverride(Config::SessionDir::Modules, "my-modules");
|
|
|
|
|
EXPECT_EQ(Config::modulesDir(), cfg + "/my-modules");
|
|
|
|
|
|
|
|
|
|
// `~` is natural to write in a config file and would otherwise be taken
|
|
|
|
|
// as a relative path, producing <configDir>/~/... which exists nowhere.
|
|
|
|
|
if (const char* home = std::getenv("HOME")) {
|
|
|
|
|
Config::setSessionDirOverride(Config::SessionDir::Cache, "~/lgx-cache");
|
|
|
|
|
EXPECT_EQ(Config::cacheDir(), std::string(home) + "/lgx-cache");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clearing restores the default.
|
|
|
|
|
Config::setSessionDirOverride(Config::SessionDir::Keyring, "");
|
|
|
|
|
EXPECT_EQ(Config::keyringDir(), cfg + "/keyring");
|
|
|
|
|
|
|
|
|
|
for (auto which : {Config::SessionDir::Modules, Config::SessionDir::Cache})
|
|
|
|
|
Config::setSessionDirOverride(which, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// An override is resolved once, when it is set. Relocating the session
|
|
|
|
|
// afterwards must not silently drag an absolute override along with it.
|
|
|
|
|
TEST_F(ConfigTest, SessionDirOverrideIsResolvedAtSetTime)
|
|
|
|
|
{
|
|
|
|
|
Config::setConfigDir(testDir);
|
|
|
|
|
Config::setSessionDirOverride(Config::SessionDir::Modules, "my-modules");
|
|
|
|
|
const std::string before = Config::modulesDir();
|
|
|
|
|
|
|
|
|
|
Config::setConfigDir(testDir + "-moved");
|
|
|
|
|
EXPECT_EQ(Config::modulesDir(), before);
|
|
|
|
|
|
|
|
|
|
Config::setSessionDirOverride(Config::SessionDir::Modules, "");
|
|
|
|
|
}
|