use db::{Db, audit, users}; use domain::{AuditAction, AuditActor, Email, NewUser, Role}; use sqlx::PgPool; fn new_user(email: &str, role: Role) -> NewUser { NewUser { email: Email::parse(email).unwrap(), password_hash: "$argon2id$dummy".to_owned(), role, } } #[sqlx::test] async fn create_then_fetch_by_id_and_email(pool: PgPool) { let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); let id = users::create_user( &mut tx, AuditActor::System, &new_user("anna@example.com", Role::Admin), ) .await .unwrap(); tx.commit().await.unwrap(); let user = users::user_by_id(db.pool(), id).await.unwrap().unwrap(); assert_eq!(user.email.as_str(), "anna@example.com"); assert_eq!(user.role, Role::Admin); let (by_email, hash) = users::credentials_by_email(db.pool(), "anna@example.com") .await .unwrap() .unwrap(); assert_eq!(by_email.id, id); assert_eq!(hash, "$argon2id$dummy"); } #[sqlx::test] async fn create_user_audits_email_and_role_but_never_the_hash(pool: PgPool) { let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); let id = users::create_user( &mut tx, AuditActor::System, &new_user("anna@example.com", Role::Editor), ) .await .unwrap(); tx.commit().await.unwrap(); let history = audit::history_for(db.pool(), "user", id.to_uuid()) .await .unwrap(); assert_eq!(history.len(), 1); assert_eq!(history[0].action, AuditAction::Created); let mut fields: Vec<&str> = history[0] .changes .iter() .map(|c| c.field.as_str()) .collect(); fields.sort_unstable(); assert_eq!(fields, vec!["email", "role"]); } #[sqlx::test] async fn missing_email_returns_none(pool: PgPool) { let db = Db::from_pool(pool); assert!( users::credentials_by_email(db.pool(), "nobody@example.com") .await .unwrap() .is_none() ); } #[sqlx::test] async fn list_users_is_ordered_by_email(pool: PgPool) { let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); users::create_user( &mut tx, AuditActor::System, &new_user("zoe@example.com", Role::Editor), ) .await .unwrap(); users::create_user( &mut tx, AuditActor::System, &new_user("amy@example.com", Role::Admin), ) .await .unwrap(); tx.commit().await.unwrap(); let users = users::list_users(db.pool()).await.unwrap(); let emails: Vec<&str> = users.iter().map(|u| u.email.as_str()).collect(); assert_eq!(emails, vec!["amy@example.com", "zoe@example.com"]); } #[sqlx::test] async fn duplicate_email_is_rejected(pool: PgPool) { let db = Db::from_pool(pool); let mut tx = db.pool().begin().await.unwrap(); users::create_user( &mut tx, AuditActor::System, &new_user("anna@example.com", Role::Admin), ) .await .unwrap(); // Same normalized email again — the lower(email) unique index must reject it. let err = users::create_user( &mut tx, AuditActor::System, &new_user("anna@example.com", Role::Editor), ) .await .unwrap_err(); assert!( matches!(err, sqlx::Error::Database(_)), "expected a unique-violation database error, got {err:?}" ); }