use kegani_cli::GenKind;
use std::fs;
use std::path::PathBuf;
struct WithTempDir {
_temp: tempfile::TempDir,
_original: PathBuf,
}
impl WithTempDir {
fn new() -> Self {
let original = std::env::current_dir()
.ok()
.map(|p| p.canonicalize().unwrap_or(p))
.unwrap_or_else(|| PathBuf::from("/"));
let temp = tempfile::TempDir::new().expect("create temp dir");
let tmp = temp.path().to_path_buf();
let canonical_tmp = tmp.canonicalize().unwrap_or(tmp);
std::env::set_current_dir(&canonical_tmp).expect("change dir to temp");
Self {
_temp: temp,
_original: original,
}
}
fn path(&self) -> PathBuf {
self._temp
.path()
.canonicalize()
.unwrap_or_else(|_| self._temp.path().to_path_buf())
}
}
impl Drop for WithTempDir {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self._original);
}
}
mod clean_tests {
use super::*;
#[test]
fn test_clean_idempotent_when_no_target() {
let result = kegani_cli::commands::clean::clean(false);
assert!(result.is_ok(), "clean should not error when target absent");
}
#[test]
fn test_clean_removes_target_dir() {
let guard = WithTempDir::new();
let tmp = guard.path();
let fake_target = tmp.join("target");
fs::create_dir_all(fake_target.join("debug")).expect("create fake target");
fs::write(fake_target.join("debug").join("a.bin"), b"artifact").expect("write artifact");
assert!(fake_target.exists());
let result = kegani_cli::commands::clean::clean(false);
assert!(result.is_ok());
assert!(!fake_target.exists(), "target should be removed");
}
}
mod init_tests {
use super::*;
fn init_project(name: &str) -> (PathBuf, WithTempDir) {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init(name, "api").expect("init should succeed");
(tmp.join(name), guard)
}
#[test]
fn test_init_creates_project_directory() {
let (project_dir, _guard) = init_project("my-api");
assert!(project_dir.is_dir(), "project directory should exist");
}
#[test]
fn test_init_writes_cargo_toml() {
let (project_dir, _guard) = init_project("sample-app");
let cargo = fs::read_to_string(project_dir.join("Cargo.toml")).unwrap();
assert!(cargo.contains(r#"name = "sample-app""#));
assert!(cargo.contains("kegani"));
assert!(cargo.contains("actix-web"));
assert!(cargo.contains("tokio"));
}
#[test]
fn test_init_writes_main_rs() {
let (project_dir, _guard) = init_project("hello-kegani");
let main_rs = fs::read_to_string(project_dir.join("src/main.rs")).unwrap();
assert!(main_rs.contains("#[tokio::main]"));
assert!(main_rs.contains("tracing_subscriber"));
assert!(main_rs.contains("App::new"));
}
#[test]
fn test_init_writes_lib_rs() {
let (project_dir, _guard) = init_project("lib-test");
let lib = fs::read_to_string(project_dir.join("src/lib.rs")).unwrap();
assert!(lib.contains("pub mod error"));
assert!(lib.contains("pub mod routes"));
assert!(lib.contains("pub mod controller"));
assert!(lib.contains("pub mod middleware"));
}
#[test]
fn test_init_writes_error_rs() {
let (project_dir, _guard) = init_project("error-test");
let error = fs::read_to_string(project_dir.join("src/error.rs")).unwrap();
assert!(error.contains("struct AppError"));
assert!(error.contains("impl ResponseError for AppError"));
}
#[test]
fn test_init_writes_routes_mod() {
let (project_dir, _guard) = init_project("routes-test");
let routes = fs::read_to_string(project_dir.join("src/routes/mod.rs")).unwrap();
assert!(routes.contains("pub mod health"));
assert!(routes.contains("pub use health::health_scope"));
}
#[test]
fn test_init_writes_controller_api_mod() {
let (project_dir, _guard) = init_project("ctrl-test");
let ctrl = fs::read_to_string(project_dir.join("src/controller/api/mod.rs")).unwrap();
assert!(ctrl.contains("keg gen api"));
}
#[test]
fn test_init_writes_api_routes_mod() {
let (project_dir, _guard) = init_project("api-routes-test");
let routes = fs::read_to_string(project_dir.join("src/routes/api/mod.rs")).unwrap();
assert!(routes.contains("keg gen api"));
}
#[test]
fn test_init_creates_api_v1_dir() {
let (project_dir, _guard) = init_project("v1-dir-test");
assert!(project_dir.join("src/routes/api/v1").is_dir(), "src/routes/api/v1 dir should exist");
}
#[test]
fn test_init_writes_manifest_config() {
let (project_dir, _guard) = init_project("cfg-test");
let config = fs::read_to_string(project_dir.join("manifest/config/config.yaml")).unwrap();
assert!(config.contains("app:"));
assert!(config.contains("database:"));
assert!(config.contains("redis:"));
assert!(config.contains("logging:"));
}
#[test]
fn test_init_writes_dockerfile() {
let (project_dir, _guard) = init_project("docker-test");
let df = fs::read_to_string(project_dir.join("Dockerfile")).unwrap();
assert!(df.contains("FROM rust:"));
assert!(df.contains("WORKDIR /app"));
}
#[test]
fn test_init_writes_docker_compose() {
let (project_dir, _guard) = init_project("compose-test");
let compose = fs::read_to_string(project_dir.join("docker-compose.yml")).unwrap();
assert!(compose.contains("postgres:"));
assert!(compose.contains("redis:"));
assert!(compose.contains("services:"));
}
#[test]
fn test_init_writes_gitignore() {
let (project_dir, _guard) = init_project("git-test");
let gitignore = fs::read_to_string(project_dir.join(".gitignore")).unwrap();
assert!(gitignore.contains("/target/"));
assert!(gitignore.contains(".env"));
}
#[test]
fn test_init_writes_readme() {
let (project_dir, _guard) = init_project("readme-test");
let readme = fs::read_to_string(project_dir.join("README.md")).unwrap();
assert!(readme.contains("readme-test"));
assert!(readme.contains("Kegani"));
}
#[test]
fn test_init_writes_env_example() {
let (project_dir, _guard) = init_project("env-test");
let env = fs::read_to_string(project_dir.join(".env.example")).unwrap();
assert!(env.contains("DATABASE_URL"));
assert!(env.contains("REDIS_URL"));
assert!(env.contains("JWT_SECRET"));
}
#[test]
fn test_init_creates_internal_layers() {
let (project_dir, _guard) = init_project("layers-test");
assert!(project_dir.join("internal/model/entity").is_dir(), "internal/model/entity should exist");
assert!(project_dir.join("internal/dto").is_dir(), "internal/dto should exist");
assert!(project_dir.join("internal/repository").is_dir(), "internal/repository should exist");
assert!(project_dir.join("internal/logic").is_dir(), "internal/logic should exist");
assert!(project_dir.join("internal/service").is_dir(), "internal/service should exist");
}
#[test]
fn test_init_creates_resource_openapi() {
let (project_dir, _guard) = init_project("openapi-test");
assert!(project_dir.join("resource/openapi").is_dir(), "resource/openapi should exist");
let schema = fs::read_to_string(project_dir.join("resource/openapi/schema.yaml")).unwrap();
assert!(schema.contains("openapi:"));
}
#[test]
fn test_init_writes_migrations_dir() {
let (project_dir, _guard) = init_project("migrate-test");
assert!(project_dir.join("migrations").is_dir(), "migrations dir should exist");
}
#[test]
fn test_init_writes_middleware() {
let (project_dir, _guard) = init_project("middleware-test");
assert!(project_dir.join("src/middleware").is_dir(), "src/middleware should exist");
let auth = fs::read_to_string(project_dir.join("src/middleware/auth.rs")).unwrap();
assert!(auth.contains("JwtAuth"));
assert!(auth.contains("extract_bearer_token"));
}
#[test]
fn test_init_writes_health_routes() {
let (project_dir, _guard) = init_project("health-test");
let health = fs::read_to_string(project_dir.join("src/routes/health.rs")).unwrap();
assert!(health.contains("/health"));
assert!(health.contains("/health/live"));
assert!(health.contains("/health/ready"));
}
#[test]
fn test_init_writes_tests_dir() {
let (project_dir, _guard) = init_project("tests-test");
assert!(project_dir.join("tests").is_dir(), "tests dir should exist");
assert!(project_dir.join("tests/api_test.rs").exists());
}
#[test]
fn test_init_errors_on_non_empty_dir() {
let guard = WithTempDir::new();
let tmp = guard.path();
let project_dir = tmp.join("existing");
fs::create_dir_all(&project_dir).expect("create dir");
fs::write(project_dir.join("README.md"), "existing content").expect("write file");
let result = kegani_cli::commands::init::init("existing", "api");
assert!(result.is_err(), "should error on non-empty directory");
}
}
mod gen_api_tests {
use super::*;
#[test]
fn test_gen_api_creates_entity() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("test-proj", "api").expect("init");
std::env::set_current_dir(&tmp.join("test-proj")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Api, "article").expect("gen api");
let entity = tmp.join("test-proj/internal/model/entity/article.rs");
assert!(entity.exists(), "entity file should exist: {:?}", entity);
let c = fs::read_to_string(&entity).unwrap();
assert!(c.contains("pub struct Article"), "should have Article struct");
assert!(c.contains("CreateArticle"), "should have CreateArticle");
assert!(c.contains("UpdateArticle"), "should have UpdateArticle");
assert!(c.contains("impl Article"), "should have impl");
}
#[test]
fn test_gen_api_creates_repository() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("repo-test", "api").expect("init");
std::env::set_current_dir(&tmp.join("repo-test")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Api, "post").expect("gen api");
let repo = tmp.join("repo-test/internal/repository/post_repo.rs");
assert!(repo.exists(), "repository file should exist: {:?}", repo);
let c = fs::read_to_string(&repo).unwrap();
assert!(c.contains("PostRepository"));
assert!(c.contains("PgPool"));
assert!(c.contains("pub async fn list"));
assert!(c.contains("pub async fn find_by_id"));
}
#[test]
fn test_gen_api_creates_logic() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("logic-test", "api").expect("init");
std::env::set_current_dir(&tmp.join("logic-test")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Api, "user").expect("gen api");
let logic = tmp.join("logic-test/internal/logic/user_logic.rs");
assert!(logic.exists(), "logic file should exist: {:?}", logic);
let c = fs::read_to_string(&logic).unwrap();
assert!(c.contains("UserLogic"));
assert!(c.contains("pub async fn list"));
assert!(c.contains("pub async fn create"));
}
#[test]
fn test_gen_api_creates_service() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("service-test", "api").expect("init");
std::env::set_current_dir(&tmp.join("service-test")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Api, "order").expect("gen api");
let service = tmp.join("service-test/internal/service/order_service.rs");
assert!(service.exists(), "service file should exist: {:?}", service);
let c = fs::read_to_string(&service).unwrap();
assert!(c.contains("OrderService"));
}
#[test]
fn test_gen_api_creates_controller() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("ctrl-api-test", "api").expect("init");
std::env::set_current_dir(&tmp.join("ctrl-api-test")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Api, "item").expect("gen api");
let ctrl = tmp.join("ctrl-api-test/src/controller/api/item.rs");
assert!(ctrl.exists(), "controller file should exist: {:?}", ctrl);
let c = fs::read_to_string(&ctrl).unwrap();
assert!(c.contains("pub async fn list"));
assert!(c.contains("pub async fn get"));
assert!(c.contains("pub async fn create"));
assert!(c.contains("pub async fn delete"));
assert!(c.contains("pub async fn update"));
}
#[test]
fn test_gen_api_creates_routes() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("routes-api-test", "api").expect("init");
std::env::set_current_dir(&tmp.join("routes-api-test")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Api, "comment").expect("gen api");
let routes = tmp.join("routes-api-test/src/routes/api/v1/comment.rs");
assert!(routes.exists(), "routes file should exist: {:?}", routes);
let c = fs::read_to_string(&routes).unwrap();
assert!(c.contains("web::scope(\"/comment\")"));
assert!(c.contains("web::get"));
assert!(c.contains("web::post"));
}
}
mod gen_model_tests {
use super::*;
#[test]
fn test_gen_model_creates_entity() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("model-test", "api").expect("init");
std::env::set_current_dir(&tmp.join("model-test")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Model, "book").expect("gen model");
let entity = tmp.join("model-test/internal/model/entity/book.rs");
assert!(entity.exists(), "entity file should exist");
let c = fs::read_to_string(&entity).unwrap();
assert!(c.contains("pub struct Book"));
assert!(c.contains("pub id: Uuid"));
assert!(c.contains("pub name: String"));
}
}
mod gen_migration_tests {
use super::*;
#[test]
fn test_gen_migration_creates_sql_file() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("mig-test", "api").expect("init");
std::env::set_current_dir(&tmp.join("mig-test")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Migration, "create_posts").expect("gen migration");
let migrations_dir = tmp.join("mig-test/migrations");
let mut entries: Vec<_> = fs::read_dir(migrations_dir)
.unwrap()
.filter_map(|e| e.ok())
.collect();
entries.sort_by_key(|e| e.path());
let last = entries.last().expect("a migration file should exist");
let c = fs::read_to_string(last.path()).unwrap();
assert!(c.contains("create_posts"), "should mention migration name");
assert!(c.contains("CREATE TABLE"), "should have CREATE TABLE");
assert!(c.contains("id UUID PRIMARY KEY"), "should have id PK");
assert!(c.contains("name VARCHAR(255)"), "should have name column");
}
#[test]
fn test_gen_migration_has_timestamp_prefix() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("ts-test", "api").expect("init");
std::env::set_current_dir(&tmp.join("ts-test")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Migration, "books").expect("gen migration");
let migrations_dir = tmp.join("ts-test/migrations");
let mut entries: Vec<_> = fs::read_dir(migrations_dir).unwrap().filter_map(|e| e.ok()).collect();
entries.sort_by_key(|e| e.path());
let binding = entries.last().unwrap().file_name();
let name = binding.to_str().unwrap();
assert!(
name.starts_with(|c: char| c.is_ascii_digit()),
"migration filename should start with digits (timestamp): got {:?}",
name
);
assert!(name.ends_with("_books.sql"), "filename should end with _books.sql");
}
}
mod gen_controller_tests {
use super::*;
#[test]
fn test_gen_controller_creates_controller_and_routes() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("ctrl-gen-test", "api").expect("init");
std::env::set_current_dir(&tmp.join("ctrl-gen-test")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Controller, "product").expect("gen controller");
let ctrl = tmp.join("ctrl-gen-test/src/controller/api/product.rs");
assert!(ctrl.exists(), "controller file should exist");
let c = fs::read_to_string(&ctrl).unwrap();
assert!(c.contains("pub async fn list"));
assert!(c.contains("pub async fn get"));
assert!(c.contains("pub async fn create"));
assert!(c.contains("pub async fn update"));
assert!(c.contains("pub async fn delete"));
let routes = tmp.join("ctrl-gen-test/src/routes/api/v1/product.rs");
assert!(routes.exists(), "routes file should exist");
}
}
mod gen_service_tests {
use super::*;
#[test]
fn test_gen_service_creates_service_file() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("svc-gen-test", "api").expect("init");
std::env::set_current_dir(&tmp.join("svc-gen-test")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Service, "inventory").expect("gen service");
let svc = tmp.join("svc-gen-test/internal/service/inventory_service.rs");
assert!(svc.exists(), "service file should exist");
let c = fs::read_to_string(&svc).unwrap();
assert!(c.contains("InventoryService"));
}
}
mod gen_repository_tests {
use super::*;
#[test]
fn test_gen_repository_creates_repo_file() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("repo-gen-test", "api").expect("init");
std::env::set_current_dir(&tmp.join("repo-gen-test")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Repository, "category").expect("gen repository");
let repo = tmp.join("repo-gen-test/internal/repository/category_repo.rs");
assert!(repo.exists(), "repository file should exist");
let c = fs::read_to_string(&repo).unwrap();
assert!(c.contains("CategoryRepository"));
assert!(c.contains("PgPool"));
}
}
mod gen_middleware_tests {
use super::*;
#[test]
fn test_gen_middleware_creates_middleware_file() {
let guard = WithTempDir::new();
let tmp = guard.path();
kegani_cli::commands::init::init("mw-gen-test", "api").expect("init");
std::env::set_current_dir(&tmp.join("mw-gen-test")).expect("change dir");
kegani_cli::commands::gen::gen(GenKind::Middleware, "ratelimit").expect("gen middleware");
let mw = tmp.join("mw-gen-test/src/middleware/ratelimit.rs");
assert!(mw.exists(), "middleware file should exist");
}
}