| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- #include <gtest/gtest.h>
- #include <filesystem>
- #include <thread>
- #include "smartbotic/database/snapshot.hpp"
- namespace smartbotic::database::storage {
- namespace {
- /// Mock collection provider for testing
- class MockCollectionProvider : public ICollectionProvider {
- public:
- void AddCollection(std::unique_ptr<Collection> collection) {
- std::string name(collection->GetName());
- collections_.emplace(name, std::move(collection));
- }
- [[nodiscard]] auto GetCollectionNames() const -> std::vector<std::string> override {
- std::vector<std::string> names;
- names.reserve(collections_.size());
- for (const auto& [name, _] : collections_) {
- names.push_back(name);
- }
- return names;
- }
- [[nodiscard]] auto GetCollection(std::string_view name) const -> const Collection* override {
- auto it = collections_.find(std::string(name));
- if (it == collections_.end()) {
- return nullptr;
- }
- return it->second.get();
- }
- [[nodiscard]] auto GetAllMetadata() const -> std::unordered_map<std::string, CollectionMeta> override {
- std::unordered_map<std::string, CollectionMeta> result;
- for (const auto& [name, collection] : collections_) {
- result.emplace(name, collection->GetMetadata());
- }
- return result;
- }
- private:
- std::unordered_map<std::string, std::unique_ptr<Collection>> collections_;
- };
- class SnapshotTest : public ::testing::Test {
- protected:
- void SetUp() override {
- // Use a unique test directory for each test
- test_dir_ = std::filesystem::temp_directory_path() / ("snapshot_test_" + std::to_string(std::time(nullptr)));
- std::filesystem::create_directories(test_dir_);
- }
- void TearDown() override {
- // Clean up test directory
- std::error_code ec;
- std::filesystem::remove_all(test_dir_, ec);
- }
- [[nodiscard]] auto CreateConfig() -> SnapshotConfig {
- SnapshotConfig config;
- config.snapshot_dir = test_dir_.string();
- config.interval = std::chrono::seconds(3600); // Long interval for tests
- config.change_threshold = 0; // Disable threshold for tests
- config.enabled = true;
- return config;
- }
- std::filesystem::path test_dir_;
- };
- // ============================================================================
- // Basic Snapshot Tests
- // ============================================================================
- TEST_F(SnapshotTest, CreateSnapshotSync_EmptyDatabase) {
- auto config = CreateConfig();
- SnapshotManager manager(config);
- auto provider = std::make_shared<MockCollectionProvider>();
- ASSERT_TRUE(manager.Start(provider));
- EXPECT_EQ(manager.GetStatus(), SnapshotManagerStatus::kRunning);
- auto result = manager.CreateSnapshotSync();
- ASSERT_TRUE(result.IsOk());
- EXPECT_EQ(result.value->collections, 0);
- EXPECT_EQ(result.value->documents, 0);
- // Snapshot file should exist
- EXPECT_TRUE(manager.SnapshotExists());
- manager.Stop();
- EXPECT_EQ(manager.GetStatus(), SnapshotManagerStatus::kStopped);
- }
- TEST_F(SnapshotTest, CreateSnapshotSync_WithCollections) {
- auto config = CreateConfig();
- SnapshotManager manager(config);
- auto provider = std::make_shared<MockCollectionProvider>();
- // Create a collection with some documents
- auto users = std::make_unique<Collection>("users");
- (void)users->Create({{"name", "Alice"}, {"email", "alice@example.com"}}, "user-1");
- (void)users->Create({{"name", "Bob"}, {"email", "bob@example.com"}}, "user-2");
- provider->AddCollection(std::move(users));
- // Create another collection
- auto posts = std::make_unique<Collection>("posts");
- (void)posts->Create({{"title", "Hello World"}, {"content", "First post"}}, "post-1");
- provider->AddCollection(std::move(posts));
- ASSERT_TRUE(manager.Start(provider));
- auto result = manager.CreateSnapshotSync();
- ASSERT_TRUE(result.IsOk());
- EXPECT_EQ(result.value->collections, 2);
- EXPECT_EQ(result.value->documents, 3);
- EXPECT_GT(result.value->bytes_written, 0);
- manager.Stop();
- }
- TEST_F(SnapshotTest, LoadSnapshot_EmptyFile) {
- auto config = CreateConfig();
- SnapshotManager manager(config);
- // No snapshot file exists
- EXPECT_FALSE(manager.SnapshotExists());
- auto result = manager.LoadSnapshot();
- ASSERT_TRUE(result.IsOk());
- EXPECT_TRUE(result.value->empty());
- }
- TEST_F(SnapshotTest, LoadSnapshot_RestoresData) {
- auto config = CreateConfig();
- // Create and save a snapshot
- {
- SnapshotManager manager(config);
- auto provider = std::make_shared<MockCollectionProvider>();
- auto users = std::make_unique<Collection>("users");
- (void)users->Create({{"name", "Alice"}, {"age", 30}}, "user-1");
- (void)users->Create({{"name", "Bob"}, {"age", 25}}, "user-2");
- provider->AddCollection(std::move(users));
- ASSERT_TRUE(manager.Start(provider));
- auto result = manager.CreateSnapshotSync();
- ASSERT_TRUE(result.IsOk());
- manager.Stop();
- }
- // Load the snapshot with a new manager
- {
- SnapshotManager manager(config);
- auto result = manager.LoadSnapshot();
- ASSERT_TRUE(result.IsOk());
- EXPECT_EQ(result.value->size(), 1);
- auto it = result.value->find("users");
- ASSERT_NE(it, result.value->end());
- const auto& [meta, documents] = it->second;
- EXPECT_EQ(meta.name, "users");
- EXPECT_EQ(documents.size(), 2);
- // Verify document data
- bool found_alice = false;
- bool found_bob = false;
- for (const auto& doc : documents) {
- if (doc.GetId() == "user-1") {
- EXPECT_EQ(doc.GetData()["name"], "Alice");
- EXPECT_EQ(doc.GetData()["age"], 30);
- found_alice = true;
- } else if (doc.GetId() == "user-2") {
- EXPECT_EQ(doc.GetData()["name"], "Bob");
- EXPECT_EQ(doc.GetData()["age"], 25);
- found_bob = true;
- }
- }
- EXPECT_TRUE(found_alice);
- EXPECT_TRUE(found_bob);
- }
- }
- TEST_F(SnapshotTest, PreservesCollectionMetadata) {
- auto config = CreateConfig();
- // Create a collection with custom metadata
- {
- SnapshotManager manager(config);
- auto provider = std::make_shared<MockCollectionProvider>();
- auto users = std::make_unique<Collection>("users");
- // Set TTL config
- TTLConfig ttl;
- ttl.enabled = true;
- ttl.ttl_seconds = 3600;
- ttl.field_name = "_createdAt";
- users->SetTTLConfig(ttl);
- // Set encryption config
- EncryptionConfig encryption;
- encryption.encrypted_fields = {"password", "ssn"};
- encryption.key_id = "key-001";
- users->SetEncryptionConfig(encryption);
- (void)users->Create({{"name", "Test"}}, "user-1");
- provider->AddCollection(std::move(users));
- ASSERT_TRUE(manager.Start(provider));
- auto result = manager.CreateSnapshotSync();
- ASSERT_TRUE(result.IsOk());
- manager.Stop();
- }
- // Load and verify metadata
- {
- SnapshotManager manager(config);
- auto result = manager.LoadSnapshot();
- ASSERT_TRUE(result.IsOk());
- auto it = result.value->find("users");
- ASSERT_NE(it, result.value->end());
- const auto& meta = it->second.first;
- EXPECT_EQ(meta.name, "users");
- // Check TTL config
- EXPECT_TRUE(meta.ttl_config.enabled);
- EXPECT_EQ(meta.ttl_config.ttl_seconds, 3600);
- EXPECT_EQ(meta.ttl_config.field_name, "_createdAt");
- // Check encryption config
- EXPECT_EQ(meta.encryption.key_id, "key-001");
- EXPECT_TRUE(meta.encryption.encrypted_fields.contains("password"));
- EXPECT_TRUE(meta.encryption.encrypted_fields.contains("ssn"));
- }
- }
- // ============================================================================
- // Async and Background Thread Tests
- // ============================================================================
- TEST_F(SnapshotTest, StartWithNullProvider_Fails) {
- auto config = CreateConfig();
- SnapshotManager manager(config);
- EXPECT_FALSE(manager.Start(nullptr));
- EXPECT_EQ(manager.GetStatus(), SnapshotManagerStatus::kStopped);
- }
- TEST_F(SnapshotTest, DoubleStart_Fails) {
- auto config = CreateConfig();
- SnapshotManager manager(config);
- auto provider = std::make_shared<MockCollectionProvider>();
- EXPECT_TRUE(manager.Start(provider));
- EXPECT_FALSE(manager.Start(provider));
- manager.Stop();
- }
- TEST_F(SnapshotTest, RequestSnapshot_TriggersAsync) {
- auto config = CreateConfig();
- config.interval = std::chrono::seconds(3600); // Very long interval
- SnapshotManager manager(config);
- auto provider = std::make_shared<MockCollectionProvider>();
- auto users = std::make_unique<Collection>("users");
- (void)users->Create({{"name", "Test"}}, "user-1");
- provider->AddCollection(std::move(users));
- ASSERT_TRUE(manager.Start(provider));
- EXPECT_FALSE(manager.SnapshotExists());
- // Request an async snapshot
- manager.RequestSnapshot();
- // Wait for the snapshot to complete (with timeout)
- int attempts = 0;
- while (!manager.SnapshotExists() && attempts < 50) {
- std::this_thread::sleep_for(std::chrono::milliseconds(100));
- ++attempts;
- }
- EXPECT_TRUE(manager.SnapshotExists());
- EXPECT_EQ(manager.GetPendingChanges(), 0);
- manager.Stop();
- }
- TEST_F(SnapshotTest, ChangeThreshold_TriggersSnapshot) {
- auto config = CreateConfig();
- config.interval = std::chrono::seconds(3600); // Very long interval
- config.change_threshold = 5; // Trigger after 5 changes
- SnapshotManager manager(config);
- auto provider = std::make_shared<MockCollectionProvider>();
- auto users = std::make_unique<Collection>("users");
- provider->AddCollection(std::move(users));
- ASSERT_TRUE(manager.Start(provider));
- EXPECT_FALSE(manager.SnapshotExists());
- // Notify changes below threshold
- manager.NotifyChanges(3);
- EXPECT_EQ(manager.GetPendingChanges(), 3);
- // Add more to exceed threshold
- manager.NotifyChanges(3); // Now at 6, exceeds 5
- // Wait for the snapshot to complete
- int attempts = 0;
- while (!manager.SnapshotExists() && attempts < 50) {
- std::this_thread::sleep_for(std::chrono::milliseconds(100));
- ++attempts;
- }
- EXPECT_TRUE(manager.SnapshotExists());
- EXPECT_EQ(manager.GetPendingChanges(), 0);
- manager.Stop();
- }
- TEST_F(SnapshotTest, GracefulShutdown_CreatesSnapshot) {
- auto config = CreateConfig();
- config.interval = std::chrono::seconds(3600); // Very long interval
- SnapshotManager manager(config);
- auto provider = std::make_shared<MockCollectionProvider>();
- auto users = std::make_unique<Collection>("users");
- (void)users->Create({{"name", "Final"}}, "user-1");
- provider->AddCollection(std::move(users));
- ASSERT_TRUE(manager.Start(provider));
- EXPECT_FALSE(manager.SnapshotExists());
- // Stop should create a final snapshot
- manager.Stop();
- EXPECT_TRUE(manager.SnapshotExists());
- EXPECT_EQ(manager.GetStatus(), SnapshotManagerStatus::kStopped);
- // Verify the snapshot contains the data
- auto result = manager.LoadSnapshot();
- ASSERT_TRUE(result.IsOk());
- EXPECT_EQ(result.value->size(), 1);
- }
- // ============================================================================
- // Statistics Tests
- // ============================================================================
- TEST_F(SnapshotTest, LastSnapshotStats_Updated) {
- auto config = CreateConfig();
- SnapshotManager manager(config);
- auto provider = std::make_shared<MockCollectionProvider>();
- auto users = std::make_unique<Collection>("users");
- (void)users->Create({{"name", "Test"}}, "user-1");
- provider->AddCollection(std::move(users));
- ASSERT_TRUE(manager.Start(provider));
- EXPECT_FALSE(manager.GetLastSnapshotStats().has_value());
- auto result = manager.CreateSnapshotSync();
- ASSERT_TRUE(result.IsOk());
- auto stats = manager.GetLastSnapshotStats();
- ASSERT_TRUE(stats.has_value());
- EXPECT_EQ(stats->collections, 1);
- EXPECT_EQ(stats->documents, 1);
- EXPECT_GT(stats->bytes_written, 0);
- manager.Stop();
- }
- // ============================================================================
- // Edge Cases
- // ============================================================================
- TEST_F(SnapshotTest, EmptyCollection) {
- auto config = CreateConfig();
- SnapshotManager manager(config);
- auto provider = std::make_shared<MockCollectionProvider>();
- provider->AddCollection(std::make_unique<Collection>("empty"));
- ASSERT_TRUE(manager.Start(provider));
- auto result = manager.CreateSnapshotSync();
- ASSERT_TRUE(result.IsOk());
- EXPECT_EQ(result.value->collections, 1);
- EXPECT_EQ(result.value->documents, 0);
- manager.Stop();
- // Reload and verify
- auto load_result = manager.LoadSnapshot();
- ASSERT_TRUE(load_result.IsOk());
- EXPECT_EQ(load_result.value->size(), 1);
- EXPECT_TRUE(load_result.value->at("empty").second.empty());
- }
- TEST_F(SnapshotTest, LargeDocuments) {
- auto config = CreateConfig();
- SnapshotManager manager(config);
- auto provider = std::make_shared<MockCollectionProvider>();
- auto data = std::make_unique<Collection>("data");
- // Create a document with a large string
- std::string large_content(100000, 'x');
- (void)data->Create({{"content", large_content}}, "large-doc");
- provider->AddCollection(std::move(data));
- ASSERT_TRUE(manager.Start(provider));
- auto result = manager.CreateSnapshotSync();
- ASSERT_TRUE(result.IsOk());
- manager.Stop();
- // Reload and verify
- auto load_result = manager.LoadSnapshot();
- ASSERT_TRUE(load_result.IsOk());
- const auto& docs = load_result.value->at("data").second;
- EXPECT_EQ(docs.size(), 1);
- EXPECT_EQ(docs[0].GetData()["content"].get<std::string>().size(), 100000);
- }
- TEST_F(SnapshotTest, SystemCollection) {
- auto config = CreateConfig();
- SnapshotManager manager(config);
- auto provider = std::make_shared<MockCollectionProvider>();
- // System collections start with underscore
- auto system_col = std::make_unique<Collection>("_sessions");
- (void)system_col->Create({{"token", "abc123"}}, "session-1");
- provider->AddCollection(std::move(system_col));
- ASSERT_TRUE(manager.Start(provider));
- auto result = manager.CreateSnapshotSync();
- ASSERT_TRUE(result.IsOk());
- manager.Stop();
- // Reload and verify system collection is preserved
- auto load_result = manager.LoadSnapshot();
- ASSERT_TRUE(load_result.IsOk());
- ASSERT_EQ(load_result.value->size(), 1);
- const auto& meta = load_result.value->at("_sessions").first;
- EXPECT_TRUE(meta.is_system);
- }
- } // namespace
- } // namespace smartbotic::database::storage
|