use jerrycan::platform::design::Design;
use jerrycan::platform::scaffold;
use std::fs;
const GOLDEN: &str = include_str!("../../../conformance/designs/todo-api.design.json");
fn db_design() -> Design {
let mut v: serde_json::Value = serde_json::from_str(GOLDEN).unwrap();
v["dependencies"] = serde_json::json!(["db", "validate"]);
serde_json::from_value(v).unwrap()
}
fn scaffold_db() -> (tempfile::TempDir, std::path::PathBuf) {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("todo-api");
scaffold::scaffold(&root, &db_design()).unwrap();
(tmp, root)
}
#[test]
fn db_mode_emits_sql_repos_with_di_factories() {
let (_t, root) = scaffold_db();
let repo = fs::read_to_string(root.join("crates/routes/todos/src/repo.rs")).unwrap();
assert!(
repo.contains("pub(crate) async fn todo_repo(db: Dep<Db>)"),
"{repo}"
);
assert!(
repo.contains("use jerrycan::db::sea_orm;"),
"facade alias resolves bare sea_orm:: paths: {repo}"
);
assert!(
repo.contains("todo::Entity::find()") && repo.contains(".all(self.db.conn())"),
"reads go through SeaORM entity finders: {repo}"
);
assert!(
!repo.contains("SELECT ") && !repo.contains("self.db.sql("),
"no raw SQL strings in generated repos: {repo}"
);
assert!(
!repo.contains("build_any_sqlx")
&& !repo.contains("self.db.pool()")
&& !repo.contains("sea_query"),
"repos are SeaORM now, not sea-query/sqlx: {repo}"
);
assert!(
repo.contains("id: sea_orm::ActiveValue::NotSet,"),
"synthetic pk is DB-assigned on insert: {repo}"
);
assert!(
!repo.contains(".last_insert_id()"),
"sqlite must not rely on last_insert_id: {repo}"
);
assert!(
repo.contains("pub async fn update(&self, id: i64, item: Todo)"),
"PUT/PATCH handlers need a persisting update: {repo}"
);
assert!(
repo.contains("title: Set(item.title),") && repo.contains("done: Set(item.done),"),
"update sets every non-pk field via the ActiveModel: {repo}"
);
assert!(repo.contains("map_err(db_error)"), "{repo}");
let lib = fs::read_to_string(root.join("crates/routes/todos/src/lib.rs")).unwrap();
assert!(lib.contains(".provide_dep(repo::todo_repo)"), "{lib}");
assert!(
!lib.contains("TodoRepo::new()"),
"no in-memory provide in db mode: {lib}"
);
}
#[test]
fn db_mode_emits_dual_dialect_migrations_from_entities() {
let (_t, root) = scaffold_db();
let sqlite = fs::read_to_string(
root.join("crates/routes/todos/migrations/sqlite/0001_create_tables.sql"),
)
.unwrap();
let postgres = fs::read_to_string(
root.join("crates/routes/todos/migrations/postgres/0001_create_tables.sql"),
)
.unwrap();
assert!(
sqlite.contains("CREATE TABLE \"todos\"") && sqlite.contains("PRIMARY KEY AUTOINCREMENT"),
"{sqlite}"
);
assert!(sqlite.to_lowercase().contains("\"title\" text not null"));
assert!(
sqlite.to_lowercase().contains("\"done\" boolean")
&& !sqlite.to_lowercase().contains("\"done\" boolean not null"),
"optional bool field is a nullable native boolean: {sqlite}"
);
assert!(postgres.to_lowercase().contains("bigserial"), "{postgres}");
assert!(
postgres.to_lowercase().contains("\"done\" bool")
&& !postgres.to_lowercase().contains("\"done\" bool not null"),
"optional bool field is a nullable native boolean: {postgres}"
);
assert!(root.join("crates/routes/todos/migrations/sqlite").exists());
let users = fs::read_to_string(
root.join("crates/routes/users/migrations/sqlite/0001_create_tables.sql"),
)
.unwrap();
assert!(users.contains("CREATE TABLE \"users\""));
}
#[test]
fn db_mode_wires_main_and_aggregated_migrations() {
let (_t, root) = scaffold_db();
let main_rs = fs::read_to_string(root.join("crates/app/src/main.rs")).unwrap();
assert!(
main_rs.contains("jerrycan::db::Db::from_env().await?"),
"{main_rs}"
);
assert!(
main_rs.contains("db.migrate(migrations::MIGRATIONS).await?"),
"{main_rs}"
);
assert!(main_rs.contains(".extend(db)"), "{main_rs}");
assert!(
main_rs.contains("OpenApi::new(include_str!"),
"validate mode mounts the doc: {main_rs}"
);
let agg = fs::read_to_string(root.join("crates/app/src/migrations.rs")).unwrap();
assert!(agg.contains("pub const MIGRATIONS"), "{agg}");
assert!(
agg.contains("routes/todos/migrations/sqlite/0001_create_tables.sql"),
"{agg}"
);
let ws = fs::read_to_string(root.join("Cargo.toml")).unwrap();
assert!(ws.contains("features = [\"db\", \"validate\"]"), "{ws}");
assert!(root.join("openapi.json").exists());
}
#[test]
fn sql_identifiers_are_quoted_so_reserved_words_survive() {
let mut v: serde_json::Value = serde_json::from_str(GOLDEN).unwrap();
v["dependencies"] = serde_json::json!(["db"]);
v["modules"][0]["entities"][0]["fields"][0]["name"] = serde_json::json!("order");
let design: Design = serde_json::from_value(v).unwrap();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("todo-api");
scaffold::scaffold(&root, &design).unwrap();
let sqlite = fs::read_to_string(
root.join("crates/routes/todos/migrations/sqlite/0001_create_tables.sql"),
)
.unwrap();
assert!(
sqlite.to_lowercase().contains("\"order\" text"),
"reserved-word column must be quoted: {sqlite}"
);
}
#[test]
fn tenancy_generates_the_tenant_guard_in_shared() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: jerrycan::platform::design::Design = serde_json::from_str(s).unwrap();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("app");
jerrycan::platform::scaffold::scaffold(&root, &d).unwrap();
let shared = std::fs::read_to_string(root.join("crates/shared/src/lib.rs")).unwrap();
assert!(shared.contains("pub struct Tenant"), "{shared}");
assert!(
shared.contains("pub async fn tenant("),
"guard factory: {shared}"
);
assert!(
shared.contains("workspace_members"),
"membership check: {shared}"
);
let main_rs = std::fs::read_to_string(root.join("crates/app/src/main.rs")).unwrap();
assert!(
main_rs.contains(".provide_dep(shared::tenant)"),
"{main_rs}"
);
}
#[test]
fn reference_slice_design_is_valid_contract_v2() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: jerrycan::platform::design::Design = serde_json::from_str(s).unwrap();
assert_eq!(d.contract_version, 2);
let qs = jerrycan::platform::questions::validate(&d);
assert!(qs.is_empty(), "{qs:?}");
assert_eq!(d.tenant_owned().len(), 2); }
#[test]
fn generate_migration_emits_numbered_pair_and_rewires() {
let (_t, root) = scaffold_db();
let created =
jerrycan::platform::genroute::generate_migration(&root, "todos", "add_due_index").unwrap();
assert!(
created
.iter()
.any(|p| p.ends_with("migrations/sqlite/0002_add_due_index.sql")),
"{created:?}"
);
assert!(
created
.iter()
.any(|p| p.ends_with("migrations/postgres/0002_add_due_index.sql")),
"{created:?}"
);
let agg = std::fs::read_to_string(root.join("crates/app/src/migrations.rs")).unwrap();
assert!(agg.contains("0002_add_due_index"), "{agg}");
let again = jerrycan::platform::genroute::generate_migration(&root, "todos", "more").unwrap();
assert!(
again.iter().any(|p| p.ends_with("0003_more.sql")),
"{again:?}"
);
}
#[tokio::test]
async fn schema_verify_flags_staleness_with_jc0520() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: jerrycan::platform::design::Design = serde_json::from_str(s).unwrap();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("app");
jerrycan::platform::scaffold::scaffold(&root, &d).unwrap();
let c = jerrycan::platform::schema::derive_schema(&root, &d)
.await
.unwrap();
std::fs::write(
root.join("schema.json"),
jerrycan::platform::schema::render(&c),
)
.unwrap();
assert!(
jerrycan::platform::schema::verify_fresh(&root, &d)
.await
.unwrap()
.is_empty()
);
std::fs::write(root.join("schema.json"), "{}").unwrap();
let diags = jerrycan::platform::schema::verify_fresh(&root, &d)
.await
.unwrap();
assert!(diags.iter().any(|x| x.code == "JC0520"), "{diags:?}");
std::fs::remove_file(root.join("schema.json")).unwrap();
assert!(
!jerrycan::platform::schema::verify_fresh(&root, &d)
.await
.unwrap()
.is_empty()
);
}
#[tokio::test]
async fn cross_module_fk_lets_per_module_migration_insert_under_fk_enforcement() {
use jerrycan::db::sea_orm::ConnectionTrait;
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: Design = serde_json::from_str(s).unwrap();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("app");
scaffold::scaffold(&root, &d).unwrap();
let all = jerrycan::platform::mounting::collect_migrations(&root).unwrap();
let leads_only: Vec<_> = all
.into_iter()
.filter(|m| m.name.starts_with("leads_"))
.collect();
assert!(
!leads_only.is_empty(),
"leads module must own a migration file"
);
let db = jerrycan::db::Db::connect("sqlite::memory:").await.unwrap();
db.conn()
.execute_unprepared("PRAGMA foreign_keys=ON")
.await
.unwrap();
db.migrate_owned(&leads_only).await.unwrap();
db.conn()
.execute_unprepared(
"INSERT INTO leads (workspace_id, phone, name, status) VALUES (1, '555', 'A', 'new')",
)
.await
.expect("cross-module fk is unenforced: insert with a dangling workspace_id must succeed");
let count = db
.conn()
.query_one(jerrycan::db::sea_orm::Statement::from_string(
jerrycan::db::sea_orm::DatabaseBackend::Sqlite,
"SELECT COUNT(*) AS n FROM leads".to_string(),
))
.await
.unwrap()
.unwrap();
let n: i64 = count
.try_get::<i64>("", "n")
.or_else(|_| count.try_get::<i32>("", "n").map(i64::from))
.unwrap();
assert_eq!(n, 1, "the lead row persisted");
}
#[test]
fn memory_mode_is_unchanged() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("todo-api");
let design: Design = serde_json::from_str(GOLDEN).unwrap();
scaffold::scaffold(&root, &design).unwrap();
let repo = fs::read_to_string(root.join("crates/routes/todos/src/repo.rs")).unwrap();
assert!(repo.contains("BTreeMap"), "in-memory repo stays: {repo}");
let main_rs = fs::read_to_string(root.join("crates/app/src/main.rs")).unwrap();
assert!(!main_rs.contains("jerrycan::db"));
assert!(!root.join("crates/app/src/migrations.rs").exists());
assert!(
root.join("openapi.json").exists(),
"openapi.json is emitted in every mode"
);
}