BareGit
#include "config.h"
#include "non_secret_random.h"

#include <chrono>
#include <filesystem>
#include <fstream>
#include <string>
#include <variant>

#include <gtest/gtest.h>

namespace
{

class TemporaryConfigRoot
{
public:
    /// Allocate a temporary configuration directory with static assets.
    TemporaryConfigRoot()
            : path_(
                  std::filesystem::path(testing::TempDir()) /
                  ("card_config_" + std::to_string(
                      std::chrono::steady_clock::now()
                          .time_since_epoch().count())))
    {
        std::filesystem::create_directories(path_ / "static");
    }

    /// Remove the complete temporary configuration directory.
    ~TemporaryConfigRoot()
    {
        std::error_code error;
        std::filesystem::remove_all(path_, error);
    }

    /// Return the temporary configuration directory.
    const std::filesystem::path& path() const
    {
        return path_;
    }

private:
    std::filesystem::path path_;
};

void writeConfig(
    const std::filesystem::path& path,
    const std::string& listen_fields,
    const std::string& extra = {})
{
    std::ofstream output(path);
    output
        << "base_url = \"https://example.test/collection\"\n"
        << listen_fields
        << "static_root = \"static\"\n"
        << "database_path = \"var/cards.sqlite3\"\n"
        << "card_storage_root = \"var/cards\"\n"
        << "avif_quality = 75\n"
        << "thumbnail_long_side = 256\n"
        << "administrator_email = \"Admin@Example.com\"\n"
        << "maximum_accumulated_pulls = 3\n"
        << "[email]\n"
        << "transport = \"file\"\n"
        << "link_file = \""
        << (path.parent_path() / "auth-link.txt").string()
        << "\"\n"
        << extra;
}

} // namespace

/// Verify TCP configuration resolves relative paths beside its TOML file.
TEST(ConfigTest, LoadsTcpConfiguration)
{
    TemporaryConfigRoot temporary;
    const std::filesystem::path config_path = temporary.path() / "app.toml";
    writeConfig(
        config_path,
        "listen_address = \"127.0.0.1\"\nlisten_port = 8080\n");

    auto config = loadConfig(config_path);

    ASSERT_TRUE(config) << config.error().msg();
    EXPECT_EQ(
        config->base_url.str(),
        "https://example.test/collection/");
    ASSERT_TRUE(std::holds_alternative<mw::IPSocketInfo>(
        config->listen_address));
    const mw::IPSocketInfo& address =
        std::get<mw::IPSocketInfo>(config->listen_address);
    EXPECT_EQ(address.address, "127.0.0.1");
    EXPECT_EQ(address.port, 8080);
    EXPECT_EQ(config->static_root, temporary.path() / "static");
    EXPECT_EQ(config->administrator_email_key, "admin@example.com");
    EXPECT_EQ(config->maximum_accumulated_pulls, 3);
    EXPECT_TRUE(std::filesystem::is_directory(
        temporary.path() / "var/cards/published"));
}

/// Verify Unix listeners do not require a TCP port.
TEST(ConfigTest, LoadsUnixSocketConfiguration)
{
    TemporaryConfigRoot temporary;
    const std::filesystem::path config_path = temporary.path() / "app.toml";
    writeConfig(
        config_path,
        "listen_address = \"unix:/tmp/card-collection.sock\"\n");

    auto config = loadConfig(config_path);

    ASSERT_TRUE(config) << config.error().msg();
    ASSERT_TRUE(std::holds_alternative<mw::SocketFileInfo>(
        config->listen_address));
    EXPECT_EQ(
        std::get<mw::SocketFileInfo>(config->listen_address).filename,
        "/tmp/card-collection.sock");
}

/// Verify unknown configuration keys are rejected.
TEST(ConfigTest, RejectsUnknownKeys)
{
    TemporaryConfigRoot temporary;
    const std::filesystem::path config_path = temporary.path() / "app.toml";
    writeConfig(
        config_path,
        "listen_address = \"127.0.0.1\"\nlisten_port = 8080\n",
        "misspelled_setting = true\n");

    EXPECT_FALSE(loadConfig(config_path));
}

/// Verify equal deterministic seeds produce equal values and byte strings.
TEST(NonSecretRandomTest, RepeatsDeterministicSequence)
{
    NonSecretRandom left(42);
    NonSecretRandom right(42);

    for(int index = 0; index < 128; ++index)
    {
        EXPECT_EQ(left.next(), right.next());
    }
    EXPECT_EQ(left.hex(32), right.hex(32));
}

/// Verify hexadecimal output has the requested lowercase encoded size.
TEST(NonSecretRandomTest, ProducesLowercaseHex)
{
    NonSecretRandom random(7);
    const std::string value = random.hex(32);

    ASSERT_EQ(value.size(), 64);
    EXPECT_EQ(value.find_first_not_of("0123456789abcdef"),
              std::string::npos);
}