#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);
}