use std::path::Path;
use modde_core::db::ModdeDb;
use modde_core::profile::{EnabledMod, Profile, ProfileSource};
use modde_core::resolver::GameId;
use modde_core::settings::{AppSettings, DatabaseSettings, DbBackend};
fn pg_url() -> Option<String> {
std::env::var("MODDE_TEST_PG_URL")
.ok()
.filter(|s| !s.is_empty())
}
async fn pg_db() -> Option<ModdeDb> {
let url = pg_url()?;
let settings = AppSettings {
database: DatabaseSettings {
backend: DbBackend::Postgres,
url: Some(url),
..DatabaseSettings::default()
},
..AppSettings::default()
};
Some(
ModdeDb::open_with_settings(&settings)
.await
.expect("failed to connect to MODDE_TEST_PG_URL"),
)
}
fn unique_game(tag: &str) -> GameId {
GameId::from(format!("pgtest-{tag}-{}", std::process::id()))
}
fn sample_profile(name: &str, game: &GameId) -> Profile {
Profile {
id: None,
name: name.to_string(),
game_id: game.clone(),
source: ProfileSource::Manual,
mods: vec![
EnabledMod {
mod_id: "mod_a".to_string(),
enabled: true,
version: Some("1.0".to_string()),
..Default::default()
},
EnabledMod {
mod_id: "mod_b".to_string(),
enabled: false,
..Default::default()
},
],
overrides: std::path::PathBuf::from("/tmp/overrides"),
load_order_rules: smallvec::smallvec![],
load_order_lock: None,
}
}
async fn cleanup(db: &ModdeDb, game: &GameId) {
for p in db.list_profiles(Some(game)).await.unwrap_or_default() {
let _ = db.delete_profile(&p.name, game).await;
}
}
#[tokio::test]
async fn pg_create_load_returning_id_and_bool_roundtrip() {
let Some(db) = pg_db().await else {
eprintln!("skipping: MODDE_TEST_PG_URL not set");
return;
};
let game = unique_game("crud");
cleanup(&db, &game).await;
let id = db
.create_profile(&sample_profile("p", &game))
.await
.unwrap();
assert!(id > 0, "RETURNING id should produce a positive id");
let loaded = db.load_profile("p", &game).await.unwrap();
assert_eq!(loaded.mods.len(), 2);
assert!(loaded.mods[0].enabled);
assert!(!loaded.mods[1].enabled);
cleanup(&db, &game).await;
}
#[tokio::test]
async fn pg_on_conflict_upsert() {
let Some(db) = pg_db().await else {
return;
};
let game = unique_game("upsert");
db.save_tool_config(&game, "mangohud", true, "{}")
.await
.unwrap();
db.save_tool_config(&game, "mangohud", false, "{\"fps\":1}")
.await
.unwrap();
let configs = db.load_tool_configs(&game).await.unwrap();
let mango: Vec<_> = configs.iter().filter(|c| c.tool_id == "mangohud").collect();
assert_eq!(mango.len(), 1, "ON CONFLICT should upsert a single row");
assert!(!mango[0].enabled, "upsert should apply the latest value");
assert_eq!(mango[0].settings_json, "{\"fps\":1}");
let _ = db.clear_applied_files(&game, "mangohud").await;
}
#[tokio::test]
async fn pg_cascade_delete_and_negative_nexus_id() {
let Some(db) = pg_db().await else {
return;
};
let game = unique_game("cascade");
cleanup(&db, &game).await;
let id = db
.create_profile(&sample_profile("p", &game))
.await
.unwrap();
db.assign_save(id, Path::new("/saves/pg-save1.ess"), Some("lbl"))
.await
.unwrap();
assert_eq!(db.list_saves(id).await.unwrap().len(), 1);
db.delete_profile("p", &game).await.unwrap();
assert_eq!(db.list_saves(id).await.unwrap().len(), 0);
let id2 = db
.create_profile(&sample_profile("q", &game))
.await
.unwrap();
db.set_mod_nexus_meta(
id2,
&modde_core::resolver::ModId::from("mod_a"),
modde_core::NexusModId::from(7),
modde_core::NexusFileId::from(7),
"skyrimspecialedition",
0,
)
.await
.unwrap();
let loaded = db.load_profile("q", &game).await.unwrap();
assert_eq!(
loaded
.mods
.iter()
.find(|m| m.mod_id == "mod_a")
.unwrap()
.nexus_mod_id,
Some(modde_core::NexusModId::from(7))
);
cleanup(&db, &game).await;
}