//! Verification tests mapped to SRS-089's acceptance criteria: //! "Storage implementation uses an embedded local data store and //! migration mechanism." //! //! And to the PoC plan §2 exit criterion: //! "Schema, migration, and repository pattern are demonstrated." use rusqlite::Connection; use sqlite_storage_spike::{ migrate::{canonical_migrations, Migration, Migrator}, repo::NewBookmark, BookmarkRepository, LocalDatabaseRepository, SettingsRepository, }; // ---------- Migration mechanism ---------- #[test] fn migrator_brings_fresh_db_to_latest_version() { let mut conn = Connection::open_in_memory().unwrap(); let migrator = Migrator::new(canonical_migrations()).unwrap(); let final_v = migrator.run(&mut conn).unwrap(); assert_eq!(final_v, migrator.highest_version()); } #[test] fn migrator_is_idempotent() { let mut conn = Connection::open_in_memory().unwrap(); let migrator = Migrator::new(canonical_migrations()).unwrap(); let v1 = migrator.run(&mut conn).unwrap(); let v2 = migrator.run(&mut conn).unwrap(); let v3 = migrator.run(&mut conn).unwrap(); assert_eq!(v1, v2); assert_eq!(v2, v3); } #[test] fn migrator_applies_only_pending_versions() { let mut conn = Connection::open_in_memory().unwrap(); // Apply only v1 first. let first = Migrator::new(vec![canonical_migrations()[0]]).unwrap(); assert_eq!(first.run(&mut conn).unwrap(), 1); // Now apply the full list — must catch up from v1 to v3 cleanly. let full = Migrator::new(canonical_migrations()).unwrap(); assert_eq!(full.run(&mut conn).unwrap(), 3); // Schema reflects v2 + v3 column additions. let cols: Vec = conn .prepare("SELECT name FROM pragma_table_info('bookmarks')") .unwrap() .query_map([], |r| r.get::<_, String>(0)) .unwrap() .collect::>() .unwrap(); assert!(cols.contains(&"nickname".to_string())); assert!(cols.contains(&"last_connected_at".to_string())); } #[test] fn migrator_rejects_out_of_order_versions() { let bogus = vec![ Migration { version: 2, name: "a", sql: "" }, Migration { version: 1, name: "b", sql: "" }, ]; assert!(Migrator::new(bogus).is_err()); } #[test] fn migrator_rejects_db_newer_than_known_migrations() { let mut conn = Connection::open_in_memory().unwrap(); conn.execute_batch("PRAGMA user_version = 99").unwrap(); let migrator = Migrator::new(canonical_migrations()).unwrap(); assert!(migrator.run(&mut conn).is_err()); } #[test] fn failed_migration_rolls_back_atomically() { let mut conn = Connection::open_in_memory().unwrap(); // v1 ok; v2 deliberately broken. let broken = vec![ canonical_migrations()[0], Migration { version: 2, name: "broken", sql: "ALTER TABLE not_a_table ADD COLUMN x TEXT;", }, ]; let m = Migrator::new(broken).unwrap(); assert!(m.run(&mut conn).is_err()); // We should still be on v1, not partially upgraded. let v: i64 = conn .query_row("PRAGMA user_version", [], |r| r.get(0)) .unwrap(); assert_eq!(v, 1); } // ---------- Repository pattern ---------- #[test] fn bookmark_repo_crud_round_trip() { let repo = LocalDatabaseRepository::open_in_memory().unwrap(); let id = BookmarkRepository::insert( &repo, NewBookmark { server_name: "Vigorous", server_host: "cn.teamspeak.app", identity_ref: "identity.primary", nickname: Some("ChanoraPoC"), }, ) .unwrap(); let got = BookmarkRepository::get(&repo, id).unwrap(); assert_eq!(got.server_name, "Vigorous"); assert_eq!(got.server_host, "cn.teamspeak.app"); assert_eq!(got.identity_ref, "identity.primary"); assert_eq!(got.nickname.as_deref(), Some("ChanoraPoC")); assert!(got.last_connected_at.is_none()); BookmarkRepository::touch_connected(&repo, id, 1_700_000_000).unwrap(); let again = BookmarkRepository::get(&repo, id).unwrap(); assert_eq!(again.last_connected_at, Some(1_700_000_000)); BookmarkRepository::delete(&repo, id).unwrap(); assert!(matches!( BookmarkRepository::get(&repo, id), Err(sqlite_storage_spike::RepoError::NotFound) )); } #[test] fn bookmark_repo_list_orders_recent_first() { let repo = LocalDatabaseRepository::open_in_memory().unwrap(); let a = BookmarkRepository::insert( &repo, NewBookmark { server_name: "A", server_host: "a.example", identity_ref: "i.a", nickname: None }, ) .unwrap(); let b = BookmarkRepository::insert( &repo, NewBookmark { server_name: "B", server_host: "b.example", identity_ref: "i.b", nickname: None }, ) .unwrap(); let _c = BookmarkRepository::insert( &repo, NewBookmark { server_name: "C", server_host: "c.example", identity_ref: "i.c", nickname: None }, ) .unwrap(); BookmarkRepository::touch_connected(&repo, b, 200).unwrap(); BookmarkRepository::touch_connected(&repo, a, 100).unwrap(); // c never connected. let list = BookmarkRepository::list_ordered(&repo).unwrap(); let names: Vec<_> = list.iter().map(|x| x.server_name.as_str()).collect(); // b (200), a (100), then c (NULL last) — c sorts after by name fallback. assert_eq!(names, vec!["B", "A", "C"]); } #[test] fn bookmark_repo_unique_host_identity_is_enforced() { let repo = LocalDatabaseRepository::open_in_memory().unwrap(); BookmarkRepository::insert( &repo, NewBookmark { server_name: "Vigorous", server_host: "cn.teamspeak.app", identity_ref: "identity.primary", nickname: None, }, ) .unwrap(); let dup = BookmarkRepository::insert( &repo, NewBookmark { server_name: "Vigorous Again", server_host: "cn.teamspeak.app", identity_ref: "identity.primary", nickname: None, }, ); assert!(dup.is_err(), "expected UNIQUE violation"); } #[test] fn settings_repo_upsert_and_delete() { let repo = LocalDatabaseRepository::open_in_memory().unwrap(); SettingsRepository::put(&repo, "audio.aec", "true").unwrap(); assert_eq!( SettingsRepository::get(&repo, "audio.aec").unwrap().as_deref(), Some("true") ); // Upsert (same key, new value). SettingsRepository::put(&repo, "audio.aec", "false").unwrap(); assert_eq!( SettingsRepository::get(&repo, "audio.aec").unwrap().as_deref(), Some("false") ); assert!(SettingsRepository::delete(&repo, "audio.aec").unwrap()); assert!(SettingsRepository::get(&repo, "audio.aec").unwrap().is_none()); assert!(!SettingsRepository::delete(&repo, "audio.aec").unwrap()); } // ---------- End-to-end on disk ---------- #[test] fn open_creates_file_and_persists_across_reopen() { let tmp = tempfile::tempdir().unwrap(); let path = tmp.path().join("chanora.sqlite"); { let repo = LocalDatabaseRepository::open(&path).unwrap(); assert_eq!(repo.schema_version().unwrap(), 3); BookmarkRepository::insert( &repo, NewBookmark { server_name: "Persist", server_host: "persist.example", identity_ref: "i.persist", nickname: None, }, ) .unwrap(); } // Reopen and verify durability + migrator no-ops. let repo2 = LocalDatabaseRepository::open(&path).unwrap(); assert_eq!(repo2.schema_version().unwrap(), 3); let list = BookmarkRepository::list_ordered(&repo2).unwrap(); assert_eq!(list.len(), 1); assert_eq!(list[0].server_name, "Persist"); }