migration_preflight_tests.rs (11278B)
1 //! Public-open regressions use only synthetic owned databases. 2 3 use super::{tests, *}; 4 use crate::{OpenOptions, Paths, SqliteStorage}; 5 use std::path::Path; 6 7 async fn file_connection(path: &Path, create: bool) -> SqliteConnection { 8 SqliteConnection::connect_with( 9 &SqliteConnectOptions::new() 10 .filename(path) 11 .create_if_missing(create), 12 ) 13 .await 14 .expect("fixture connection") 15 } 16 17 async fn fixture(runtime_version: u32, private_version: u32) -> (tempfile::TempDir, Paths) { 18 let directory = tempfile::tempdir().expect("temporary directory"); 19 let paths = Paths::from_directory(directory.path()).expect("owned paths"); 20 let mut runtime = file_connection(paths.runtime(), true).await; 21 tests::establish_runtime_version(&mut runtime, runtime_version).await; 22 sqlx::query("INSERT INTO radroots_runtime_source_generations (generation, state, created_at_unix_ms) VALUES (?, 'active', 10)") 23 .bind([7_u8; 32].as_slice()).execute(&mut runtime).await.expect("retained generation"); 24 runtime.close().await.expect("close runtime fixture"); 25 let mut private = file_connection(paths.private(), true).await; 26 tests::establish_private_version(&mut private, private_version).await; 27 private.close().await.expect("close private fixture"); 28 (directory, paths) 29 } 30 31 async fn alter(path: &Path, sql: &'static str) { 32 let mut connection = file_connection(path, false).await; 33 sqlx::raw_sql(sql) 34 .execute(&mut connection) 35 .await 36 .expect("fixture alteration"); 37 connection.close().await.expect("close fixture alteration"); 38 } 39 40 async fn assert_refused_unchanged(paths: Paths, mode: OpenMode, expected: fn(&Error) -> bool) { 41 let runtime = std::fs::read(paths.runtime()).expect("runtime bytes"); 42 let private = std::fs::read(paths.private()).expect("private bytes"); 43 for _ in 0..2 { 44 let error = match SqliteStorage::open(OpenOptions::new(paths.clone(), mode)).await { 45 Ok(store) => { 46 store.close().await.expect("close unexpected store"); 47 panic!("incompatible pair opened") 48 } 49 Err(error) => error, 50 }; 51 assert!(expected(&error), "unexpected error: {error:?}"); 52 assert!( 53 std::fs::read(paths.runtime()).expect("retained runtime") == runtime, 54 "runtime bytes changed" 55 ); 56 assert!( 57 std::fs::read(paths.private()).expect("retained private") == private, 58 "private bytes changed" 59 ); 60 } 61 } 62 63 #[tokio::test] 64 async fn future_private_schema_preserves_pending_runtime_and_both_original_files() { 65 let (_directory, paths) = fixture(16, private::CURRENT_VERSION).await; 66 alter(paths.private(), "PRAGMA user_version = 999").await; 67 assert_refused_unchanged(paths, OpenMode::ReadWriteExisting, |error| { 68 matches!( 69 error, 70 Error::SchemaTooNew { 71 database: PRIVATE_DATABASE, 72 actual: 999, 73 .. 74 } 75 ) 76 }) 77 .await; 78 } 79 80 #[tokio::test] 81 async fn foreign_private_namespace_preserves_pending_runtime_and_both_original_files() { 82 let (_directory, paths) = fixture(16, private::CURRENT_VERSION).await; 83 alter(paths.private(), "PRAGMA application_id = 42").await; 84 assert_refused_unchanged(paths, OpenMode::ReadWriteExisting, |error| { 85 matches!( 86 error, 87 Error::SchemaIdentityMismatch { 88 database: PRIVATE_DATABASE, 89 actual: 42, 90 .. 91 } 92 ) 93 }) 94 .await; 95 } 96 97 #[tokio::test] 98 async fn unexpected_private_catalog_preserves_pending_runtime_and_both_original_files() { 99 let (_directory, paths) = fixture(16, private::CURRENT_VERSION).await; 100 alter(paths.private(), "CREATE TABLE foreign_state (value TEXT)").await; 101 assert_refused_unchanged(paths, OpenMode::ReadWriteExisting, |error| { 102 matches!( 103 error, 104 Error::SchemaCatalogMismatch { 105 database: PRIVATE_DATABASE, 106 .. 107 } 108 ) 109 }) 110 .await; 111 } 112 113 #[tokio::test] 114 async fn future_runtime_schema_preserves_pending_private_and_both_original_files() { 115 let (_directory, paths) = fixture(runtime::CURRENT_VERSION, 3).await; 116 alter(paths.runtime(), "PRAGMA user_version = 999").await; 117 assert_refused_unchanged(paths, OpenMode::ReadWriteExisting, |error| { 118 matches!( 119 error, 120 Error::SchemaTooNew { 121 database: RUNTIME_DATABASE, 122 actual: 999, 123 .. 124 } 125 ) 126 }) 127 .await; 128 } 129 130 #[tokio::test] 131 async fn read_only_pending_pair_preserves_both_original_files() { 132 let (_directory, paths) = fixture(16, 3).await; 133 assert_refused_unchanged(paths, OpenMode::ReadOnly, |error| { 134 matches!( 135 error, 136 Error::SchemaMigrationRequired { 137 database: RUNTIME_DATABASE, 138 actual: 16, 139 .. 140 } 141 ) 142 }) 143 .await; 144 } 145 146 #[tokio::test] 147 async fn compatible_pending_pair_migrates_and_reopens_with_same_generation() { 148 use radroots_storage::EventStore; 149 let (_directory, paths) = fixture(16, 3).await; 150 for _ in 0..2 { 151 let store = 152 SqliteStorage::open(OpenOptions::new(paths.clone(), OpenMode::ReadWriteExisting)) 153 .await 154 .expect("supported pair"); 155 assert_eq!( 156 store 157 .status() 158 .await 159 .expect("status") 160 .generation() 161 .as_bytes(), 162 &[7; 32] 163 ); 164 store.close().await.expect("close supported pair"); 165 } 166 for (path, version) in [ 167 (paths.runtime(), runtime::CURRENT_VERSION), 168 (paths.private(), private::CURRENT_VERSION), 169 ] { 170 let mut connection = file_connection(path, false).await; 171 assert_eq!( 172 tests::pragma(&mut connection, "user_version").await, 173 i64::from(version) 174 ); 175 connection.close().await.expect("close inspected fixture"); 176 } 177 } 178 179 #[tokio::test] 180 async fn unversioned_namespace_collision_preserves_both_original_files() { 181 let (_directory, paths) = fixture(16, private::CURRENT_VERSION).await; 182 alter( 183 paths.private(), 184 "PRAGMA user_version = 0; PRAGMA application_id = 0", 185 ) 186 .await; 187 assert_refused_unchanged(paths, OpenMode::ReadWriteExisting, |error| { 188 matches!( 189 error, 190 Error::UnrecognizedSchema { 191 database: PRIVATE_DATABASE 192 } 193 ) 194 }) 195 .await; 196 } 197 198 #[tokio::test] 199 async fn corrupt_private_member_preserves_pending_runtime_and_corrupt_evidence() { 200 let (_directory, paths) = fixture(16, private::CURRENT_VERSION).await; 201 std::fs::write(paths.private(), b"synthetic invalid SQLite evidence").expect("corrupt fixture"); 202 assert_refused_unchanged(paths, OpenMode::ReadWriteExisting, |error| { 203 matches!( 204 error, 205 Error::DatabaseCorrupt { 206 database: PRIVATE_DATABASE 207 } 208 ) 209 }) 210 .await; 211 } 212 213 #[tokio::test] 214 async fn incompatible_private_member_prevents_creation_of_missing_runtime() { 215 use radroots_storage::event::SourceGeneration; 216 let (_directory, paths) = fixture(16, private::CURRENT_VERSION).await; 217 std::fs::remove_file(paths.runtime()).expect("remove synthetic runtime"); 218 alter(paths.private(), "PRAGMA user_version = 999").await; 219 let before = std::fs::read(paths.private()).expect("private evidence"); 220 for _ in 0..2 { 221 let options = OpenOptions::new(paths.clone(), OpenMode::Create) 222 .with_source_generation(SourceGeneration::new([9; 32]).expect("generation"), 10) 223 .expect("options"); 224 assert!(matches!( 225 SqliteStorage::open(options).await, 226 Err(Error::SchemaTooNew { 227 database: PRIVATE_DATABASE, 228 actual: 999, 229 .. 230 }) 231 )); 232 assert!(!paths.runtime().exists()); 233 assert!(std::fs::read(paths.private()).expect("private retained") == before); 234 } 235 } 236 237 #[tokio::test] 238 async fn ineligible_v10_authored_evidence_preserves_both_original_files() { 239 let (_directory, paths) = fixture(10, 3).await; 240 alter( 241 paths.runtime(), 242 "INSERT INTO radroots_runtime_journal_operations ( 243 instance_id, operation_id, idempotency_key, input_digest, prepared_at_unix_ms, 244 revision, stage, cancellation_state, updated_at_unix_ms 245 ) VALUES (zeroblob(16), x'ff', 'retained-invalid-journal', zeroblob(32), 10, 246 1, 'prepared', 'not_requested', 10)", 247 ) 248 .await; 249 assert_refused_unchanged(paths, OpenMode::ReadWriteExisting, |error| { 250 matches!( 251 error, 252 Error::AuthoredMigrationBlocked { 253 invalid_or_unsupported: 1, 254 .. 255 } 256 ) 257 }) 258 .await; 259 } 260 261 #[tokio::test] 262 async fn incompatible_generation_preserves_pending_schemas_and_original_files() { 263 use radroots_storage::event::SourceGeneration; 264 let (_directory, paths) = fixture(16, 3).await; 265 let runtime = std::fs::read(paths.runtime()).expect("runtime evidence"); 266 let private = std::fs::read(paths.private()).expect("private evidence"); 267 for _ in 0..2 { 268 let options = OpenOptions::new(paths.clone(), OpenMode::ReadWriteExisting) 269 .with_source_generation(SourceGeneration::new([9; 32]).expect("generation"), 10) 270 .expect("options"); 271 assert!(matches!( 272 SqliteStorage::open(options).await, 273 Err(Error::SourceGenerationMismatch) 274 )); 275 assert!( 276 std::fs::read(paths.runtime()).expect("retained runtime") == runtime, 277 "runtime bytes changed before rejecting generation" 278 ); 279 assert!( 280 std::fs::read(paths.private()).expect("retained private") == private, 281 "private bytes changed before rejecting generation" 282 ); 283 } 284 } 285 286 #[test] 287 fn unrelated_metadata_failure_retains_typed_metadata_diagnostic() { 288 assert!(matches!( 289 schema_metadata_error(&sqlx::Error::RowNotFound, PRIVATE_DATABASE), 290 Error::SchemaMetadataUnavailable { 291 database: PRIVATE_DATABASE 292 } 293 )); 294 } 295 296 #[tokio::test] 297 async fn unfingerprinted_private_v2_envelope_preserves_both_original_files() { 298 let (_directory, paths) = fixture(16, 3).await; 299 alter( 300 paths.private(), 301 "INSERT INTO radroots_private_artifacts ( 302 artifact_id, artifact_kind, schema_id, commitment, protected_size_bytes, 303 secret_provider, secret_reference, key_version, envelope_version, 304 encrypted_envelope, revision, stage, created_at_unix_ms, updated_at_unix_ms 305 ) VALUES (zeroblob(16), 'trade.private_terms', 'trade.private_terms.v1', zeroblob(32), 1, 306 'memory', 'synthetic-key', 1, 2, x'03', 1, 'active', 1, 1)", 307 ) 308 .await; 309 assert_refused_unchanged(paths, OpenMode::ReadWriteExisting, |error| { 310 matches!( 311 error, 312 Error::SchemaMigrationFailed { 313 database: PRIVATE_DATABASE, 314 target_version: 4 315 } 316 ) 317 }) 318 .await; 319 }