#include "db/database_manager.h" #include #include using namespace radiantwave::db; TEST(DatabaseManagerTest, InitializeAndMigrate) { std::string temp_path = (std::filesystem::temp_directory_path() / "test_rw_db1.db").string(); if (std::filesystem::exists(temp_path)) { std::filesystem::remove(temp_path); } DatabaseManager db; EXPECT_TRUE(db.initialize(temp_path)); EXPECT_TRUE(std::filesystem::exists(temp_path)); db.close(); std::filesystem::remove(temp_path); } TEST(DatabaseManagerTest, KeyValueStoreOperations) { std::string temp_path = (std::filesystem::temp_directory_path() / "test_rw_db2.db").string(); if (std::filesystem::exists(temp_path)) { std::filesystem::remove(temp_path); } DatabaseManager db; ASSERT_TRUE(db.initialize(temp_path)); EXPECT_FALSE(db.hasKV("device_token")); EXPECT_EQ(db.getKV("device_token", "default_token"), "default_token"); EXPECT_TRUE(db.setKV("device_token", "ABC-123-XYZ")); EXPECT_TRUE(db.hasKV("device_token")); EXPECT_EQ(db.getKV("device_token"), "ABC-123-XYZ"); // Upsert test EXPECT_TRUE(db.setKV("device_token", "NEW-TOKEN-789")); EXPECT_EQ(db.getKV("device_token"), "NEW-TOKEN-789"); // Delete test EXPECT_TRUE(db.deleteKV("device_token")); EXPECT_FALSE(db.hasKV("device_token")); db.close(); std::filesystem::remove(temp_path); } TEST(DatabaseManagerTest, ProclamationsAndDownloads) { std::string temp_path = (std::filesystem::temp_directory_path() / "test_rw_db3.db").string(); if (std::filesystem::exists(temp_path)) { std::filesystem::remove(temp_path); } DatabaseManager db; ASSERT_TRUE(db.initialize(temp_path)); ProclamationRecord proc; proc.id = "p1"; proc.title = "Peace"; proc.content = "I am peace."; proc.solfeggio_freq_hz = 528.0; EXPECT_TRUE(db.saveProclamation(proc)); auto active = db.getActiveProclamations(); ASSERT_EQ(active.size(), 1u); EXPECT_EQ(active[0].title, "Peace"); EXPECT_DOUBLE_EQ(active[0].solfeggio_freq_hz, 528.0); AssetDownloadRecord dl; dl.id = "a1"; dl.filename = "audio1.wav"; dl.url = "https://example.com/audio1.wav"; dl.status = "pending"; EXPECT_TRUE(db.updateAssetDownload(dl)); auto pending = db.getPendingAssetDownloads(); ASSERT_EQ(pending.size(), 1u); EXPECT_EQ(pending[0].filename, "audio1.wav"); EXPECT_TRUE(db.logEvent("INFO", "Test", "All systems operational")); db.close(); std::filesystem::remove(temp_path); } namespace { // Helper: build a fresh in-memory-less temp DatabaseManager. struct DbFixture { std::string path; DatabaseManager db; DbFixture(const char* tag) { path = (std::filesystem::temp_directory_path() / tag).string(); if (std::filesystem::exists(path)) { std::filesystem::remove(path); } db.initialize(path); } ~DbFixture() { db.close(); std::filesystem::remove(path); } }; ProclamationRecord makeProc(const std::string& id, const std::string& title, int cat) { ProclamationRecord p; p.id = id; p.title = title; p.content = "content of " + title; p.category_id = cat; return p; } } // namespace TEST(DatabaseManagerTest, SyncProclamationsPreservesSelectionAndDeletesRemoved) { const std::string defId = "default-1"; DbFixture fx("test_rw_db_sync.db"); ProclamationRecord a = makeProc("a", "Alpha", 1); ProclamationRecord b = makeProc("b", "Beta", 1); // First sync inserts both. The incoming `selected` flag is never trusted. ASSERT_TRUE(fx.db.syncProclamations({a, b}, defId)); // Simulate the user picking "a". ASSERT_TRUE(fx.db.setProclamationSelected("a", true)); // Re-sync: "a" stays selected, "b" is removed because the API no longer // returns it. ASSERT_TRUE(fx.db.syncProclamations({a}, defId)); auto all = fx.db.getAllProclamations(); ASSERT_EQ(all.size(), 1u); EXPECT_EQ(all[0].id, "a"); EXPECT_TRUE(all[0].selected); } TEST(DatabaseManagerTest, RequiredLevelIdRoundTripsThroughSync) { const std::string defId = "default-1"; DbFixture fx("test_rw_db_level.db"); ProclamationRecord def = makeProc(defId, "Default", 1); def.required_level_id = 1; ProclamationRecord elite = makeProc("elite", "Elite Only", 2); elite.required_level_id = 3; ASSERT_TRUE(fx.db.syncProclamations({def, elite}, defId)); auto all = fx.db.getAllProclamations(); ASSERT_EQ(all.size(), 2u); for (const auto& p : all) { EXPECT_EQ(p.required_level_id, p.id == "elite" ? 3 : 1); } // A re-sync with the same records keeps the level (and selection). ASSERT_TRUE(fx.db.syncProclamations({def, elite}, defId)); auto again = fx.db.getAllProclamations(); ASSERT_EQ(again.size(), 2u); for (const auto& p : again) { EXPECT_EQ(p.required_level_id, p.id == "elite" ? 3 : 1); } } TEST(DatabaseManagerTest, SyncForcesDefaultProclamationSelected) { const std::string defId = "default-1"; DbFixture fx("test_rw_db_def.db"); ProclamationRecord def = makeProc(defId, "Default", 3); ProclamationRecord other = makeProc("x", "Other", 3); def.selected = false; ASSERT_TRUE(fx.db.syncProclamations({def, other}, defId)); ASSERT_TRUE(fx.db.ensureDefaultProclamationSelected(defId)); auto all = fx.db.getAllProclamations(); for (const auto& p : all) { if (p.id == defId) { EXPECT_TRUE(p.selected); } } } TEST(DatabaseManagerTest, SaveSelectedProclamationsClearsOthers) { const std::string defId = "default-1"; DbFixture fx("test_rw_db_save.db"); ProclamationRecord def = makeProc(defId, "Default", 1); ProclamationRecord a = makeProc("a", "A", 1); ProclamationRecord b = makeProc("b", "B", 1); ASSERT_TRUE(fx.db.syncProclamations({def, a, b}, defId)); ASSERT_TRUE(fx.db.setProclamationSelected("a", true)); // Deselect everything except a; the default must still end up selected. ASSERT_TRUE(fx.db.saveSelectedProclamations({"a"}, defId)); auto all = fx.db.getAllProclamations(); for (const auto& p : all) { bool expected = (p.id == "a" || p.id == defId); EXPECT_EQ(p.selected, expected) << "proclamation " << p.id; } } TEST(DatabaseManagerTest, ReplaceAndReadCategories) { DbFixture fx("test_rw_db_cats.db"); std::vector cats; cats.push_back({3, "Root", 0, 1}); cats.push_back({7, "Child", 3, 2}); ASSERT_TRUE(fx.db.replaceCategories(cats)); auto all = fx.db.getAllCategories(); ASSERT_EQ(all.size(), 2u); EXPECT_EQ(all[0].id, 3); EXPECT_EQ(all[0].parent_id, 0); EXPECT_EQ(all[1].id, 7); EXPECT_EQ(all[1].parent_id, 3); // Replacing wipes previous categories. ASSERT_TRUE(fx.db.replaceCategories({})); EXPECT_EQ(fx.db.getAllCategories().size(), 0u); } TEST(DatabaseManagerTest, SyncAudioFilesUpsertsAndDeletesMissing) { DbFixture fx("test_rw_db_audio.db"); std::vector first = { {0, "174 Hz Solfeggio", "174Hz.wav", 1, 1}, {0, "Original Audio", "OriginalAudio.wav", 2, 0}, {0, "Alpha Waves", "AlphaWaves.wav", 3, 13}, }; ASSERT_TRUE(fx.db.syncAudioFiles(first)); auto rows = fx.db.getAudioFiles(); ASSERT_EQ(rows.size(), 3u); EXPECT_EQ(rows[0].filename, "174Hz.wav"); EXPECT_EQ(rows[0].display_name, "174 Hz Solfeggio"); EXPECT_EQ(rows[0].display_order, 1); EXPECT_EQ(rows[0].solfeggio_frequency_id, 1); EXPECT_EQ(rows[1].filename, "OriginalAudio.wav"); EXPECT_EQ(rows[1].solfeggio_frequency_id, 0); EXPECT_EQ(rows[2].filename, "AlphaWaves.wav"); EXPECT_EQ(rows[2].solfeggio_frequency_id, 13); // Manifest update: renamed display name, reordered, one file removed. std::vector second = { {0, "174 Hz Solfeggio (Updated)", "174Hz.wav", 2, 1}, {0, "Alpha Waves", "AlphaWaves.wav", 1, 13}, }; ASSERT_TRUE(fx.db.syncAudioFiles(second)); rows = fx.db.getAudioFiles(); ASSERT_EQ(rows.size(), 2u); EXPECT_EQ(rows[0].filename, "AlphaWaves.wav"); EXPECT_EQ(rows[0].display_order, 1); EXPECT_EQ(rows[1].filename, "174Hz.wav"); EXPECT_EQ(rows[1].display_name, "174 Hz Solfeggio (Updated)"); EXPECT_EQ(rows[1].display_order, 2); } TEST(DatabaseManagerTest, SyncAudioFilesClearsEverythingWhenEmpty) { DbFixture fx("test_rw_db_audio_empty.db"); ASSERT_TRUE(fx.db.syncAudioFiles({{0, "Original Audio", "OriginalAudio.wav", 1, 0}})); ASSERT_EQ(fx.db.getAudioFiles().size(), 1u); ASSERT_TRUE(fx.db.syncAudioFiles({})); EXPECT_EQ(fx.db.getAudioFiles().size(), 0u); } TEST(DatabaseManagerTest, PalettesSeededAndRead) { DbFixture fx("test_rw_db_palette.db"); auto palette = fx.db.getPaletteForFrequencyId(5); // 528 Hz ASSERT_EQ(palette.size(), 7u); EXPECT_EQ(palette[0].r, 0x32); EXPECT_EQ(palette[0].g, 0xCD); EXPECT_EQ(palette[0].b, 0x32); EXPECT_EQ(palette[0].a, 255u); EXPECT_EQ(palette[6].r, 0xF0); EXPECT_EQ(palette[6].g, 0xFF); EXPECT_EQ(palette[6].b, 0xF0); auto red = fx.db.getPaletteForFrequencyId(2); // 396 Hz ASSERT_EQ(red.size(), 7u); EXPECT_EQ(red[0].r, 0x8B); EXPECT_EQ(red[0].g, 0x00); EXPECT_EQ(red[0].b, 0x00); } TEST(DatabaseManagerTest, UnknownFrequencyFallsBackTo528) { DbFixture fx("test_rw_db_palette_fb.db"); auto palette = fx.db.getPaletteForFrequencyId(9999); ASSERT_EQ(palette.size(), 7u); EXPECT_EQ(palette[0].r, 0x32); EXPECT_EQ(palette[0].g, 0xCD); EXPECT_EQ(palette[0].b, 0x32); } TEST(DatabaseManagerTest, PaletteSeedIsIdempotent) { DbFixture fx("test_rw_db_palette_seed.db"); // Re-initialize on the same file; seeding must not duplicate rows. fx.db.close(); ASSERT_TRUE(fx.db.initialize(fx.path)); auto palette = fx.db.getPaletteForFrequencyId(9); // 963 Hz ASSERT_EQ(palette.size(), 7u); EXPECT_EQ(palette[0].r, 0x94); EXPECT_EQ(palette[0].g, 0x00); EXPECT_EQ(palette[0].b, 0xD3); }