mod tests { use super::*; #[tokio::test] async fn persists_and_rejects_stale_writes() { let dir = tempfile::tempdir().unwrap(); let file = dir.path().join("test.sqlite3"); let store = Store::open(&file, &Config::default()).await.unwrap(); assert_eq!(store.load().await.unwrap().revision, 0); let mut config = crate::config::demo_config("127.0.0.1:9000".parse().unwrap()); config.cache.memory_preset = Some(crate::cache::MemoryPreset::Recommended); config.cache.cold_preset = Some(crate::cache::ColdPreset::Recommended); config.transform.preset = Some(crate::transform::Preset::Performance); config.transform.memory_budget_bytes = 512 * 1024 * 1024; config.transform.max_inflight = 64; assert!(store.replace(0, &config).await.unwrap()); assert!(!store.replace(0, &Config::default()).await.unwrap()); assert_eq!(store.recent_audit().await.unwrap().len(), 1); store.close().await; let reopened = Store::open(&file, &Config::default()).await.unwrap(); let data = reopened.load().await.unwrap(); assert_eq!(data.revision, 1); assert_eq!(data.config.vhosts[0].backends[0].id, "demo"); assert_eq!( data.config.cache.memory_preset, Some(crate::cache::MemoryPreset::Recommended) ); assert_eq!( data.config.cache.cold_preset, Some(crate::cache::ColdPreset::Recommended) ); assert_eq!( data.config.transform.preset, Some(crate::transform::Preset::Performance) ); assert_eq!(data.config.transform.memory_budget_bytes, 512 * 1024 * 1024); assert_eq!(data.config.transform.max_inflight, 64); reopened.close().await; } #[tokio::test] async fn configuration_history_keeps_revision_documents_and_actor() { let dir = tempfile::tempdir().unwrap(); let file = dir.path().join("history.sqlite3"); let store = Store::open(&file, &Config::default()).await.unwrap(); let mut first = Config::default(); first.server_header.value = "first".into(); assert!(store.replace_as(0, &first, "alice").await.unwrap()); let mut second = first.clone(); second.server_header.value = "second".into(); assert!(store.replace_as(1, &second, "bob").await.unwrap()); let history = store.configuration_history(10).await.unwrap(); assert_eq!(history[0].revision, 2); assert_eq!(history[0].actor, "bob"); assert_eq!(history[1].revision, 1); let detail = store.configuration_revision(2).await.unwrap().unwrap(); assert_eq!(detail.config.server_header.value, "second"); assert_eq!(detail.previous_revision, Some(1)); assert_eq!(detail.previous_config.unwrap().server_header.value, "first"); store.close().await; } #[tokio::test] async fn certificate_renewal_observations_survive_reopen_in_schema_v3() { let dir = tempfile::tempdir().unwrap(); let file = dir.path().join("renewals.sqlite3"); let store = Store::open(&file, &Config::default()).await.unwrap(); let mut values = std::collections::BTreeMap::new(); values.insert( "example-cert".to_string(), crate::acme::RenewalState { last_checked: 100, next_check: 200, last_renewed: Some(50), not_after: Some(500), failures: 2, status: "retry".to_string(), message: "temporary CA failure".to_string(), }, ); store.replace_certificate_renewals(&values).await.unwrap(); let version: i64 = sqlx::query_scalar("PRAGMA user_version") .fetch_one(&store.pool) .await .unwrap(); assert_eq!(version, SCHEMA_VERSION); store.close().await; let reopened = Store::open(&file, &Config::default()).await.unwrap(); let saved = reopened.load_certificate_renewals().await.unwrap(); let state = saved.get("example-cert").unwrap(); assert_eq!(state.last_checked, 100); assert_eq!(state.next_check, 200); assert_eq!(state.last_renewed, Some(50)); assert_eq!(state.not_after, Some(500)); assert_eq!(state.failures, 2); assert_eq!(state.status, "retry"); assert_eq!(state.message, "temporary CA failure"); reopened.close().await; } #[tokio::test] async fn certificate_api_credentials_persist_hash_comments_and_individual_usage() { let dir = tempfile::tempdir().unwrap(); let file = dir.path().join("certificate-api.sqlite3"); let store = Store::open(&file, &Config::default()).await.unwrap(); let token = "b".repeat(64); let hash = crate::auth::secret_hash(&token); let saved = store .upsert_certificate_api_credential( "certbot-main", &hash, "production certbot", "owner", ) .await .unwrap(); assert_eq!(saved.key_id, "certbot-main"); assert_eq!(saved.comment, "production certbot"); assert!(saved.last_used_at.is_none()); let raw: String = sqlx::query_scalar( "SELECT token_hash FROM certificate_api_credentials WHERE key_id = ?", ) .bind("certbot-main") .fetch_one(&store.pool) .await .unwrap(); assert_eq!(raw, hash); assert_ne!(raw, token); let second_hash = crate::auth::secret_hash(&"d".repeat(64)); store .upsert_certificate_api_credential( "deploy-hook", &second_hash, "secondary deploy hook", "owner", ) .await .unwrap(); assert_eq!(store.certificate_api_credentials().await.unwrap().len(), 2); assert!(store .verify_certificate_api_credential("certbot-main", &hash) .await .unwrap()); assert!(!store .verify_certificate_api_credential( "certbot-main", &crate::auth::secret_hash(&"c".repeat(64)), ) .await .unwrap()); let credentials = store.certificate_api_credentials().await.unwrap(); assert!(credentials .iter() .find(|item| item.key_id == "certbot-main") .unwrap() .last_used_at .is_some()); assert!(credentials .iter() .find(|item| item.key_id == "deploy-hook") .unwrap() .last_used_at .is_none()); assert!(store .revoke_certificate_api_credential("certbot-main", "owner") .await .unwrap()); let remaining = store.certificate_api_credentials().await.unwrap(); assert_eq!(remaining.len(), 1); assert_eq!(remaining[0].key_id, "deploy-hook"); assert_eq!(store.revoke_all_certificate_api_credentials("owner").await.unwrap(), 1); assert!(store.certificate_api_credentials().await.unwrap().is_empty()); store.close().await; } #[tokio::test] async fn existing_empty_config_is_not_reseeded() { let dir = tempfile::tempdir().unwrap(); let file = dir.path().join("test.sqlite3"); let store = Store::open(&file, &Config::default()).await.unwrap(); store.close().await; let demo = crate::config::demo_config("127.0.0.1:9000".parse().unwrap()); let store = Store::open(&file, &demo).await.unwrap(); assert!(store.load().await.unwrap().config.vhosts.is_empty()); store.close().await; } #[tokio::test] async fn migration_keeps_v1_config_and_revision() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("old.sqlite3"); let options = SqliteConnectOptions::new() .filename(&path) .create_if_missing(true); let pool = SqlitePoolOptions::new() .max_connections(1) .connect_with(options) .await .unwrap(); sqlx::query("CREATE TABLE configuration(id INTEGER PRIMARY KEY, revision INTEGER NOT NULL, document TEXT NOT NULL)") .execute(&pool).await.unwrap(); let demo = crate::config::demo_config("127.0.0.1:9000".parse().unwrap()); sqlx::query("INSERT INTO configuration VALUES(1, 42, ?)") .bind(serde_json::to_string(&demo).unwrap()) .execute(&pool) .await .unwrap(); sqlx::query("PRAGMA user_version = 1") .execute(&pool) .await .unwrap(); pool.close().await; let store = Store::open(&path, &Config::default()).await.unwrap(); let saved = store.load().await.unwrap(); assert_eq!(saved.revision, 42); assert_eq!(saved.config.vhosts[0].backends[0].id, "demo"); assert!(store.admin_record().await.unwrap().is_none()); store.close().await; } }