#include #include #include #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) { std::string name(collection->GetName()); collections_.emplace(name, std::move(collection)); } [[nodiscard]] auto GetCollectionNames() const -> std::vector override { std::vector 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 override { std::unordered_map result; for (const auto& [name, collection] : collections_) { result.emplace(name, collection->GetMetadata()); } return result; } private: std::unordered_map> 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(); 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(); // Create a collection with some documents auto users = std::make_unique("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("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(); auto users = std::make_unique("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(); auto users = std::make_unique("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(); 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(); auto users = std::make_unique("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(); auto users = std::make_unique("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(); auto users = std::make_unique("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(); auto users = std::make_unique("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(); provider->AddCollection(std::make_unique("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(); auto data = std::make_unique("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().size(), 100000); } TEST_F(SnapshotTest, SystemCollection) { auto config = CreateConfig(); SnapshotManager manager(config); auto provider = std::make_shared(); // System collections start with underscore auto system_col = std::make_unique("_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