use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;
#[allow(deprecated)]
fn prax_cmd() -> Command {
Command::cargo_bin("prax").unwrap()
}
#[test]
fn test_help_command() {
prax_cmd()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Prax CLI"))
.stdout(predicate::str::contains("Usage: prax <COMMAND>"))
.stdout(predicate::str::contains("init"))
.stdout(predicate::str::contains("generate"))
.stdout(predicate::str::contains("migrate"))
.stdout(predicate::str::contains("db"));
}
#[test]
fn test_version_command() {
prax_cmd()
.arg("version")
.assert()
.success()
.stdout(predicate::str::contains("Version"))
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn test_init_help() {
prax_cmd()
.args(["init", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Initialize a new Prax project"))
.stdout(predicate::str::contains("--provider"));
}
#[test]
fn test_generate_help() {
prax_cmd()
.args(["generate", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Generate Rust client code"))
.stdout(predicate::str::contains("--schema"));
}
#[test]
fn test_migrate_help() {
prax_cmd()
.args(["migrate", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("migration commands"))
.stdout(predicate::str::contains("dev"))
.stdout(predicate::str::contains("deploy"))
.stdout(predicate::str::contains("reset"))
.stdout(predicate::str::contains("status"));
}
#[test]
fn test_db_help() {
prax_cmd()
.args(["db", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("database operations"))
.stdout(predicate::str::contains("push"))
.stdout(predicate::str::contains("pull"));
}
#[test]
fn test_validate_help() {
prax_cmd()
.args(["validate", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("validation"));
}
#[test]
fn test_format_help() {
prax_cmd()
.args(["format", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Format schema"));
}
#[test]
fn test_init_creates_project_structure() {
let temp_dir = TempDir::new().unwrap();
let project_name = "test_project";
prax_cmd()
.current_dir(temp_dir.path())
.args(["init", project_name, "--yes", "--provider", "postgresql"])
.assert()
.success()
.stdout(predicate::str::contains("initialized successfully"));
let project_path = temp_dir.path().join(project_name);
assert!(project_path.exists(), "Project directory should exist");
assert!(
project_path.join("prax").join("schema.prax").exists(),
"prax/schema.prax should exist"
);
assert!(
project_path.join("prax.toml").exists(),
"prax.toml should exist"
);
assert!(
project_path.join("prax").join("migrations").exists(),
"prax/migrations directory should exist"
);
}
#[test]
fn test_init_with_different_providers() {
for provider in ["postgresql", "mysql", "sqlite"] {
let temp_dir = TempDir::new().unwrap();
let project_name = format!("test_{}", provider);
prax_cmd()
.current_dir(temp_dir.path())
.args(["init", &project_name, "--yes", "--provider", provider])
.assert()
.success();
let config_path = temp_dir.path().join(&project_name).join("prax.toml");
assert!(config_path.exists());
let config_content = fs::read_to_string(config_path).unwrap();
assert!(config_content.contains(provider));
}
}
#[test]
fn test_validate_with_valid_schema() {
let temp_dir = TempDir::new().unwrap();
let schema_path = temp_dir.path().join("schema.prax");
let schema_content = r#"
model User {
id Int @id @auto
name String
email String @unique
}
"#;
fs::write(&schema_path, schema_content).unwrap();
prax_cmd()
.args(["validate", "--schema", schema_path.to_str().unwrap()])
.assert()
.success()
.stdout(predicate::str::contains("valid"));
}
#[test]
fn test_validate_with_invalid_schema() {
let temp_dir = TempDir::new().unwrap();
let schema_path = temp_dir.path().join("schema.prax");
let schema_content = r#"
model User {
id Int @id @auto
name String
email String @unique
// Missing closing brace
"#;
fs::write(&schema_path, schema_content).unwrap();
prax_cmd()
.args(["validate", "--schema", schema_path.to_str().unwrap()])
.assert()
.failure();
}
#[test]
fn test_format_schema() {
let temp_dir = TempDir::new().unwrap();
let schema_path = temp_dir.path().join("schema.prax");
let schema_content = r#"
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User{
id Int @id @auto
name String
email String @unique
@@map("users")
}
"#;
fs::write(&schema_path, schema_content).unwrap();
prax_cmd()
.args(["format", "--schema", schema_path.to_str().unwrap()])
.assert()
.success();
let formatted = fs::read_to_string(&schema_path).unwrap();
assert!(
formatted.contains("provider = \"mysql\""),
"format rewrote the datasource provider:\n{formatted}"
);
assert!(
!formatted.contains("postgresql"),
"format injected a hardcoded postgresql provider:\n{formatted}"
);
assert!(
!formatted.contains("generator client"),
"format injected a hardcoded generator block:\n{formatted}"
);
assert!(
formatted
.lines()
.any(|l| { l.trim_start().starts_with("url") && l.contains(r#"env("DATABASE_URL")"#) }),
"format dropped the datasource url:\n{formatted}"
);
assert!(
formatted.contains("@id") && !formatted.contains("@@id"),
"field @id corrupted:\n{formatted}"
);
assert!(
formatted.contains("@unique") && !formatted.contains("@@unique"),
"field @unique corrupted:\n{formatted}"
);
assert!(
formatted.contains("@@map(\"users\")"),
"block @@map lost:\n{formatted}"
);
let once = formatted.clone();
prax_cmd()
.args(["format", "--schema", schema_path.to_str().unwrap()])
.assert()
.success();
let twice = fs::read_to_string(&schema_path).unwrap();
assert_eq!(once, twice, "format is not idempotent:\n{twice}");
prax_cmd()
.args([
"format",
"--check",
"--schema",
schema_path.to_str().unwrap(),
])
.assert()
.success();
}
#[test]
fn test_format_directory_with_cross_file_relations() {
let temp_dir = TempDir::new().unwrap();
let schema_dir = temp_dir.path().join("schema");
fs::create_dir(&schema_dir).unwrap();
fs::write(
schema_dir.join("datasource.prax"),
"datasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n",
)
.unwrap();
fs::write(
schema_dir.join("user.prax"),
"model User {\n id Int @id\n posts Post[]\n}\n",
)
.unwrap();
fs::write(
schema_dir.join("post.prax"),
"model Post {\n id Int @id\n authorId Int\n author User @relation(fields: [authorId], references: [id])\n}\n",
)
.unwrap();
fs::write(
schema_dir.join("role.prax"),
"enum Role {\n User @map(\"user\")\n Admin\n\n @@map(\"role\")\n}\n",
)
.unwrap();
fs::write(
schema_dir.join("active_users.prax"),
"view ActiveUsers {\n id Int @unique\n email String\n\n @@map(\"active_users\")\n}\n",
)
.unwrap();
fs::write(
schema_dir.join("address.prax"),
"type Address {\n street String @map(\"street_address\")\n}\n",
)
.unwrap();
prax_cmd()
.args(["format", "--schema", schema_dir.to_str().unwrap()])
.assert()
.success();
let user = fs::read_to_string(schema_dir.join("user.prax")).unwrap();
assert!(
user.contains("posts Post[]"),
"format mangled the cross-file relation:\n{user}"
);
let post = fs::read_to_string(schema_dir.join("post.prax")).unwrap();
assert!(
post.contains("@relation(fields: [authorId], references: [id])"),
"format mangled @relation:\n{post}"
);
assert!(
!post.contains("@@relation"),
"field @relation corrupted to @@:\n{post}"
);
let role = fs::read_to_string(schema_dir.join("role.prax")).unwrap();
assert!(
role.contains("@map(\"user\")")
&& !role.contains("@@map(\"user\")")
&& role.contains("@@map(\"role\")"),
"enum attribute positions corrupted:\n{role}"
);
let view = fs::read_to_string(schema_dir.join("active_users.prax")).unwrap();
assert!(
view.contains("@unique") && !view.contains("@@unique"),
"view field @unique corrupted:\n{view}"
);
assert!(
view.contains("@@map(\"active_users\")"),
"view block @@map lost:\n{view}"
);
let address = fs::read_to_string(schema_dir.join("address.prax")).unwrap();
assert!(
address.contains("@map(\"street_address\")")
&& !address.contains("@@map(\"street_address\")"),
"composite field @map corrupted:\n{address}"
);
let datasource = fs::read_to_string(schema_dir.join("datasource.prax")).unwrap();
assert!(
datasource
.lines()
.any(|l| l.trim_start().starts_with("url") && l.contains(r#"env("DATABASE_URL")"#)),
"format dropped the datasource url:\n{datasource}"
);
}
#[test]
fn test_format_rejects_syntax_error() {
let temp_dir = TempDir::new().unwrap();
let schema_path = temp_dir.path().join("schema.prax");
fs::write(&schema_path, "model User {\n id Int @id\n").unwrap();
prax_cmd()
.args(["format", "--schema", schema_path.to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains("Syntax error"));
}
#[test]
fn test_format_directory_with_malformed_file_fails() {
let temp_dir = TempDir::new().unwrap();
let schema_dir = temp_dir.path().join("schema");
fs::create_dir(&schema_dir).unwrap();
fs::write(
schema_dir.join("user.prax"),
"model User {\n id Int @id\n}\n",
)
.unwrap();
fs::write(
schema_dir.join("bad.prax"),
"model Broken {\n id Int @id\n",
)
.unwrap();
prax_cmd()
.args(["format", "--schema", schema_dir.to_str().unwrap()])
.assert()
.failure()
.stderr(predicate::str::contains("Syntax error"));
}
#[test]
fn test_generate_missing_schema() {
let temp_dir = TempDir::new().unwrap();
let schema_path = temp_dir.path().join("nonexistent.prax");
prax_cmd()
.args(["generate", "--schema", schema_path.to_str().unwrap()])
.assert()
.failure();
}
#[test]
fn test_migrate_status_no_config() {
let temp_dir = TempDir::new().unwrap();
let _result = prax_cmd()
.current_dir(temp_dir.path())
.args(["migrate", "status"])
.assert();
}
#[test]
fn test_invalid_command() {
prax_cmd()
.arg("invalid_command")
.assert()
.failure()
.stderr(predicate::str::contains("error"));
}
#[test]
fn test_global_options() {
prax_cmd()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn test_generate_emits_runtime_trait_impls_and_client_struct() {
let temp_dir = TempDir::new().unwrap();
let schema_path = temp_dir.path().join("schema.prax");
let schema_content = r#"
datasource db {
provider = "postgresql"
url = "postgres://localhost/test"
}
model User {
id Int @id @auto
email String @unique
name String
}
"#;
fs::write(&schema_path, schema_content).unwrap();
let output_dir = temp_dir.path().join("out");
prax_cmd()
.args([
"generate",
"--schema",
schema_path.to_str().unwrap(),
"--output",
output_dir.to_str().unwrap(),
])
.assert()
.success();
let user_module = fs::read_to_string(output_dir.join("user.rs")).expect("user.rs not emitted");
assert!(
user_module.contains("impl prax_query::row::FromRow for User"),
"user.rs missing FromRow impl:\n{user_module}"
);
assert!(
user_module.contains("FromColumn>::from_column(row, \"email\")"),
"user.rs FromRow does not decode email column:\n{user_module}"
);
assert!(
user_module.contains("impl prax_query::traits::ModelWithPk for User"),
"user.rs missing ModelWithPk impl:\n{user_module}"
);
assert!(
user_module.contains("fn pk_value(&self)"),
"user.rs ModelWithPk missing pk_value:\n{user_module}"
);
assert!(
user_module.contains("fn get_column_value(") && user_module.contains("column: &str"),
"user.rs ModelWithPk missing get_column_value:\n{user_module}"
);
assert!(
user_module.contains("pub struct Client<E: prax_query::QueryEngine>"),
"user.rs missing per-model Client<E> struct:\n{user_module}"
);
assert!(
!user_module.contains("UserOperations"),
"user.rs still emits the legacy UserOperations<E> name:\n{user_module}"
);
let mod_rs = fs::read_to_string(output_dir.join("mod.rs")).expect("mod.rs not emitted");
assert!(
mod_rs.contains("user::Client::new(self.engine.clone())"),
"mod.rs accessor not routed through user::Client:\n{mod_rs}"
);
}
#[test]
fn test_generate_emits_prettyplease_formatted_rust() {
let temp_dir = TempDir::new().unwrap();
let schema_path = temp_dir.path().join("schema.prax");
let schema_content = r#"
datasource db {
provider = "postgresql"
url = "postgres://localhost/test"
}
model User {
id Int @id @auto
email String @unique
name String
}
model Post {
id Int @id @auto
title String
userId Int @map("user_id")
}
enum Role {
Admin
Member
}
"#;
fs::write(&schema_path, schema_content).unwrap();
let output_dir = temp_dir.path().join("out");
prax_cmd()
.args([
"generate",
"--schema",
schema_path.to_str().unwrap(),
"--output",
output_dir.to_str().unwrap(),
])
.assert()
.success();
let expected_files = [
"mod.rs",
"user.rs",
"post.rs",
"role.rs",
"types.rs",
"filters.rs",
];
for name in expected_files {
let path = output_dir.join(name);
let raw = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("expected file {} to exist: {}", name, e));
let parsed = syn::parse_file(&raw)
.unwrap_or_else(|e| panic!("{} did not parse as Rust:\n{}\nerror: {}", name, raw, e));
let reformatted = prettyplease::unparse(&parsed);
assert_eq!(
raw, reformatted,
"{} is not prettyplease-stable: the generator emitted a shape \
prettyplease re-flowed on the second pass",
name
);
}
}
#[test]
fn test_import_prisma_directory_creates_mirrored_prax_directory() {
let input = TempDir::new().unwrap();
fs::create_dir_all(input.path().join("models")).unwrap();
fs::write(
input.path().join("schema.prisma"),
r#"datasource db { provider = "postgresql" url = env("X") }"#,
)
.unwrap();
fs::write(
input.path().join("models/u.prisma"),
"model U { id Int @id @default(autoincrement()) }",
)
.unwrap();
let output_dir = TempDir::new().unwrap();
prax_cmd()
.arg("import")
.arg("--from")
.arg("prisma")
.arg("--input")
.arg(input.path())
.arg("--output")
.arg(output_dir.path())
.arg("--force")
.assert()
.success();
assert!(output_dir.path().join("schema.prax").exists());
assert!(output_dir.path().join("models/u.prax").exists());
}