BareGit
#include "configuration.h"

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

#include <gtest/gtest.h>

namespace
{

class TemporaryConfig
{
public:
    explicit TemporaryConfig(std::string content)
        : path(std::filesystem::temp_directory_path() /
               "status_tracker_test_config.yaml")
    {
        std::ofstream file(path);
        file << content;
    }

    ~TemporaryConfig()
    {
        std::filesystem::remove(path);
    }

    const std::filesystem::path path;
};

TEST(Configuration, LoadsGroupedProtocolSettings)
{
    TemporaryConfig file(
        "worker_count: 4\n"
        "database_path: tracker.sqlite\n"
        "unix_socket: /run/probius.sock\n"
        "socket_permission: 0o660\n"
        "groups:\n"
        "  - name: Public services\n"
        "    services:\n"
        "      blog:\n"
        "        name: Blog\n"
        "        description: Personal website\n"
        "        url: https://blog.example/\n"
        "        timeout_second: 3\n"
        "        interval:\n"
        "          value: 1\n"
        "          unit: minute\n"
        "        endpoint:\n"
        "          protocol: HTTP\n"
        "      ssh:\n"
        "        name: SSH\n"
        "        description: SSH server\n"
        "        interval:\n"
        "          value: 2\n"
        "          unit: hour\n"
        "        endpoint:\n"
        "          protocol: tcp\n"
        "          host: server.example\n"
        "          port: 22\n"
        "          timeout_second: 7\n"
        "      dns:\n"
        "        name: DNS\n"
        "        description: DNS server\n"
        "        interval:\n"
        "          value: 30\n"
        "          unit: second\n"
        "        endpoint:\n"
        "          protocol: UDP\n"
        "          host: dns.example\n"
        "          port: 53\n"
        "          payload: ping\n"
        "      gateway:\n"
        "        name: Gateway\n"
        "        description: Network gateway\n"
        "        interval:\n"
        "          value: 1\n"
        "          unit: day\n"
        "        endpoint:\n"
        "          protocol: icmp\n"
        "          host: 192.0.2.1\n");

    auto configuration = Configuration::fromYaml(file.path);
    ASSERT_TRUE(configuration)
        << (configuration ? "" : configuration.error().msg());
    EXPECT_EQ(configuration->worker_count, 4);
    EXPECT_EQ(configuration->database_path, "tracker.sqlite");
    EXPECT_EQ(configuration->unix_socket, "/run/probius.sock");
    ASSERT_TRUE(configuration->socket_permission);
    EXPECT_EQ(*configuration->socket_permission, 0660);
    ASSERT_EQ(configuration->groups.size(), 1);
    ASSERT_EQ(configuration->groups.front().services.size(), 4);

    const auto& services = configuration->groups.front().services;
    EXPECT_EQ(services[0].timeout, std::chrono::seconds(3));
    EXPECT_EQ(services[0].interval, std::chrono::minutes(1));
    ASSERT_TRUE(std::holds_alternative<HttpEndpoint>(services[0].endpoint));
    EXPECT_TRUE(std::get<HttpEndpoint>(services[0].endpoint).url.empty());
    ASSERT_TRUE(services[0].url);
    EXPECT_EQ(*services[0].url, "https://blog.example/");

    EXPECT_EQ(services[1].timeout, std::chrono::seconds(7));
    ASSERT_TRUE(std::holds_alternative<TcpEndpoint>(services[1].endpoint));
    EXPECT_EQ(std::get<TcpEndpoint>(services[1].endpoint).port, 22);

    ASSERT_TRUE(std::holds_alternative<UdpEndpoint>(services[2].endpoint));
    EXPECT_EQ(std::get<UdpEndpoint>(services[2].endpoint).payload, "ping");
    EXPECT_EQ(services[3].interval, std::chrono::hours(24));
    ASSERT_TRUE(std::holds_alternative<IcmpEndpoint>(services[3].endpoint));
}

TEST(Configuration, RejectsInvalidValues)
{
    const std::vector<std::string> contents = {
        "worker_count: 0\n"
        "database_path: db\n"
        "groups: {}\n",
        "worker_count: 1\n"
        "database_path: db\n"
        "groups:\n"
        "  - name: g\n"
        "    services:\n"
        "      x:\n"
        "        name: x\n"
        "        description: x\n"
        "        interval: {value: 1, unit: fortnight}\n"
        "        endpoint: {protocol: TCP, host: x, port: 1}\n",
        "worker_count: 1\n"
        "database_path: db\n"
        "groups:\n"
        "  - name: g\n"
        "    services:\n"
        "      x:\n"
        "        name: x\n"
        "        description: x\n"
        "        interval: {value: 1, unit: minute}\n"
        "        endpoint: {protocol: TCP, host: x, port: 0}\n",
        "worker_count: 1\n"
        "database_path: db\n"
        "unix_socket: /run/probius.sock\n"
        "socket_permission: 0o668\n"
        "groups: []\n",
        "worker_count: 1\n"
        "database_path: db\n"
        "unix_socket: /run/probius.sock\n"
        "socket_permission: 0o1000\n"
        "groups: []\n"
    };
    for(const auto& content : contents)
    {
        TemporaryConfig file(content);
        EXPECT_FALSE(Configuration::fromYaml(file.path));
    }
}

TEST(Configuration, RejectsDuplicateServiceIDs)
{
    TemporaryConfig file(
        "worker_count: 1\n"
        "database_path: db.sqlite\n"
        "groups:\n"
        "  - name: first\n"
        "    services:\n"
        "      duplicate:\n"
        "        name: First\n"
        "        description: First service\n"
        "        interval: {value: 1, unit: minute}\n"
        "        endpoint: {protocol: ICMP, host: 192.0.2.1}\n"
        "  - name: second\n"
        "    services:\n"
        "      duplicate:\n"
        "        name: Second\n"
        "        description: Second service\n"
        "        interval: {value: 1, unit: minute}\n"
        "        endpoint: {protocol: ICMP, host: 192.0.2.2}\n");

    EXPECT_FALSE(Configuration::fromYaml(file.path));
}

TEST(Configuration, RejectsMalformedYaml)
{
    TemporaryConfig file("worker_count: [\n");
    EXPECT_FALSE(Configuration::fromYaml(file.path));
}

}