#include "data/BackendFactory.hpp" #include "data/LedgerCache.hpp" #include "data/cassandra/Handle.hpp" #include "util/AsioContextTestFixture.hpp" #include "util/MockPrometheus.hpp" #include "util/config/ConfigConstraints.hpp" #include "util/config/ConfigDefinition.hpp" #include "util/config/ConfigFileJson.hpp" #include "util/config/ConfigValue.hpp" #include "util/config/Types.hpp" #include #include #include #include #include #include #include #include #include #include using namespace util::config; struct BackendCassandraFactoryTest : SyncAsioContextTest, util::prometheus::WithPrometheus { static constexpr auto kKEYSPACE = "factory_test"; static constexpr auto kPROVIDER = "cassandra"; protected: ClioConfigDefinition cfg_{ {"database.type", ConfigValue{ConfigType::String}.defaultValue("cassandra")}, {"database.cassandra.contact_points", ConfigValue{ConfigType::String}.defaultValue(TestGlobals::instance().backendHost)}, {"database.cassandra.secure_connect_bundle", ConfigValue{ConfigType::String}.optional()}, {"database.cassandra.port", ConfigValue{ConfigType::Integer}.optional()}, {"database.cassandra.keyspace", ConfigValue{ConfigType::String}.defaultValue(kKEYSPACE)}, {"database.cassandra.provider", ConfigValue{ConfigType::String}.defaultValue(kPROVIDER)}, {"database.cassandra.replication_factor", ConfigValue{ConfigType::Integer}.defaultValue(1)}, {"database.cassandra.table_prefix", ConfigValue{ConfigType::String}.optional()}, {"database.cassandra.max_write_requests_outstanding", ConfigValue{ConfigType::Integer}.defaultValue(10'000)}, {"database.cassandra.max_read_requests_outstanding", ConfigValue{ConfigType::Integer}.defaultValue(100'000)}, {"database.cassandra.threads", ConfigValue{ConfigType::Integer}.defaultValue( static_cast(std::thread::hardware_concurrency()) )}, {"database.cassandra.core_connections_per_host", ConfigValue{ConfigType::Integer}.defaultValue(1)}, {"database.cassandra.queue_size_io", ConfigValue{ConfigType::Integer}.optional()}, {"database.cassandra.write_batch_size", ConfigValue{ConfigType::Integer}.defaultValue(20)}, {"database.cassandra.connect_timeout", ConfigValue{ConfigType::Integer}.defaultValue(1).optional()}, {"database.cassandra.request_timeout", ConfigValue{ConfigType::Integer}.optional()}, {"database.cassandra.username", ConfigValue{ConfigType::String}.optional()}, {"database.cassandra.password", ConfigValue{ConfigType::String}.optional()}, {"database.cassandra.certfile", ConfigValue{ConfigType::String}.optional()}, {"read_only", ConfigValue{ConfigType::Boolean}.defaultValue(false)} }; void useConfig(std::string config) { auto jsonConfig = boost::json::parse(config).as_object(); auto const parseErrors = cfg_.parse(ConfigFileJson{jsonConfig}); if (parseErrors) { std::ranges::for_each(*parseErrors, [](auto const& error) { std::cout << error.error << std::endl; }); FAIL() << "Failed to parse config"; } } }; class BackendCassandraFactoryTestWithDB : public BackendCassandraFactoryTest { public: ~BackendCassandraFactoryTestWithDB() override { // drop the keyspace for next test data::cassandra::Handle const handle{TestGlobals::instance().backendHost}; EXPECT_TRUE(handle.connect()); handle.execute("DROP KEYSPACE " + std::string{kKEYSPACE}); } }; TEST_F(BackendCassandraFactoryTest, NoSuchBackend) { useConfig(R"JSON( {"database": {"type": "unknown"}} )JSON"); auto cache = data::LedgerCache{}; EXPECT_THROW(data::makeBackend(cfg_, cache), std::runtime_error); } TEST_F(BackendCassandraFactoryTest, CreateCassandraBackendDBDisconnect) { useConfig(R"JSON( {"database": { "type": "cassandra", "cassandra": { "contact_points": "127.0.0.2" } }} )JSON"); auto cache = data::LedgerCache{}; EXPECT_THROW(data::makeBackend(cfg_, cache), std::runtime_error); } TEST_F(BackendCassandraFactoryTestWithDB, CreateCassandraBackend) { { auto cache = data::LedgerCache{}; auto backend = data::makeBackend(cfg_, cache); EXPECT_TRUE(backend); // empty db does not have ledger range EXPECT_FALSE(backend->fetchLedgerRange()); // insert range table data::cassandra::Handle const handle{TestGlobals::instance().backendHost}; EXPECT_TRUE(handle.connect()); handle.execute( fmt::format( "INSERT INTO {}.ledger_range (is_latest, sequence) VALUES (False, 100)", kKEYSPACE ) ); handle.execute( fmt::format( "INSERT INTO {}.ledger_range (is_latest, sequence) VALUES (True, 500)", kKEYSPACE ) ); } { auto cache = data::LedgerCache{}; auto backend = data::makeBackend(cfg_, cache); EXPECT_TRUE(backend); auto const range = backend->fetchLedgerRange(); EXPECT_EQ(range->minSequence, 100); EXPECT_EQ(range->maxSequence, 500); } } TEST_F(BackendCassandraFactoryTestWithDB, CreateCassandraBackendReadOnlyWithEmptyDB) { useConfig(R"JSON( {"read_only": true} )JSON"); auto cache = data::LedgerCache{}; EXPECT_THROW(data::makeBackend(cfg_, cache), std::runtime_error); } TEST_F(BackendCassandraFactoryTestWithDB, CreateCassandraBackendReadOnlyWithDBReady) { auto cfgReadOnly = cfg_; ASSERT_FALSE(cfgReadOnly.parse( ConfigFileJson{boost::json::parse(R"JSON( {"read_only": true} )JSON").as_object()} )); auto cache = data::LedgerCache{}; EXPECT_TRUE(data::makeBackend(cfg_, cache)); EXPECT_TRUE(data::makeBackend(cfgReadOnly, cache)); }