use super::*;
#[test]
fn new_name_flag_sets_project_name() {
let dir = temp_dir("new_name_flag");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = new_project_non_interactive(Some("explicit-name".into()), false, &dir).unwrap();
let content = fs::read_to_string(output.join("presentation/Cargo.toml")).unwrap();
assert!(content.contains("name = \"explicit-name\""));
cleanup(&dir);
}
#[test]
fn new_db_flag_creates_db_files() {
let dir = temp_dir("new_db_flag");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = new_project_non_interactive(Some("db-project".into()), true, &dir).unwrap();
assert!(output.join("docker-compose.yml").exists());
assert!(output.join(".env").exists());
assert!(output.join(".env.example").exists());
assert!(output.join(".cargo/config.toml").exists());
assert!(output.join("infrastructure/migrations").is_dir());
assert!(output.join("infrastructure/src/db.rs").exists());
cleanup(&dir);
}
#[test]
fn new_without_db_flag_has_no_db_files() {
let dir = temp_dir("new_no_db");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = new_project_non_interactive(Some("plain-project".into()), false, &dir).unwrap();
assert!(!output.join(".env.example").exists());
assert!(!output.join(".cargo/config.toml").exists());
assert!(!output.join("infrastructure/src/db.rs").exists());
cleanup(&dir);
}
#[test]
fn creates_project_structure() {
let dir = temp_dir("cg_structure");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("test-app", &dir).unwrap();
assert!(output.join("Cargo.toml").exists());
assert!(output.join("business/src/lib.rs").exists());
assert!(output.join("infrastructure/src/lib.rs").exists());
assert!(output.join("presentation/src/main.rs").exists());
cleanup(&dir);
}
#[test]
fn presentation_cargo_toml_has_project_name() {
let dir = temp_dir("cg_cargo");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("my-puerto-app", &dir).unwrap();
let content = fs::read_to_string(output.join("presentation/Cargo.toml")).unwrap();
assert!(content.contains("name = \"my-puerto-app\""));
cleanup(&dir);
}
#[test]
fn main_rs_is_minimal_entry_point() {
let dir = temp_dir("cg_main");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("cool-project", &dir).unwrap();
let content = fs::read_to_string(output.join("presentation/src/main.rs")).unwrap();
assert!(content.contains("generated::bootstrap::build_app"));
assert!(content.contains("mod generated"));
assert!(!content.contains("{{project-name}}"));
cleanup(&dir);
}
#[test]
fn bootstrap_rs_wires_ddd_layers() {
let dir = temp_dir("cg_bootstrap");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("api-test", &dir).unwrap();
let content =
fs::read_to_string(output.join("presentation/src/generated/bootstrap.rs")).unwrap();
assert!(content.contains("GetGreetingUseCaseImpl"));
assert!(content.contains("InMemoryGreetingRepository"));
assert!(content.contains("GreetingApi"));
assert!(content.contains("OpenApiService"));
assert!(content.contains("8080"));
cleanup(&dir);
}
#[test]
fn ddd_layers_exist() {
let dir = temp_dir("cg_ddd");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("ddd-app", &dir).unwrap();
assert!(
output
.join("business/src/domain/greeting/model.rs")
.exists()
);
assert!(
output
.join("business/src/domain/greeting/errors.rs")
.exists()
);
assert!(
output
.join("business/src/domain/greeting/repository.rs")
.exists()
);
assert!(
output
.join("business/src/domain/greeting/use_cases/get_greeting.rs")
.exists()
);
assert!(
output
.join("business/src/application/greeting/get_greeting.rs")
.exists()
);
assert!(
output
.join("infrastructure/src/greeting/repository.rs")
.exists()
);
assert!(
output
.join("presentation/src/api/greeting/routes.rs")
.exists()
);
assert!(output.join("presentation/src/api/greeting/dto.rs").exists());
assert!(
output
.join("presentation/src/api/greeting/error_mapper.rs")
.exists()
);
assert!(
output
.join("presentation/src/api/greeting/responses.rs")
.exists()
);
assert!(output.join("puerto.toml").exists());
assert!(output.join("presentation/src/generated.rs").exists());
assert!(
output
.join("presentation/src/generated/bootstrap.rs")
.exists()
);
assert!(output.join("presentation/src/api/tags.rs").exists());
cleanup(&dir);
}
#[test]
fn puerto_toml_has_project_name_and_greeting_entity() {
let dir = temp_dir("cg_puerto_toml");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("my-app", &dir).unwrap();
let content = fs::read_to_string(output.join("puerto.toml")).unwrap();
assert!(content.contains("name = \"my-app\""));
assert!(content.contains("name = \"Greeting\""));
assert!(content.contains("get_greeting"));
cleanup(&dir);
}
#[test]
#[ignore = "slow: compiles and tests a full generated project"]
fn generated_project_compiles_and_tests_pass() {
let dir = temp_dir("cg_compile");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("compile-test", &dir).unwrap();
let result = std::process::Command::new("cargo")
.args(["test", "--workspace"])
.current_dir(&output)
.output()
.expect("failed to run cargo test");
if !result.status.success() {
eprintln!("stdout:\n{}", String::from_utf8_lossy(&result.stdout));
eprintln!("stderr:\n{}", String::from_utf8_lossy(&result.stderr));
panic!("cargo test failed in generated project");
}
cleanup(&dir);
}
pub(super) fn new_project_non_interactive(
name: Option<String>,
db: bool,
destination: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
let tmp = extract_template()?;
let args = GenerateArgs {
template_path: TemplatePath {
path: Some(tmp.path().to_string_lossy().into_owned()),
..Default::default()
},
name: name.clone(),
destination: Some(destination.to_path_buf()),
vcs: Some(Vcs::None),
no_workspace: true,
..Default::default()
};
let output = generate(args)?;
drop(tmp);
if db {
scaffold::apply_db_to_new_project(&output)?;
}
Ok(output)
}
fn generate_db_project(
name: &str,
destination: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
let output = generate_project(name, destination)?;
scaffold::apply_db_to_new_project(&output)?;
Ok(output)
}
#[test]
fn db_project_has_env_example() {
let dir = temp_dir("db_env");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_db_project("db-app", &dir).unwrap();
let content = fs::read_to_string(output.join(".env.example")).unwrap();
assert!(content.contains("DATABASE_URL"));
assert!(content.contains("db_app"));
cleanup(&dir);
}
#[test]
fn db_project_has_env_file_with_project_db_name() {
let dir = temp_dir("db_env_file");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_db_project("my-api", &dir).unwrap();
let content = fs::read_to_string(output.join(".env")).unwrap();
assert!(content.contains("DATABASE_URL"));
assert!(content.contains("my_api"));
cleanup(&dir);
}
#[test]
fn db_project_has_docker_compose_with_project_name() {
let dir = temp_dir("db_docker_compose");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_db_project("my-api", &dir).unwrap();
let content = fs::read_to_string(output.join("docker-compose.yml")).unwrap();
assert!(content.contains("postgres:16"));
assert!(content.contains("my-api-postgres"));
assert!(content.contains("my_api"));
assert!(content.contains("5432"));
cleanup(&dir);
}
#[test]
fn db_project_has_gitignore_with_env() {
let dir = temp_dir("db_gitignore");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_db_project("db-app", &dir).unwrap();
let gitignore_path = output.join(".gitignore");
if gitignore_path.exists() {
let content = fs::read_to_string(&gitignore_path).unwrap();
assert!(content.contains(".env"));
}
cleanup(&dir);
}
#[test]
fn db_project_makefile_has_docker_and_sqlx_targets() {
let dir = temp_dir("db_makefile_targets");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_db_project("db-app", &dir).unwrap();
let content = fs::read_to_string(output.join("Makefile")).unwrap();
assert!(content.contains("docker-compose/up"));
assert!(content.contains("docker-compose/down"));
assert!(content.contains("sqlx/migrate"));
assert!(content.contains("sqlx/prepare"));
assert!(content.contains("sqlx/check"));
assert!(content.contains("sqlx/online"));
assert!(content.contains("sqlx/offline"));
assert!(content.contains("reset-db"));
assert!(content.contains("test/infrastructure"));
cleanup(&dir);
}
#[test]
fn db_project_has_cargo_config_with_sqlx_offline() {
let dir = temp_dir("db_cargo_config");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_db_project("db-app", &dir).unwrap();
let content = fs::read_to_string(output.join(".cargo/config.toml")).unwrap();
assert!(content.contains("SQLX_OFFLINE"));
cleanup(&dir);
}
#[test]
fn db_project_has_migrations_dir() {
let dir = temp_dir("db_migrations");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_db_project("db-app", &dir).unwrap();
assert!(output.join("infrastructure/migrations").is_dir());
cleanup(&dir);
}
#[test]
fn db_project_has_db_rs_with_pool_function() {
let dir = temp_dir("db_db_rs");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_db_project("db-app", &dir).unwrap();
let content = fs::read_to_string(output.join("infrastructure/src/db.rs")).unwrap();
assert!(content.contains("create_postgres_pool"));
assert!(content.contains("PgPool"));
cleanup(&dir);
}
#[test]
fn db_project_infrastructure_cargo_has_sqlx() {
let dir = temp_dir("db_infra_cargo");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_db_project("db-app", &dir).unwrap();
let content = fs::read_to_string(output.join("infrastructure/Cargo.toml")).unwrap();
assert!(content.contains("sqlx"));
assert!(content.contains("postgres"));
cleanup(&dir);
}
#[test]
fn db_project_puerto_toml_has_project_db_true() {
let dir = temp_dir("db_proj_toml");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_db_project("db-app", &dir).unwrap();
let config = puerto_toml::read(&output).unwrap();
assert!(
config.project.db,
"project.db should be true for --db projects"
);
cleanup(&dir);
}
#[test]
fn no_db_project_puerto_toml_omits_project_db() {
let dir = temp_dir("no_db_proj_toml");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("plain-app", &dir).unwrap();
let config = puerto_toml::read(&output).unwrap();
assert!(
!config.project.db,
"project.db should be false for non-db projects"
);
let content = fs::read_to_string(output.join("puerto.toml")).unwrap();
assert!(
!content.contains("db = true"),
"puerto.toml should not contain db = true for non-db projects"
);
cleanup(&dir);
}
#[test]
fn no_demo_has_no_greeting_files() {
let dir = temp_dir("no_demo_files");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = new_project_non_interactive(Some("no-demo-app".into()), false, &dir).unwrap();
scaffold::apply_no_demo(&output).unwrap();
assert!(!output.join("business/src/domain/greeting").exists());
assert!(!output.join("business/src/application/greeting").exists());
assert!(!output.join("infrastructure/src/greeting").exists());
assert!(!output.join("presentation/src/api/greeting").exists());
assert!(!output.join("presentation/src/api/greeting.rs").exists());
cleanup(&dir);
}
#[test]
fn no_demo_puerto_toml_has_no_entity_block() {
let dir = temp_dir("no_demo_toml");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = new_project_non_interactive(Some("no-demo-app".into()), false, &dir).unwrap();
scaffold::apply_no_demo(&output).unwrap();
let content = fs::read_to_string(output.join("puerto.toml")).unwrap();
assert!(
!content.contains("[[entity]]"),
"puerto.toml should have no entity blocks"
);
assert!(
!content.contains("Greeting"),
"puerto.toml should not mention Greeting"
);
assert!(
content.contains("[project]"),
"puerto.toml should still have [project]"
);
cleanup(&dir);
}
#[test]
fn no_demo_lib_files_have_no_greeting_modules() {
let dir = temp_dir("no_demo_lib");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = new_project_non_interactive(Some("no-demo-app".into()), false, &dir).unwrap();
scaffold::apply_no_demo(&output).unwrap();
let biz_lib = fs::read_to_string(output.join("business/src/lib.rs")).unwrap();
assert!(
!biz_lib.contains("greeting"),
"business lib.rs should not reference greeting"
);
let infra_lib = fs::read_to_string(output.join("infrastructure/src/lib.rs")).unwrap();
assert!(
!infra_lib.contains("greeting"),
"infra lib.rs should not reference greeting"
);
let api_rs = fs::read_to_string(output.join("presentation/src/api.rs")).unwrap();
assert!(
!api_rs.contains("greeting"),
"api.rs should not reference greeting"
);
assert!(
api_rs.contains("pub mod error"),
"api.rs should retain error module"
);
cleanup(&dir);
}
#[test]
fn no_demo_bootstrap_has_no_greeting() {
let dir = temp_dir("no_demo_bootstrap");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = new_project_non_interactive(Some("no-demo-app".into()), false, &dir).unwrap();
scaffold::apply_no_demo(&output).unwrap();
let content =
fs::read_to_string(output.join("presentation/src/generated/bootstrap.rs")).unwrap();
assert!(
!content.contains("Greeting"),
"bootstrap.rs should have no Greeting reference"
);
assert!(
!content.contains("greeting"),
"bootstrap.rs should have no greeting reference"
);
cleanup(&dir);
}
#[test]
fn default_project_still_has_greeting_files() {
let dir = temp_dir("with_demo");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = new_project_non_interactive(Some("demo-app".into()), false, &dir).unwrap();
assert!(
output
.join("business/src/domain/greeting/model.rs")
.exists()
);
assert!(
output
.join("presentation/src/api/greeting/routes.rs")
.exists()
);
cleanup(&dir);
}
#[test]
fn no_demo_then_scaffold_generates_new_entity() {
let dir = temp_dir("no_demo_then_scaffold");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = new_project_non_interactive(Some("no-demo-app".into()), false, &dir).unwrap();
scaffold::apply_no_demo(&output).unwrap();
scaffold::run_scaffold("Player", &output, &[], OverwritePolicy::Ask).unwrap();
assert!(output.join("business/src/domain/player/model.rs").exists());
assert!(
output
.join("presentation/src/api/player/routes.rs")
.exists()
);
assert!(!output.join("business/src/domain/greeting").exists());
cleanup(&dir);
}
#[test]
fn no_demo_keeps_tags_module_declared() {
let dir = temp_dir("no_demo_tags");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("no-demo-tags", &dir).unwrap();
scaffold::apply_no_demo(&output).unwrap();
let api_rs = fs::read_to_string(output.join("presentation/src/api.rs")).unwrap();
assert!(
api_rs.contains("pub mod tags;"),
"generated routes.rs import crate::api::tags::ApiTags — the declaration must survive \
--no-demo. Got:\n{api_rs}"
);
cleanup(&dir);
}
#[test]
fn api_rs_patch_restores_missing_tags_module() {
let dir = temp_dir("api_rs_tags_restore");
cleanup(&dir);
fs::create_dir_all(&dir).unwrap();
let output = generate_project("tags-restore", &dir).unwrap();
fs::write(output.join("presentation/src/api.rs"), "pub mod error;\n").unwrap();
crate::patchers::api_rs::patch_api_rs(&output, "product").unwrap();
let api_rs = fs::read_to_string(output.join("presentation/src/api.rs")).unwrap();
assert!(api_rs.contains("pub mod tags;"));
assert!(api_rs.contains("pub mod product;"));
cleanup(&dir);
}