// Copyright (C) 2016-2025 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#include <config.h>
#include <agent/ca_cfg_mgr.h>
#include <agent/parser_context.h>
#include <config/unix_command_config.h>
#include <exceptions/exceptions.h>
#include <process/testutils/d_test_stubs.h>
#include <process/d_cfg_mgr.h>
#include <http/basic_auth_config.h>
#include <agent/tests/test_callout_libraries.h>
#include <agent/tests/test_data_files_config.h>
#include <hooks/hooks_parser.h>
#include <util/filesystem.h>
#include <boost/pointer_cast.hpp>
#include <boost/scoped_ptr.hpp>
#include <gtest/gtest.h>

using namespace isc::agent;
using namespace isc::data;
using namespace isc::hooks;
using namespace isc::http;
using namespace isc::process;
using namespace isc::config;
using namespace isc::util;

namespace  {

/// @brief Almost regular agent CfgMgr with internal parse method exposed.
class NakedAgentCfgMgr : public CtrlAgentCfgMgr {
public:
    using CtrlAgentCfgMgr::parse;
};

// Tests construction of CtrlAgentCfgMgr class.
TEST(CtrlAgentCfgMgr, construction) {
    boost::scoped_ptr<CtrlAgentCfgMgr> cfg_mgr;

    // Verify that configuration manager constructions without error.
    ASSERT_NO_THROW(cfg_mgr.reset(new CtrlAgentCfgMgr()));

    // Verify that the context can be retrieved and is not null.
    CtrlAgentCfgContextPtr context;
    ASSERT_NO_THROW(context = cfg_mgr->getCtrlAgentCfgContext());
    EXPECT_TRUE(context);

    // Verify that the manager can be destructed without error.
    EXPECT_NO_THROW(cfg_mgr.reset());
}

// Tests if getContext can be retrieved.
TEST(CtrlAgentCfgMgr, getContext) {
    CtrlAgentCfgMgr cfg_mgr;

    CtrlAgentCfgContextPtr ctx;
    ASSERT_NO_THROW(ctx = cfg_mgr.getCtrlAgentCfgContext());
    ASSERT_TRUE(ctx);
}

// Tests if context can store and retrieve HTTP parameters
TEST(CtrlAgentCfgMgr, contextHttpParams) {
    CtrlAgentCfgContext ctx;

    // Check http parameters
    ctx.setHttpPort(12345);
    EXPECT_EQ(12345, ctx.getHttpPort());

    ctx.setHttpHost("alnitak");
    EXPECT_EQ("alnitak", ctx.getHttpHost());
}

// Tests if context can store and retrieve TLS parameters.
TEST(CtrlAgentCfgMgr, contextTlsParams) {
    CtrlAgentCfgContext ctx;

    // Check TLS parameters
    ctx.setTrustAnchor("my-ca");
    EXPECT_EQ("my-ca", ctx.getTrustAnchor());

    ctx.setCertFile("my-cert");
    EXPECT_EQ("my-cert", ctx.getCertFile());

    ctx.setKeyFile("my-key");
    EXPECT_EQ("my-key", ctx.getKeyFile());

    EXPECT_TRUE(ctx.getCertRequired());
    ctx.setCertRequired(false);
    EXPECT_FALSE(ctx.getCertRequired());
}

// Tests if context can store and retrieve control socket information.
TEST(CtrlAgentCfgMgr, contextSocketInfo) {

    CtrlAgentCfgContext ctx;

    // Check control socket parameters
    // By default, there are no control sockets stored.
    EXPECT_FALSE(ctx.getControlSocketInfo("d2"));
    EXPECT_FALSE(ctx.getControlSocketInfo("dhcp4"));
    EXPECT_FALSE(ctx.getControlSocketInfo("dhcp6"));

    ConstElementPtr socket1 = Element::fromJSON("{ \"socket-type\": \"unix\",\n"
                                                "  \"socket-name\": \"socket1\" }");
    ConstElementPtr socket2 = Element::fromJSON("{ \"socket-type\": \"unix\",\n"
                                                "  \"socket-name\": \"socket2\" }");
    ConstElementPtr socket3 = Element::fromJSON("{ \"socket-type\": \"unix\",\n"
                                                "  \"socket-name\": \"socket3\" }");
    // Ok, now set the control socket for D2
    EXPECT_NO_THROW(ctx.setControlSocketInfo(socket1, "d2"));

    // Now check the values returned
    EXPECT_EQ(socket1, ctx.getControlSocketInfo("d2"));
    EXPECT_FALSE(ctx.getControlSocketInfo("dhcp4"));
    EXPECT_FALSE(ctx.getControlSocketInfo("dhcp6"));

    // Now set the v6 socket and sanity check again
    EXPECT_NO_THROW(ctx.setControlSocketInfo(socket2, "dhcp6"));

    // Should be possible to retrieve two sockets.
    EXPECT_EQ(socket1, ctx.getControlSocketInfo("d2"));
    EXPECT_EQ(socket2, ctx.getControlSocketInfo("dhcp6"));
    EXPECT_FALSE(ctx.getControlSocketInfo("dhcp4"));

    // Finally, set the third control socket.
    EXPECT_NO_THROW(ctx.setControlSocketInfo(socket3, "dhcp4"));
    EXPECT_EQ(socket1, ctx.getControlSocketInfo("d2"));
    EXPECT_EQ(socket2, ctx.getControlSocketInfo("dhcp6"));
    EXPECT_EQ(socket3, ctx.getControlSocketInfo("dhcp4"));
}

// Tests if copied context retains all parameters.
TEST(CtrlAgentCfgMgr, contextSocketInfoCopy) {

    CtrlAgentCfgContext ctx;

    ConstElementPtr socket1 = Element::fromJSON("{ \"socket-type\": \"unix\",\n"
                                                "  \"socket-name\": \"socket1\" }");
    ConstElementPtr socket2 = Element::fromJSON("{ \"socket-type\": \"unix\",\n"
                                                "  \"socket-name\": \"socket2\" }");
    ConstElementPtr socket3 = Element::fromJSON("{ \"socket-type\": \"unix\",\n"
                                                "  \"socket-name\": \"socket3\" }");
    // Ok, now set the control sockets
    EXPECT_NO_THROW(ctx.setControlSocketInfo(socket1, "d2"));
    EXPECT_NO_THROW(ctx.setControlSocketInfo(socket2, "dhcp4"));
    EXPECT_NO_THROW(ctx.setControlSocketInfo(socket3, "dhcp6"));

    EXPECT_NO_THROW(ctx.setHttpPort(12345));
    EXPECT_NO_THROW(ctx.setHttpHost("bellatrix"));

    HooksConfig& libs = ctx.getHooksConfig();
    string exp_name("testlib1.so");
    ConstElementPtr exp_param(new StringElement("myparam"));
    libs.add(exp_name, exp_param);

    BasicHttpAuthConfigPtr auth(new BasicHttpAuthConfig());
    auth->setRealm("foobar");
    auth->add("foo", "", "bar", "");
    EXPECT_NO_THROW(ctx.setAuthConfig(auth));

    // Make a copy.
    ConfigPtr copy_base(ctx.clone());
    CtrlAgentCfgContextPtr copy = boost::dynamic_pointer_cast<CtrlAgentCfgContext>(copy_base);
    ASSERT_TRUE(copy);

    // Now check the values returned
    EXPECT_EQ(12345, copy->getHttpPort());
    EXPECT_EQ("bellatrix", copy->getHttpHost());

    // Check socket info
    ASSERT_TRUE(copy->getControlSocketInfo("d2"));
    ASSERT_TRUE(copy->getControlSocketInfo("dhcp4"));
    ASSERT_TRUE(copy->getControlSocketInfo("dhcp6"));
    EXPECT_EQ(socket1->str(), copy->getControlSocketInfo("d2")->str());
    EXPECT_EQ(socket2->str(), copy->getControlSocketInfo("dhcp4")->str());
    EXPECT_EQ(socket3->str(), copy->getControlSocketInfo("dhcp6")->str());

    // Check hook libs
    const HookLibsCollection& libs2 = copy->getHooksConfig().get();
    ASSERT_EQ(1, libs2.size());
    EXPECT_EQ(exp_name, libs2[0].libname_);
    ASSERT_TRUE(libs2[0].parameters_);
    EXPECT_EQ(exp_param->str(), libs2[0].parameters_->str());

    // Check authentication
    const HttpAuthConfigPtr& auth2 = copy->getAuthConfig();
    ASSERT_TRUE(auth2);
    EXPECT_EQ(auth->toElement()->str(), auth2->toElement()->str());
}


// Tests if the context can store and retrieve hook libs information.
TEST(CtrlAgentCfgMgr, contextHookParams) {
    CtrlAgentCfgContext ctx;

    // By default there should be no hooks.
    HooksConfig& libs = ctx.getHooksConfig();
    EXPECT_TRUE(libs.get().empty());

    libs.add("libone.so", ConstElementPtr());
    libs.add("libtwo.so", Element::fromJSON("{\"foo\": true}"));
    libs.add("libthree.so", Element::fromJSON("{\"bar\": 42}"));

    const HooksConfig& stored_libs = ctx.getHooksConfig();
    EXPECT_EQ(3, stored_libs.get().size());

    // @todo add a == operator to HooksConfig
    EXPECT_EQ(libs.get(), stored_libs.get());
}

// Test if the context can store and retrieve basic HTTP authentication
// configuration.
TEST(CtrlAgentCfgMgr, contextAuthConfig) {
    CtrlAgentCfgContext ctx;

    // By default there should be no authentication.
    EXPECT_FALSE(ctx.getAuthConfig());
    BasicHttpAuthConfigPtr auth(new BasicHttpAuthConfig());
    EXPECT_NO_THROW(ctx.setAuthConfig(auth));

    auth->setRealm("foobar");
    auth->add("foo", "", "bar", "");
    auth->add("test", "", "123\xa3", "");

    const HttpAuthConfigPtr& stored_auth = ctx.getAuthConfig();
    ASSERT_TRUE(stored_auth);
    EXPECT_FALSE(stored_auth->empty());
    EXPECT_EQ(auth->toElement()->str(), stored_auth->toElement()->str());
}

// Test if the context can store and retrieve basic HTTP authentication
// configuration using files.
TEST(CtrlAgentCfgMgr, contextAuthConfigFile) {
    CtrlAgentCfgContext ctx;

    // By default there should be no authentication.
    EXPECT_FALSE(ctx.getAuthConfig());
    BasicHttpAuthConfigPtr auth(new BasicHttpAuthConfig());
    EXPECT_NO_THROW(ctx.setAuthConfig(auth));

    auth->setRealm("foobar");
    auth->setDirectory("/tmp");
    auth->add("", "foo", "", "bar");
    auth->add("", "test", "", "pwd");

    const HttpAuthConfigPtr& stored_auth = ctx.getAuthConfig();
    ASSERT_TRUE(stored_auth);
    EXPECT_FALSE(stored_auth->empty());
    EXPECT_EQ(auth->toElement()->str(), stored_auth->toElement()->str());
}

/// Control Agent configurations used in tests.
const char* AGENT_CONFIGS[] = {

    // configuration 0: empty (nothing specified)
    "{ }",

    // Configuration 1: http parameters only (no control sockets, not hooks)
    "{   \"http-host\": \"betelgeuse\",\n"
    "    \"http-port\": 8001\n"
    "}",

    // Configuration 2: http and 1 socket
    "{\n"
    "    \"http-host\": \"betelgeuse\",\n"
    "    \"http-port\": 8001,\n"
    "    \"control-sockets\": {\n"
    "        \"dhcp4\": {\n"
    "            \"socket-name\": \"socket-v4\"\n"
    "        }\n"
    "    }\n"
    "}",

    // Configuration 3: http and all 3 sockets
    "{\n"
    "    \"http-host\": \"betelgeuse\",\n"
    "    \"http-port\": 8001,\n"
    "    \"control-sockets\": {\n"
    "        \"dhcp4\": {\n"
    "            \"socket-name\": \"socket-v4\"\n"
    "        },\n"
    "        \"dhcp6\": {\n"
    "            \"socket-name\": \"socket-v6\"\n"
    "        },\n"
    "        \"d2\": {\n"
    "            \"socket-name\": \"socket-d2\"\n"
    "        }\n"
    "   }\n"
    "}",

    // Configuration 4: http, 1 socket and hooks
    // CA is able to load hook libraries that augment its operation.
    // The primary functionality is the ability to add new commands.
    "{\n"
    "    \"http-host\": \"betelgeuse\",\n"
    "    \"http-port\": 8001,\n"
    "    \"control-sockets\": {\n"
    "        \"dhcp4\": {\n"
    "            \"socket-name\": \"socket-v4\"\n"
    "        }\n"
    "   },\n"
    "    \"hooks-libraries\": ["
    "        {"
    "          \"library\": \"%LIBRARY%\","
    "              \"parameters\": {\n"
    "              \"param1\": \"foo\"\n"
    "            }\n"
    "        }\n"
    "     ]\n"
    "}",

    // Configuration 5: http and 1 socket (d2 only)
    "{\n"
    "    \"http-host\": \"betelgeuse\",\n"
    "    \"http-port\": 8001,\n"
    "    \"control-sockets\": {\n"
    "        \"d2\": {\n"
    "            \"socket-name\": \"socket-d2\"\n"
    "        }\n"
    "    }\n"
    "}",

    // Configuration 6: http and 1 socket (dhcp6 only)
    "{\n"
    "    \"http-host\": \"betelgeuse\",\n"
    "    \"http-port\": 8001,\n"
    "    \"control-sockets\": {\n"
    "        \"dhcp6\": {\n"
    "            \"socket-name\": \"socket-v6\"\n"
    "        }\n"
    "    }\n"
    "}",

    // Configuration 7: http, 1 socket and authentication
    "{\n"
    "    \"http-host\": \"betelgeuse\",\n"
    "    \"http-port\": 8001,\n"
    "    \"authentication\": {\n"
    "        \"type\": \"basic\",\n"
    "        \"realm\": \"foobar\",\n"
    "        \"clients\": ["
    "            {"
    "              \"user\": \"foo\",\n"
    "              \"password\": \"bar\"\n"
    "            },{\n"
    "              \"user\": \"test\",\n"
    "              \"password\": \"123\\u00a3\"\n"
    "            }\n"
    "         ]\n"
    "    },\n"
    "    \"control-sockets\": {\n"
    "        \"dhcp4\": {\n"
    "            \"socket-name\": \"socket-v4\"\n"
    "        }\n"
    "    }\n"
    "}",

    // Configuration 8: http and 2 sockets with user contexts and comments
    "{\n"
    "    \"user-context\": { \"comment\": \"Indirect comment\" },\n"
    "    \"http-host\": \"betelgeuse\",\n"
    "    \"http-port\": 8001,\n"
    "    \"http-headers\": [ {\n"
    "        \"comment\": \"HSTS header\",\n"
    "        \"name\": \"Strict-Transport-Security\",\n"
    "        \"value\": \"max-age=31536000\"\n"
    "    } ],\n"
    "    \"authentication\": {\n"
    "        \"comment\": \"basic HTTP authentication\",\n"
    "        \"type\": \"basic\",\n"
    "        \"realm\": \"foobar\",\n"
    "        \"clients\": ["
    "            {"
    "              \"comment\": \"foo is authorized\",\n"
    "              \"user\": \"foo\",\n"
    "              \"password\": \"bar\"\n"
    "            },{\n"
    "              \"user\": \"test\",\n"
    "              \"user-context\": { \"no password\": true }\n"
    "            }\n"
    "         ]\n"
    "    },\n"
    "    \"control-sockets\": {\n"
    "        \"dhcp4\": {\n"
    "            \"comment\": \"dhcp4 socket\",\n"
    "            \"socket-name\": \"socket-v4\"\n"
    "        },\n"
    "        \"dhcp6\": {\n"
    "            \"socket-name\": \"socket-v6\",\n"
    "            \"user-context\": { \"version\": 1 }\n"
    "        }\n"
    "    }\n"
    "}",

    // Configuration 9: https aka http over TLS
    "{\n"
    "    \"http-host\": \"betelgeuse\",\n"
    "    \"http-port\": 8001,\n"
    "    \"trust-anchor\": \"my-ca\",\n"
    "    \"cert-file\": \"my-cert\",\n"
    "    \"key-file\": \"my-key\",\n"
    "    \"cert-required\": false\n"
    "}",

    // Configuration 10: http, 1 socket and authentication using files
    "{\n"
    "    \"http-host\": \"betelgeuse\",\n"
    "    \"http-port\": 8001,\n"
    "    \"authentication\": {\n"
    "        \"type\": \"basic\",\n"
    "        \"realm\": \"foobar\",\n"
    "        \"directory\": \"" CA_TEST_DATA_DIR "\",\n"
    "        \"clients\": ["
    "            {"
    "              \"user-file\": \"hiddenu\",\n"
    "              \"password-file\": \"hiddenp\"\n"
    "            },{\n"
    "              \"password-file\": \"hiddens\"\n"
    "            }\n"
    "         ]\n"
    "    },\n"
    "    \"control-sockets\": {\n"
    "        \"dhcp4\": {\n"
    "            \"socket-name\": \"socket-v4\"\n"
    "        }\n"
    "    }\n"
    "}"
};

/// @brief Class used for testing CfgMgr
class AgentParserTest : public isc::process::ConfigParseTest {
public:
    virtual void SetUp() {
        resetHooksPath();
        setSocketTestPath();
        file::PathChecker::enableEnforcement(false);
    }

    virtual void TearDown() {
        resetHooksPath();
        resetSocketPath();
        file::PathChecker::enableEnforcement(true);
    }

    /// @brief Sets the Hooks path from which hooks can be loaded.
    /// @param explicit_path path to use as the hooks path.
    void setHooksTestPath(const std::string explicit_path = "") {
        HooksLibrariesParser::getHooksPath(true,
                                           (!explicit_path.empty() ?
                                            explicit_path : CA_HOOKS_TEST_PATH));
    }

    /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
    void resetHooksPath() {
        HooksLibrariesParser::getHooksPath(true);
    }

    /// @brief Sets the path in which the socket can be created.
    /// @param explicit_path path to use as the socket path.
    void setSocketTestPath(const std::string explicit_path = "") {
        auto path = UnixCommandConfig::getSocketPath(true, (!explicit_path.empty() ?
                                                     explicit_path : TEST_DATA_BUILDDIR));
        UnixCommandConfig::setSocketPathPerms(file::getPermissions(path));
    }

    /// @brief Resets the socket path to the default.
    void resetSocketPath() {
        UnixCommandConfig::getSocketPath(true);
        UnixCommandConfig::setSocketPathPerms();
    }

    /// @brief Tries to load input text as a configuration
    ///
    /// @param config text containing input configuration
    /// @param expected_answer expected result of configuration (0 = success)
    void configParse(const char* config, int expected_answer) {
        isc::agent::ParserContext parser;
        ConstElementPtr json = parser.parseString(config, ParserContext::PARSER_SUB_AGENT);

        EXPECT_NO_THROW(answer_ = cfg_mgr_.parse(json, false));
        EXPECT_TRUE(checkAnswer(expected_answer));
    }

    /// @brief Replaces %LIBRARY% with specified library name
    ///
    /// @param config input config text (should contain "%LIBRARY%" string)
    /// @param lib_name %LIBRARY% will be replaced with that name
    /// @return configuration text with library name replaced
    std::string pathReplacer(const char* config, const char* lib_name) {
        string txt(config);
        txt.replace(txt.find("%LIBRARY%"), strlen("%LIBRARY%"), string(lib_name));
        return (txt);
    }

    /// @brief Make expected contents of socket info with socket path added.
    ///
    /// @param name name of the socket
    /// @return expected string
    std::string makeSocketStr(const std::string& name) {
        std::ostringstream os;
        os << "{ \"socket-name\": \""
           << name << "\", \"socket-type\": \"unix\","
           << " \"validated-socket-name\": \""
           << UnixCommandConfig::getSocketPath() << "/" << name
           << "\" }";
        return (os.str());
    }

    /// Configuration Manager (used in tests)
    NakedAgentCfgMgr cfg_mgr_;
};

// This test verifies if an empty config is handled properly. In practice such
// a config makes little sense, but perhaps it's ok for a default deployment.
// Sadly, our bison parser requires at last one parameter to be present.
// Until we determine whether we want the empty config to be allowed or not,
// this test remains disabled.
TEST_F(AgentParserTest, DISABLED_configParseEmpty) {
    configParse(AGENT_CONFIGS[0], 0);
}

// This test checks if a config with only HTTP parameters is parsed properly.
TEST_F(AgentParserTest, configParseHttpOnly) {
    configParse(AGENT_CONFIGS[1], 0);

    CtrlAgentCfgContextPtr ctx = cfg_mgr_.getCtrlAgentCfgContext();
    ASSERT_TRUE(ctx);
    EXPECT_EQ("betelgeuse", ctx->getHttpHost());
    EXPECT_EQ(8001, ctx->getHttpPort());
}

// Tests if a single socket can be configured. BTW this test also checks
// if default value for socket-type is specified (the config doesn't have it,
// so the default value should be filed in).
TEST_F(AgentParserTest, configParseSocketDhcp4) {
    configParse(AGENT_CONFIGS[2], 0);

    CtrlAgentCfgContextPtr ctx = cfg_mgr_.getCtrlAgentCfgContext();
    ASSERT_TRUE(ctx);
    ConstElementPtr socket = ctx->getControlSocketInfo("dhcp4");
    ASSERT_TRUE(socket);
    EXPECT_EQ(makeSocketStr("socket-v4"), socket->str());
    EXPECT_FALSE(ctx->getControlSocketInfo("dhcp6"));
    EXPECT_FALSE(ctx->getControlSocketInfo("d2"));
}

// Tests if a single socket can be configured. BTW this test also checks
// if default value for socket-type is specified (the config doesn't have it,
// so the default value should be filed in).
TEST_F(AgentParserTest, configParseSocketD2) {
    configParse(AGENT_CONFIGS[5], 0);

    CtrlAgentCfgContextPtr ctx = cfg_mgr_.getCtrlAgentCfgContext();
    ASSERT_TRUE(ctx);
    ConstElementPtr socket = ctx->getControlSocketInfo("d2");
    ASSERT_TRUE(socket);
    EXPECT_EQ(makeSocketStr("socket-d2"), socket->str());
    EXPECT_FALSE(ctx->getControlSocketInfo("dhcp4"));
    EXPECT_FALSE(ctx->getControlSocketInfo("dhcp6"));
}

// Tests if a single socket can be configured. BTW this test also checks
// if default value for socket-type is specified (the config doesn't have it,
// so the default value should be filed in).
TEST_F(AgentParserTest, configParseSocketDhcp6) {
    configParse(AGENT_CONFIGS[6], 0);

    CtrlAgentCfgContextPtr ctx = cfg_mgr_.getCtrlAgentCfgContext();
    ASSERT_TRUE(ctx);
    ConstElementPtr socket = ctx->getControlSocketInfo("dhcp6");
    ASSERT_TRUE(socket);
    EXPECT_EQ(makeSocketStr("socket-v6"), socket->str());
    EXPECT_FALSE(ctx->getControlSocketInfo("dhcp4"));
    EXPECT_FALSE(ctx->getControlSocketInfo("d2"));
}

// This tests if all 3 sockets can be configured and makes sure the parser
// doesn't confuse them.
TEST_F(AgentParserTest, configParse3Sockets) {
    configParse(AGENT_CONFIGS[3], 0);
    CtrlAgentCfgContextPtr ctx = cfg_mgr_.getCtrlAgentCfgContext();
    ASSERT_TRUE(ctx);
    ConstElementPtr socket2 = ctx->getControlSocketInfo("d2");
    ConstElementPtr socket4 = ctx->getControlSocketInfo("dhcp4");
    ConstElementPtr socket6 = ctx->getControlSocketInfo("dhcp6");
    ASSERT_TRUE(socket2);
    EXPECT_EQ(makeSocketStr("socket-d2"), socket2->str());
    ASSERT_TRUE(socket4);
    EXPECT_EQ(makeSocketStr("socket-v4"), socket4->str());
    ASSERT_TRUE(socket6);
    EXPECT_EQ(makeSocketStr("socket-v6"), socket6->str());
}

// This test checks that the config file with hook library specified can be
// loaded. This one is a bit tricky, because the parser sanity checks the lib
// name. In particular, it checks if such a library exists. Therefore we
// can't use AGENT_CONFIGS[4] as is, but need to run it through path replacer.
TEST_F(AgentParserTest, configParseHooks) {
    setHooksTestPath();

    // Create the configuration with proper lib path.
    std::string cfg = pathReplacer(AGENT_CONFIGS[4], CALLOUT_LIBRARY);
    // The configuration should be successful.
    configParse(cfg.c_str(), 0);

    // The context now should have the library specified.
    CtrlAgentCfgContextPtr ctx = cfg_mgr_.getCtrlAgentCfgContext();
    const HookLibsCollection libs = ctx->getHooksConfig().get();
    ASSERT_EQ(1, libs.size());
    EXPECT_EQ(string(CALLOUT_LIBRARY), libs[0].libname_);
    ASSERT_TRUE(libs[0].parameters_);
    EXPECT_EQ("{ \"param1\": \"foo\" }", libs[0].parameters_->str());
}

// This test checks that the config file with basic HTTP authentication can be
// loaded.
TEST_F(AgentParserTest, configParseAuth) {
    configParse(AGENT_CONFIGS[7], 0);
    CtrlAgentCfgContextPtr ctx = cfg_mgr_.getCtrlAgentCfgContext();
    const HttpAuthConfigPtr& auth = ctx->getAuthConfig();
    ASSERT_TRUE(auth);
    const BasicHttpAuthConfigPtr& basic_auth =
        boost::dynamic_pointer_cast<BasicHttpAuthConfig>(auth);
    ASSERT_TRUE(basic_auth);

    // Check realm
    EXPECT_EQ("foobar", basic_auth->getRealm());

    // Check credentials
    auto credentials = basic_auth->getCredentialMap();
    EXPECT_EQ(2, credentials.size());
    std::string user;
    EXPECT_NO_THROW(user = credentials.at("Zm9vOmJhcg=="));
    EXPECT_EQ("foo", user);
    EXPECT_NO_THROW(user = credentials.at("dGVzdDoxMjPCow=="));
    EXPECT_EQ("test", user);

    // Check clients.
    BasicHttpAuthConfig expected;
    expected.setRealm("foobar");
    expected.add("foo", "", "bar", "");
    expected.add("test", "", "123\xa3", "");
    EXPECT_EQ(expected.toElement()->str(), basic_auth->toElement()->str());
}

// This test checks comments.
TEST_F(AgentParserTest, comments) {
    configParse(AGENT_CONFIGS[8], 0);
    CtrlAgentCfgContextPtr agent_ctx = cfg_mgr_.getCtrlAgentCfgContext();
    ASSERT_TRUE(agent_ctx);

    // Check global user context.
    ConstElementPtr ctx = agent_ctx->getContext();
    ASSERT_TRUE(ctx);
    ASSERT_EQ(1, ctx->size());
    ASSERT_TRUE(ctx->get("comment"));
    EXPECT_EQ("\"Indirect comment\"", ctx->get("comment")->str());

    // There is a DHCP4 control socket.
    ConstElementPtr socket4 = agent_ctx->getControlSocketInfo("dhcp4");
    ASSERT_TRUE(socket4);

    // Check DHCP4 control socket user context.
    ConstElementPtr ctx4 = socket4->get("user-context");
    ASSERT_TRUE(ctx4);
    ASSERT_EQ(1, ctx4->size());
    ASSERT_TRUE(ctx4->get("comment"));
    EXPECT_EQ("\"dhcp4 socket\"", ctx4->get("comment")->str());

    // There is a DHCP6 control socket.
    ConstElementPtr socket6 = agent_ctx->getControlSocketInfo("dhcp6");
    ASSERT_TRUE(socket6);

    // Check DHCP6 control socket user context.
    ConstElementPtr ctx6 = socket6->get("user-context");
    ASSERT_TRUE(ctx6);
    ASSERT_EQ(1, ctx6->size());
    ASSERT_TRUE(ctx6->get("version"));
    EXPECT_EQ("1", ctx6->get("version")->str());

    // Check HTTP header comment.
    const CfgHttpHeaders& headers = agent_ctx->getHttpHeaders();
    ASSERT_EQ(1, headers.size());
    ConstElementPtr ctx7 = headers[0].getContext();
    ASSERT_TRUE(ctx7);
    ASSERT_EQ(1, ctx7->size());
    ASSERT_TRUE(ctx7->get("comment"));
    EXPECT_EQ("\"HSTS header\"", ctx7->get("comment")->str());

    // Check authentication comment.
    const HttpAuthConfigPtr& auth = agent_ctx->getAuthConfig();
    ASSERT_TRUE(auth);
    ConstElementPtr ctx8 = auth->getContext();
    ASSERT_TRUE(ctx8);
    ASSERT_EQ(1, ctx8->size());
    ASSERT_TRUE(ctx8->get("comment"));
    EXPECT_EQ("\"basic HTTP authentication\"", ctx8->get("comment")->str());

    // Check basic HTTP authentication client comment.
    const BasicHttpAuthConfigPtr& basic_auth =
        boost::dynamic_pointer_cast<BasicHttpAuthConfig>(auth);
    ASSERT_TRUE(basic_auth);
    auto clients = basic_auth->getClientList();
    ASSERT_EQ(2, clients.size());
    ConstElementPtr ctx9 = clients.front().getContext();
    ASSERT_TRUE(ctx9);
    ASSERT_EQ(1, ctx9->size());
    ASSERT_TRUE(ctx9->get("comment"));
    EXPECT_EQ("\"foo is authorized\"", ctx9->get("comment")->str());

    // Check basic HTTP authentication client user context.
    ConstElementPtr ctx10 = clients.back().getContext();
    ASSERT_TRUE(ctx10);
    ASSERT_EQ(1, ctx10->size());
    ASSERT_TRUE(ctx10->get("no password"));
    EXPECT_EQ("true", ctx10->get("no password")->str());
}

// This test checks if a config with TLS parameters is parsed properly.
TEST_F(AgentParserTest, configParseTls) {
    configParse(AGENT_CONFIGS[9], 0);

    CtrlAgentCfgContextPtr ctx = cfg_mgr_.getCtrlAgentCfgContext();
    ASSERT_TRUE(ctx);
    EXPECT_EQ("my-ca", ctx->getTrustAnchor());
    EXPECT_EQ("my-cert", ctx->getCertFile());
    EXPECT_EQ("my-key", ctx->getKeyFile());
    EXPECT_FALSE(ctx->getCertRequired());
}

// This test checks that the config file with basic HTTP authentication
// using files can be loaded.
TEST_F(AgentParserTest, configParseAuthFile) {
    configParse(AGENT_CONFIGS[10], 0);
    CtrlAgentCfgContextPtr ctx = cfg_mgr_.getCtrlAgentCfgContext();
    const HttpAuthConfigPtr& auth = ctx->getAuthConfig();
    ASSERT_TRUE(auth);
    const BasicHttpAuthConfigPtr& basic_auth =
        boost::dynamic_pointer_cast<BasicHttpAuthConfig>(auth);
    ASSERT_TRUE(basic_auth);

    // Check realm
    EXPECT_EQ("foobar", basic_auth->getRealm());

    // Check directory
    EXPECT_EQ(std::string(CA_TEST_DATA_DIR), basic_auth->getDirectory());

    // Check credentials
    auto credentials = basic_auth->getCredentialMap();
    EXPECT_EQ(2, credentials.size());
    std::string user;
    EXPECT_NO_THROW(user = credentials.at("a2VhdGVzdDpLZWFUZXN0"));
    EXPECT_EQ("keatest", user);
    EXPECT_NO_THROW(user = credentials.at("a2VhOnRlc3Q="));
    EXPECT_EQ("kea", user);

    // Check clients.
    BasicHttpAuthConfig expected;
    expected.setRealm("foobar");
    expected.setDirectory(std::string(CA_TEST_DATA_DIR));
    expected.add("", "hiddenu", "", "hiddenp");
    expected.add("", "", "", "hiddens", true);
    EXPECT_EQ(expected.toElement()->str(), basic_auth->toElement()->str());
}

} // end of anonymous namespace
