From 8babaf8fdc4aaddbb364622d9f1ea7d05586a59d Mon Sep 17 00:00:00 2001 From: Elad Kaplan Date: Wed, 22 Oct 2025 08:56:49 +0300 Subject: [PATCH] adding create user task --- loco-new/Cargo.lock | 2 +- loco-new/base_template/src/app.rs.t | 3 + loco-new/base_template/src/tasks/mod.rs.t | 3 + .../base_template/src/tasks/user_create.rs | 99 +++++++++++++++++++ loco-new/base_template/tests/tasks/mod.rs.t | 3 + .../tests/tasks/user_create.rs.t | 58 +++++++++++ loco-new/setup.rhai | 3 + ...tes__auth__src_app_rs_auth_false_None.snap | 3 +- ...s__auth__src_app_rs_auth_false_Sqlite.snap | 3 +- ...ates__auth__src_app_rs_auth_true_None.snap | 2 + ...es__auth__src_app_rs_auth_true_Sqlite.snap | 2 + ...mplates__background__src_app_rs_Async.snap | 3 +- ...ates__background__src_app_rs_Blocking.snap | 3 +- ...emplates__background__src_app_rs_None.snap | 3 +- ...mplates__background__src_app_rs_Queue.snap | 3 +- ...r#mod__templates__db__src_app_rs_None.snap | 3 +- ...d__templates__db__src_app_rs_Postgres.snap | 3 +- ...mod__templates__db__src_app_rs_Sqlite.snap | 3 +- ...alizers__src_app_rs_with_initializers.snap | 3 +- ...zers__src_app_rs_without_initializers.snap | 3 +- 20 files changed, 196 insertions(+), 12 deletions(-) create mode 100644 loco-new/base_template/src/tasks/user_create.rs create mode 100644 loco-new/base_template/tests/tasks/user_create.rs.t diff --git a/loco-new/Cargo.lock b/loco-new/Cargo.lock index 40bed1ba0..910fd20b4 100644 --- a/loco-new/Cargo.lock +++ b/loco-new/Cargo.lock @@ -728,7 +728,7 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "loco" -version = "0.16.2" +version = "0.16.3" dependencies = [ "clap", "colored", diff --git a/loco-new/base_template/src/app.rs.t b/loco-new/base_template/src/app.rs.t index 84ac946a1..a0dbc05b4 100644 --- a/loco-new/base_template/src/app.rs.t +++ b/loco-new/base_template/src/app.rs.t @@ -93,6 +93,9 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { // tasks-inject (do not remove) + {%- if settings.auth %} + tasks.register(tasks::user_create::UserCreate); + {%- endif %} } {%- if settings.db %} diff --git a/loco-new/base_template/src/tasks/mod.rs.t b/loco-new/base_template/src/tasks/mod.rs.t index e69de29bb..a0023a2c0 100644 --- a/loco-new/base_template/src/tasks/mod.rs.t +++ b/loco-new/base_template/src/tasks/mod.rs.t @@ -0,0 +1,3 @@ +{%- if settings.auth %} +pub mod user_create; +{%- endif %} \ No newline at end of file diff --git a/loco-new/base_template/src/tasks/user_create.rs b/loco-new/base_template/src/tasks/user_create.rs new file mode 100644 index 000000000..5f74b8475 --- /dev/null +++ b/loco-new/base_template/src/tasks/user_create.rs @@ -0,0 +1,99 @@ +use loco_rs::prelude::*; + +use crate::{ + mailers::auth::AuthMailer, + models::{_entities::users, users::RegisterParams}, +}; + +pub struct UserCreate; +#[async_trait] +impl Task for UserCreate { + fn task(&self) -> TaskInfo { + TaskInfo { + name: "user:create".to_string(), + detail: "Create a new user with email, name, and password. Sends welcome email and sets up email verification.\nUsage:\ncargo run task user:create email:user@example.com name:\"John Doe\" password:\"securepassword\"".to_string(), + } + } + async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> { + let email = vars + .cli_arg("email") + .map_err(|_| Error::string("email is mandatory"))?; + let name = vars + .cli_arg("name") + .map_err(|_| Error::string("name is mandatory"))?; + let password = vars + .cli_arg("password") + .map_err(|_| Error::string("password is mandatory"))?; + + let register_params = RegisterParams { + email: email.to_string(), + password: password.to_string(), + name: name.to_string(), + }; + + // Create user with password using the same logic as register controller + let res = users::Model::create_with_password(&app_context.db, ®ister_params).await; + + let user = match res { + Ok(user) => { + tracing::info!( + message = "User created successfully", + user_email = ®ister_params.email, + user_pid = user.pid.to_string(), + "user created via task" + ); + user + } + Err(err) => { + tracing::error!( + message = err.to_string(), + user_email = ®ister_params.email, + "could not create user via task" + ); + return Err(Error::string( + &format!("Failed to create user. err: {err}",), + )); + } + }; + + // Set email verification sent (same as register controller) + let user = user + .into_active_model() + .set_email_verification_sent(&app_context.db) + .await + .map_err(|err| { + tracing::error!( + message = err.to_string(), + user_email = ®ister_params.email, + "could not set email verification" + ); + Error::string("Failed to set email verification") + })?; + + // Send welcome email (same as register controller) + AuthMailer::send_welcome(app_context, &user) + .await + .map_err(|err| { + tracing::error!( + message = err.to_string(), + user_email = ®ister_params.email, + "could not send welcome email" + ); + Error::string("Failed to send welcome email") + })?; + + tracing::info!( + message = "User creation task completed successfully", + user_email = ®ister_params.email, + user_pid = user.pid.to_string(), + "user creation task finished" + ); + + println!("✅ User created successfully!"); + println!(" Email: {}", user.email); + println!(" Name: {}", user.name); + println!(" PID: {}", user.pid); + + Ok(()) + } +} diff --git a/loco-new/base_template/tests/tasks/mod.rs.t b/loco-new/base_template/tests/tasks/mod.rs.t index e69de29bb..a0023a2c0 100644 --- a/loco-new/base_template/tests/tasks/mod.rs.t +++ b/loco-new/base_template/tests/tasks/mod.rs.t @@ -0,0 +1,3 @@ +{%- if settings.auth %} +pub mod user_create; +{%- endif %} \ No newline at end of file diff --git a/loco-new/base_template/tests/tasks/user_create.rs.t b/loco-new/base_template/tests/tasks/user_create.rs.t new file mode 100644 index 000000000..9acb186e3 --- /dev/null +++ b/loco-new/base_template/tests/tasks/user_create.rs.t @@ -0,0 +1,58 @@ +use loco_rs::{task, testing::prelude::*}; +use {{settings.module_name}}::{app::App, models::users}; + +use loco_rs::boot::run_task; +use serial_test::serial; + +#[tokio::test] +#[serial] +async fn test_can_run_user_create() { + let boot = boot_test::().await.unwrap(); + + let email = "test@example.com"; + let user = users::Model::find_by_email(&boot.app_context.db, email).await; + assert!(user.is_err()); + + let vars = task::Vars::from_cli_args(vec![ + ("email".to_string(), email.to_string()), + ("name".to_string(), "Test User".to_string()), + ("password".to_string(), "securepassword".to_string()), + ]); + assert!( + run_task::(&boot.app_context, Some(&"user:create".to_string()), &vars) + .await + .is_ok() + ); + + let deliveries = boot.app_context.mailer.unwrap().deliveries(); + assert_eq!(deliveries.count, 1, "Exactly one email should be sent"); + + let user = users::Model::find_by_email(&boot.app_context.db, email).await; + assert!(user.is_ok()); +} + +#[tokio::test] +#[serial] +async fn test_user_email_already_exists() { + let boot = boot_test::().await.unwrap(); + seed::(&boot.app_context).await.unwrap(); + + let email = "user1@example.com"; + + let vars = task::Vars::from_cli_args(vec![ + ("email".to_string(), email.to_string()), + ("name".to_string(), "Test User".to_string()), + ("password".to_string(), "securepassword".to_string()), + ]); + let err = run_task::(&boot.app_context, Some(&"user:create".to_string()), &vars) + .await + .expect_err("err"); + + assert_eq!( + err.to_string(), + "Failed to create user. err: Entity already exists" + ); + + let deliveries = boot.app_context.mailer.unwrap().deliveries(); + assert_eq!(deliveries.count, 0, "No email should be sent"); +} \ No newline at end of file diff --git a/loco-new/setup.rhai b/loco-new/setup.rhai index f8605a276..5d8c21f84 100644 --- a/loco-new/setup.rhai +++ b/loco-new/setup.rhai @@ -69,6 +69,7 @@ if db { gen.copy_file("migration/src/m20220101_000001_users.rs"); // Users migration file gen.copy_file("src/models/_entities/users.rs"); // Users entity definition gen.copy_file("src/models/users.rs"); // Users model logic + gen.copy_file("src/tasks/user_create.rs"); // Fixtures and test setup for authentication gen.copy_dir("src/fixtures"); // Database fixtures directory @@ -77,6 +78,8 @@ if db { gen.copy_dir("tests/models/snapshots"); // Test snapshots for models gen.copy_template("tests/models/users.rs.t"); // User model test template gen.copy_template("tests/requests/prepare_data.rs.t"); // Data preparation template for tests + gen.copy_template("tests/tasks/user_create.rs.t"); + } gen.copy_template("examples/playground.rs.t"); // Example playground template with DB setup diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_false_None.snap b/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_false_None.snap index 74432aa13..a103518a3 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_false_None.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_false_None.snap @@ -1,6 +1,7 @@ --- source: tests/templates/auth.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use async_trait::async_trait; use loco_rs::{ @@ -55,6 +56,6 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { - // tasks-inject (do not remove) + // tasks-inject (do not remove) } } diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_false_Sqlite.snap b/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_false_Sqlite.snap index d36c51ac8..8715b030d 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_false_Sqlite.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_false_Sqlite.snap @@ -1,6 +1,7 @@ --- source: tests/templates/auth.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use std::path::Path; use async_trait::async_trait; @@ -58,7 +59,7 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { - // tasks-inject (do not remove) + // tasks-inject (do not remove) } async fn truncate(_ctx: &AppContext) -> Result<()> { Ok(()) diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_true_None.snap b/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_true_None.snap index e4b17057e..3eee6112f 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_true_None.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_true_None.snap @@ -1,6 +1,7 @@ --- source: tests/templates/auth.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use async_trait::async_trait; use loco_rs::{ @@ -58,5 +59,6 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { // tasks-inject (do not remove) + tasks.register(tasks::user_create::UserCreate); } } diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_true_Sqlite.snap b/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_true_Sqlite.snap index cafc3cd62..924c184f8 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_true_Sqlite.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__auth__src_app_rs_auth_true_Sqlite.snap @@ -1,6 +1,7 @@ --- source: tests/templates/auth.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use std::path::Path; use async_trait::async_trait; @@ -61,6 +62,7 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { // tasks-inject (do not remove) + tasks.register(tasks::user_create::UserCreate); } async fn truncate(ctx: &AppContext) -> Result<()> { truncate_table(&ctx.db, users::Entity).await?; diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Async.snap b/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Async.snap index ef4238632..06705a9d1 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Async.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Async.snap @@ -1,6 +1,7 @@ --- source: tests/templates/background.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use async_trait::async_trait; use loco_rs::{ @@ -58,6 +59,6 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { - // tasks-inject (do not remove) + // tasks-inject (do not remove) } } diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Blocking.snap b/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Blocking.snap index ef4238632..06705a9d1 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Blocking.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Blocking.snap @@ -1,6 +1,7 @@ --- source: tests/templates/background.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use async_trait::async_trait; use loco_rs::{ @@ -58,6 +59,6 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { - // tasks-inject (do not remove) + // tasks-inject (do not remove) } } diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_None.snap b/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_None.snap index a16af2cfb..d4758d0e0 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_None.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_None.snap @@ -1,6 +1,7 @@ --- source: tests/templates/background.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use async_trait::async_trait; use loco_rs::{ @@ -55,6 +56,6 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { - // tasks-inject (do not remove) + // tasks-inject (do not remove) } } diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Queue.snap b/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Queue.snap index ef4238632..06705a9d1 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Queue.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__background__src_app_rs_Queue.snap @@ -1,6 +1,7 @@ --- source: tests/templates/background.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use async_trait::async_trait; use loco_rs::{ @@ -58,6 +59,6 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { - // tasks-inject (do not remove) + // tasks-inject (do not remove) } } diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_None.snap b/loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_None.snap index 4dbd95391..c868f4118 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_None.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_None.snap @@ -1,6 +1,7 @@ --- source: tests/templates/db.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use async_trait::async_trait; use loco_rs::{ @@ -55,6 +56,6 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { - // tasks-inject (do not remove) + // tasks-inject (do not remove) } } diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_Postgres.snap b/loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_Postgres.snap index 6714b3a2c..c11af1a4e 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_Postgres.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_Postgres.snap @@ -1,6 +1,7 @@ --- source: tests/templates/db.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use std::path::Path; use async_trait::async_trait; @@ -58,7 +59,7 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { - // tasks-inject (do not remove) + // tasks-inject (do not remove) } async fn truncate(_ctx: &AppContext) -> Result<()> { Ok(()) diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_Sqlite.snap b/loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_Sqlite.snap index 6714b3a2c..c11af1a4e 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_Sqlite.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__db__src_app_rs_Sqlite.snap @@ -1,6 +1,7 @@ --- source: tests/templates/db.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use std::path::Path; use async_trait::async_trait; @@ -58,7 +59,7 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { - // tasks-inject (do not remove) + // tasks-inject (do not remove) } async fn truncate(_ctx: &AppContext) -> Result<()> { Ok(()) diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__initializers__src_app_rs_with_initializers.snap b/loco-new/tests/templates/snapshots/r#mod__templates__initializers__src_app_rs_with_initializers.snap index a53e75ee8..934ebc0f1 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__initializers__src_app_rs_with_initializers.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__initializers__src_app_rs_with_initializers.snap @@ -1,6 +1,7 @@ --- source: tests/templates/initializers.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use async_trait::async_trait; use loco_rs::{ @@ -55,6 +56,6 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { - // tasks-inject (do not remove) + // tasks-inject (do not remove) } } diff --git a/loco-new/tests/templates/snapshots/r#mod__templates__initializers__src_app_rs_without_initializers.snap b/loco-new/tests/templates/snapshots/r#mod__templates__initializers__src_app_rs_without_initializers.snap index 04f925ee1..e8f85021d 100644 --- a/loco-new/tests/templates/snapshots/r#mod__templates__initializers__src_app_rs_without_initializers.snap +++ b/loco-new/tests/templates/snapshots/r#mod__templates__initializers__src_app_rs_without_initializers.snap @@ -1,6 +1,7 @@ --- source: tests/templates/initializers.rs expression: "std::fs::read_to_string(generator.path(\"src/app.rs\")).expect(\"could not open file\")" +snapshot_kind: text --- use async_trait::async_trait; use loco_rs::{ @@ -55,6 +56,6 @@ impl Hooks for App { #[allow(unused_variables)] fn register_tasks(tasks: &mut Tasks) { - // tasks-inject (do not remove) + // tasks-inject (do not remove) } }