lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }