KeiSeiKit-1.0/_primitives/_rust/kei-migrate/tests/integration.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

174 lines
5.4 KiB
Rust

//! Integration tests for kei-migrate against a SQLite file (safe, no deps).
//!
//! SQLite is chosen as the test backend because it has no server dependency
//! and the sqlx-Any path through it exercises the same code path as Postgres
//! / MySQL for everything except dialect-specific DDL (which we abstract in
//! `db::Backend::create_tracker_sql`).
use kei_migrate::{cmd_create, db, discover, do_down, do_status, do_up, tracker};
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
struct Env {
_tmp: TempDir,
url: String,
dir: PathBuf,
}
fn setup() -> Env {
let tmp = TempDir::new().unwrap();
let db_path = tmp.path().join("test.db");
let url = format!("sqlite://{}?mode=rwc", db_path.display());
let dir = tmp.path().join("migrations");
fs::create_dir_all(&dir).unwrap();
Env { _tmp: tmp, url, dir }
}
fn write_migration(dir: &std::path::Path, version: i64, name: &str, up: &str, down: Option<&str>) {
fs::write(dir.join(format!("{}_{}.sql", version, name)), up).unwrap();
if let Some(d) = down {
fs::write(dir.join(format!("{}_{}.down.sql", version, name)), d).unwrap();
}
}
#[test]
fn detects_backend_from_url_scheme() {
assert_eq!(
db::detect_backend("postgres://u:p@h/d").unwrap(),
db::Backend::Postgres
);
assert_eq!(
db::detect_backend("sqlite:///tmp/x.db").unwrap(),
db::Backend::Sqlite
);
assert_eq!(
db::detect_backend("mysql://u:p@h/d").unwrap(),
db::Backend::Mysql
);
assert!(db::detect_backend("mongodb://h").is_err());
}
#[test]
fn scan_empty_dir_is_empty() {
let env = setup();
let migs = discover::scan(&env.dir).unwrap();
assert!(migs.is_empty());
}
#[test]
fn scan_sorts_by_version_and_skips_down_files() {
let env = setup();
write_migration(&env.dir, 2, "second", "SELECT 1;", Some("SELECT 2;"));
write_migration(&env.dir, 1, "first", "SELECT 3;", None);
let migs = discover::scan(&env.dir).unwrap();
assert_eq!(migs.len(), 2);
assert_eq!(migs[0].version, 1);
assert_eq!(migs[1].version, 2);
assert!(migs[0].down_path.is_none());
assert!(migs[1].down_path.is_some());
}
#[test]
fn scan_rejects_duplicate_versions() {
let env = setup();
write_migration(&env.dir, 1, "a", "SELECT 1;", None);
// same version, different name
fs::write(env.dir.join("1_b.sql"), "SELECT 2;").unwrap();
let err = discover::scan(&env.dir).unwrap_err();
assert!(err.to_string().contains("duplicate migration version"));
}
#[tokio::test]
async fn up_applies_pending_and_tracks_them() {
let env = setup();
write_migration(
&env.dir,
1,
"create_t",
"CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT);",
Some("DROP TABLE t;"),
);
write_migration(
&env.dir,
2,
"insert_row",
"INSERT INTO t (id, v) VALUES (1, 'a');",
Some("DELETE FROM t WHERE id = 1;"),
);
let n = do_up(&env.url, &env.dir).await.unwrap();
assert_eq!(n, 2);
// re-running is a no-op
let n2 = do_up(&env.url, &env.dir).await.unwrap();
assert_eq!(n2, 0);
// status: 2 applied, 0 pending
let (a, p) = do_status(&env.url, &env.dir).await.unwrap();
assert_eq!(a, 2);
assert_eq!(p, 0);
}
#[tokio::test]
async fn down_reverts_last_n() {
let env = setup();
write_migration(
&env.dir,
1,
"create_t",
"CREATE TABLE t (id INTEGER PRIMARY KEY);",
Some("DROP TABLE t;"),
);
write_migration(
&env.dir,
2,
"add_col",
"ALTER TABLE t ADD COLUMN v TEXT;",
Some("-- down unsupported on sqlite but we just drop the table for this test\nDROP TABLE t; CREATE TABLE t (id INTEGER PRIMARY KEY);"),
);
do_up(&env.url, &env.dir).await.unwrap();
let reverted = do_down(&env.url, &env.dir, 1).await.unwrap();
assert_eq!(reverted, 1);
let (a, p) = do_status(&env.url, &env.dir).await.unwrap();
assert_eq!(a, 1);
assert_eq!(p, 1);
}
#[tokio::test]
async fn down_refuses_irreversible_marker() {
let env = setup();
write_migration(
&env.dir,
1,
"make_t",
"CREATE TABLE t (id INTEGER PRIMARY KEY);",
Some("-- IRREVERSIBLE\n-- don't auto-revert"),
);
do_up(&env.url, &env.dir).await.unwrap();
let err = do_down(&env.url, &env.dir, 1).await.unwrap_err();
assert!(err.to_string().contains("IRREVERSIBLE"));
}
#[tokio::test]
async fn up_detects_checksum_drift() {
let env = setup();
let up_path = env.dir.join("1_seed.sql");
fs::write(&up_path, "CREATE TABLE t (id INTEGER PRIMARY KEY);").unwrap();
do_up(&env.url, &env.dir).await.unwrap();
// someone edits the already-applied file
fs::write(&up_path, "CREATE TABLE t (id INTEGER PRIMARY KEY, extra TEXT);").unwrap();
let err = do_up(&env.url, &env.dir).await.unwrap_err();
assert!(err.to_string().contains("checksum drift"));
// tracker module is exercised end-to-end here
let _ = tracker::applied_versions; // keep tracker import used
}
#[test]
fn create_scaffolds_up_and_down_files() {
let env = setup();
let (up, down) = cmd_create::run(&env.dir, "add_users_index").unwrap();
assert!(up.exists());
assert!(down.exists());
let up_txt = fs::read_to_string(&up).unwrap();
let down_txt = fs::read_to_string(&down).unwrap();
assert!(up_txt.contains("up migration"));
assert!(down_txt.contains("down migration"));
}