441 lines
14 KiB
C++
441 lines
14 KiB
C++
#include "db/database_manager.h"
|
|
#include <gtest/gtest.h>
|
|
#include <filesystem>
|
|
|
|
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, 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<ProclamationCategoryRecord> cats;
|
|
cats.push_back({3, "Spiritual", 1});
|
|
cats.push_back({7, "Physical", 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].name, "Spiritual");
|
|
EXPECT_EQ(all[0].sort_order, 1);
|
|
EXPECT_EQ(all[1].id, 7);
|
|
EXPECT_EQ(all[1].name, "Physical");
|
|
EXPECT_EQ(all[1].sort_order, 2);
|
|
|
|
// Replacing wipes previous categories.
|
|
ASSERT_TRUE(fx.db.replaceCategories({}));
|
|
EXPECT_EQ(fx.db.getAllCategories().size(), 0u);
|
|
}
|
|
|
|
TEST(DatabaseManagerTest, ReplaceAndReadGroups) {
|
|
DbFixture fx("test_rw_db_groups.db");
|
|
|
|
std::vector<ProclamationGroupRecord> groups;
|
|
groups.push_back({10, "Prayers", 2});
|
|
groups.push_back({4, "Affirmations", 1});
|
|
|
|
ASSERT_TRUE(fx.db.replaceGroups(groups));
|
|
|
|
auto all = fx.db.getAllGroups();
|
|
ASSERT_EQ(all.size(), 2u);
|
|
// Ordered by sort_order, not by id.
|
|
EXPECT_EQ(all[0].id, 4);
|
|
EXPECT_EQ(all[0].name, "Affirmations");
|
|
EXPECT_EQ(all[1].id, 10);
|
|
EXPECT_EQ(all[1].name, "Prayers");
|
|
|
|
ASSERT_TRUE(fx.db.replaceGroups({}));
|
|
EXPECT_EQ(fx.db.getAllGroups().size(), 0u);
|
|
}
|
|
|
|
TEST(DatabaseManagerTest, MigratesLegacySubCategoriesToGroups) {
|
|
const std::string path =
|
|
(std::filesystem::temp_directory_path() / "test_rw_db_migrate_v1.db").string();
|
|
if (std::filesystem::exists(path)) {
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
// Build a pre-v2 database by hand: a two-level category tree and a
|
|
// proclamation selected inside a sub-category.
|
|
{
|
|
sqlite3* raw = nullptr;
|
|
ASSERT_EQ(sqlite3_open(path.c_str(), &raw), SQLITE_OK);
|
|
const char* legacy =
|
|
"CREATE TABLE proclamations ("
|
|
" id TEXT PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL,"
|
|
" solfeggio_freq_hz REAL DEFAULT 528.0, category TEXT DEFAULT 'general',"
|
|
" is_active INTEGER DEFAULT 1, selected INTEGER DEFAULT 0,"
|
|
" category_id INTEGER DEFAULT 0, solfeggio_frequency_id INTEGER DEFAULT 0,"
|
|
" created_at DATETIME DEFAULT CURRENT_TIMESTAMP);"
|
|
"CREATE TABLE proclamation_categories ("
|
|
" id INTEGER PRIMARY KEY, name TEXT NOT NULL,"
|
|
" parent_id INTEGER DEFAULT 0, sort_order INTEGER DEFAULT 0);"
|
|
"INSERT INTO proclamation_categories (id, name, parent_id, sort_order)"
|
|
" VALUES (3,'Spiritual',0,1),(7,'Inner',3,2);"
|
|
"INSERT INTO proclamations (id,title,content,category_id,solfeggio_frequency_id,selected)"
|
|
" VALUES ('p1','Peace','x',7,2,1),('p2','Root-level','y',3,5,0);";
|
|
char* err = nullptr;
|
|
int rc = sqlite3_exec(raw, legacy, nullptr, nullptr, &err);
|
|
if (err) sqlite3_free(err);
|
|
sqlite3_close(raw);
|
|
ASSERT_EQ(rc, SQLITE_OK);
|
|
}
|
|
|
|
DatabaseManager db;
|
|
ASSERT_TRUE(db.initialize(path));
|
|
|
|
// Each former sub-category becomes a group; only the root remains a category.
|
|
auto groups = db.getAllGroups();
|
|
ASSERT_EQ(groups.size(), 1u);
|
|
EXPECT_EQ(groups[0].id, 7);
|
|
EXPECT_EQ(groups[0].name, "Inner");
|
|
|
|
auto cats = db.getAllCategories();
|
|
ASSERT_EQ(cats.size(), 1u);
|
|
EXPECT_EQ(cats[0].id, 3);
|
|
EXPECT_EQ(cats[0].name, "Spiritual");
|
|
|
|
// Proclamations pointing at the old sub-category are remapped to the root
|
|
// category + the new group; selection is preserved.
|
|
auto procs = db.getAllProclamations();
|
|
ASSERT_EQ(procs.size(), 2u);
|
|
for (const auto& p : procs) {
|
|
EXPECT_EQ(p.category_id, 3) << "proclamation " << p.id;
|
|
if (p.id == "p1") {
|
|
EXPECT_EQ(p.group_id, 7);
|
|
EXPECT_TRUE(p.selected);
|
|
} else if (p.id == "p2") {
|
|
EXPECT_EQ(p.group_id, 0);
|
|
EXPECT_FALSE(p.selected);
|
|
}
|
|
}
|
|
|
|
// Re-running the migration is a no-op (user_version gate + idempotent SQL).
|
|
db.close();
|
|
ASSERT_TRUE(db.initialize(path));
|
|
EXPECT_EQ(db.getAllGroups().size(), 1u);
|
|
EXPECT_EQ(db.getAllCategories().size(), 1u);
|
|
EXPECT_EQ(db.getAllProclamations().size(), 2u);
|
|
|
|
db.close();
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
TEST(DatabaseManagerTest, SyncAudioFilesUpsertsAndDeletesMissing) {
|
|
DbFixture fx("test_rw_db_audio.db");
|
|
|
|
std::vector<AudioFileRecord> 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<AudioFileRecord> 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, ReplaceAndReadScheduleBlocks) {
|
|
DbFixture fx("test_rw_db_schedule.db");
|
|
|
|
ScheduleBlockRecord morning;
|
|
morning.id = "b1";
|
|
morning.start_minutes = 360;
|
|
morning.end_minutes = 480;
|
|
morning.overnight = false;
|
|
morning.duration_minutes = 120;
|
|
morning.dynamic_audio = false;
|
|
morning.proclamation_ids = {"p2", "p1"};
|
|
|
|
ScheduleBlockRecord night;
|
|
night.id = "b2";
|
|
night.start_minutes = 1260;
|
|
night.end_minutes = 540;
|
|
night.overnight = true;
|
|
night.duration_minutes = 720;
|
|
night.dynamic_audio = true;
|
|
night.proclamation_ids = {"p3"};
|
|
|
|
ASSERT_TRUE(fx.db.replaceScheduleBlocks({morning, night}));
|
|
|
|
auto rows = fx.db.getScheduleBlocks();
|
|
ASSERT_EQ(rows.size(), 2u);
|
|
|
|
EXPECT_EQ(rows[0].id, "b1");
|
|
EXPECT_EQ(rows[0].start_minutes, 360);
|
|
EXPECT_EQ(rows[0].end_minutes, 480);
|
|
EXPECT_FALSE(rows[0].overnight);
|
|
EXPECT_EQ(rows[0].duration_minutes, 120);
|
|
EXPECT_FALSE(rows[0].dynamic_audio);
|
|
// Play order is preserved, not sorted.
|
|
ASSERT_EQ(rows[0].proclamation_ids.size(), 2u);
|
|
EXPECT_EQ(rows[0].proclamation_ids[0], "p2");
|
|
EXPECT_EQ(rows[0].proclamation_ids[1], "p1");
|
|
|
|
EXPECT_EQ(rows[1].id, "b2");
|
|
EXPECT_TRUE(rows[1].overnight);
|
|
EXPECT_TRUE(rows[1].dynamic_audio);
|
|
ASSERT_EQ(rows[1].proclamation_ids.size(), 1u);
|
|
EXPECT_EQ(rows[1].proclamation_ids[0], "p3");
|
|
|
|
// A re-sync replaces the whole schedule.
|
|
ASSERT_TRUE(fx.db.replaceScheduleBlocks({}));
|
|
EXPECT_EQ(fx.db.getScheduleBlocks().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);
|
|
}
|