diff --git a/backend/src/main.rs b/backend/src/main.rs index 91f64178..272f1cff 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -17,8 +17,6 @@ use auth::{auth_middleware, hash_password}; use models::ApiResponse; use routes::{health, projects, tasks, users}; - - async fn echo_handler( Json(payload): Json, ) -> ResponseJson> { @@ -32,10 +30,15 @@ async fn echo_handler( #[tokio::main] async fn main() -> anyhow::Result<()> { // Load environment variables from .env file - dotenvy::dotenv().ok(); + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?; + dotenvy::from_path(format!("{manifest_dir}/.env")).ok(); + // dotenvy::dotenv().ok(); tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env().add_directive("bloop_backend=debug".parse()?)) + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("bloop_backend=debug".parse()?), + ) .init(); // Database connection @@ -87,8 +90,7 @@ async fn create_admin_account(pool: &sqlx::PgPool) -> anyhow::Result<()> { use uuid::Uuid; let admin_email = "admin@example.com"; - let admin_password = env::var("ADMIN_PASSWORD") - .unwrap_or_else(|_| "admin123".to_string()); + let admin_password = env::var("ADMIN_PASSWORD").unwrap_or_else(|_| "admin123".to_string()); // Check if admin already exists let existing_admin = sqlx::query!( @@ -112,7 +114,7 @@ async fn create_admin_account(pool: &sqlx::PgPool) -> anyhow::Result<()> { ) .execute(pool) .await?; - + tracing::info!("Updated admin account"); } else { // Create new admin account diff --git a/backend/src/routes/users.rs b/backend/src/routes/users.rs index 5721a917..38f334d7 100644 --- a/backend/src/routes/users.rs +++ b/backend/src/routes/users.rs @@ -319,3 +319,357 @@ pub fn protected_users_router() -> Router { .route("/users", get(get_users).post(create_user)) .route("/users/:id", get(get_user).put(update_user).delete(delete_user)) } + +#[cfg(test)] +mod tests { + use super::*; + use axum::extract::Extension; + use sqlx::PgPool; + use uuid::Uuid; + use chrono::Utc; + use crate::models::user::{LoginRequest, CreateUser, UpdateUser}; + use crate::auth::{AuthUser, hash_password}; + + async fn create_test_user(pool: &PgPool, email: &str, password: &str, is_admin: bool) -> User { + let id = Uuid::new_v4(); + let now = Utc::now(); + let password_hash = hash_password(password).unwrap(); + + sqlx::query_as!( + User, + "INSERT INTO users (id, email, password_hash, is_admin, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, email, password_hash, is_admin, created_at, updated_at", + id, + email, + password_hash, + is_admin, + now, + now + ) + .fetch_one(pool) + .await + .unwrap() + } + + #[sqlx::test] + async fn test_login_success(pool: PgPool) { + let user = create_test_user(&pool, "test@example.com", "password123", false).await; + + let login_request = LoginRequest { + email: "test@example.com".to_string(), + password: "password123".to_string(), + }; + + let result = login(Extension(pool), Json(login_request)).await; + assert!(result.is_ok()); + + let response = result.unwrap().0; + assert!(response.success); + assert!(response.data.is_some()); + assert_eq!(response.data.as_ref().unwrap().user.email, user.email); + } + + #[sqlx::test] + async fn test_login_invalid_password(pool: PgPool) { + create_test_user(&pool, "test@example.com", "password123", false).await; + + let login_request = LoginRequest { + email: "test@example.com".to_string(), + password: "wrongpassword".to_string(), + }; + + let result = login(Extension(pool), Json(login_request)).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), StatusCode::UNAUTHORIZED); + } + + #[sqlx::test] + async fn test_login_user_not_found(pool: PgPool) { + let login_request = LoginRequest { + email: "nonexistent@example.com".to_string(), + password: "password123".to_string(), + }; + + let result = login(Extension(pool), Json(login_request)).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), StatusCode::UNAUTHORIZED); + } + + #[sqlx::test] + async fn test_get_users_as_admin(pool: PgPool) { + let admin_user = create_test_user(&pool, "admin@example.com", "password123", true).await; + create_test_user(&pool, "user1@example.com", "password123", false).await; + create_test_user(&pool, "user2@example.com", "password123", false).await; + + let auth = AuthUser { + user_id: admin_user.id, + email: admin_user.email, + is_admin: true, + }; + + let result = get_users(auth, Extension(pool)).await; + assert!(result.is_ok()); + + let response = result.unwrap().0; + assert!(response.success); + assert!(response.data.is_some()); + assert_eq!(response.data.unwrap().len(), 3); + } + + #[sqlx::test] + async fn test_get_user_own_profile(pool: PgPool) { + let user = create_test_user(&pool, "test@example.com", "password123", false).await; + + let auth = AuthUser { + user_id: user.id, + email: user.email.clone(), + is_admin: false, + }; + + let result = get_user(auth, Path(user.id), Extension(pool)).await; + assert!(result.is_ok()); + + let response = result.unwrap().0; + assert!(response.success); + assert!(response.data.is_some()); + assert_eq!(response.data.unwrap().email, user.email); + } + + #[sqlx::test] + async fn test_get_user_forbidden_non_admin(pool: PgPool) { + let user1 = create_test_user(&pool, "user1@example.com", "password123", false).await; + let user2 = create_test_user(&pool, "user2@example.com", "password123", false).await; + + let auth = AuthUser { + user_id: user1.id, + email: user1.email, + is_admin: false, + }; + + let result = get_user(auth, Path(user2.id), Extension(pool)).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN); + } + + #[sqlx::test] + async fn test_get_user_admin_can_view_any(pool: PgPool) { + let admin_user = create_test_user(&pool, "admin@example.com", "password123", true).await; + let regular_user = create_test_user(&pool, "user@example.com", "password123", false).await; + + let auth = AuthUser { + user_id: admin_user.id, + email: admin_user.email, + is_admin: true, + }; + + let result = get_user(auth, Path(regular_user.id), Extension(pool)).await; + assert!(result.is_ok()); + + let response = result.unwrap().0; + assert!(response.success); + assert!(response.data.is_some()); + assert_eq!(response.data.unwrap().email, regular_user.email); + } + + #[sqlx::test] + async fn test_create_user_as_admin(pool: PgPool) { + let admin_user = create_test_user(&pool, "admin@example.com", "password123", true).await; + + let auth = AuthUser { + user_id: admin_user.id, + email: admin_user.email, + is_admin: true, + }; + + let create_request = CreateUser { + email: "newuser@example.com".to_string(), + password: "password123".to_string(), + is_admin: Some(false), + }; + + let result = create_user(auth, Extension(pool), Json(create_request)).await; + assert!(result.is_ok()); + + let response = result.unwrap().0; + assert!(response.success); + assert!(response.data.is_some()); + assert_eq!(response.data.unwrap().email, "newuser@example.com"); + } + + #[sqlx::test] + async fn test_create_user_forbidden_non_admin(pool: PgPool) { + let regular_user = create_test_user(&pool, "user@example.com", "password123", false).await; + + let auth = AuthUser { + user_id: regular_user.id, + email: regular_user.email, + is_admin: false, + }; + + let create_request = CreateUser { + email: "newuser@example.com".to_string(), + password: "password123".to_string(), + is_admin: Some(false), + }; + + let result = create_user(auth, Extension(pool), Json(create_request)).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN); + } + + #[sqlx::test] + async fn test_create_user_duplicate_email(pool: PgPool) { + let admin_user = create_test_user(&pool, "admin@example.com", "password123", true).await; + create_test_user(&pool, "existing@example.com", "password123", false).await; + + let auth = AuthUser { + user_id: admin_user.id, + email: admin_user.email, + is_admin: true, + }; + + let create_request = CreateUser { + email: "existing@example.com".to_string(), + password: "password123".to_string(), + is_admin: Some(false), + }; + + let result = create_user(auth, Extension(pool), Json(create_request)).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), StatusCode::CONFLICT); + } + + #[sqlx::test] + async fn test_update_user_own_profile(pool: PgPool) { + let user = create_test_user(&pool, "user@example.com", "password123", false).await; + + let auth = AuthUser { + user_id: user.id, + email: user.email.clone(), + is_admin: false, + }; + + let update_request = UpdateUser { + email: Some("newemail@example.com".to_string()), + password: Some("newpassword123".to_string()), + is_admin: None, + }; + + let result = update_user(auth, Path(user.id), Extension(pool), Json(update_request)).await; + assert!(result.is_ok()); + + let response = result.unwrap().0; + assert!(response.success); + assert!(response.data.is_some()); + assert_eq!(response.data.unwrap().email, "newemail@example.com"); + } + + #[sqlx::test] + async fn test_update_user_forbidden_non_admin(pool: PgPool) { + let user1 = create_test_user(&pool, "user1@example.com", "password123", false).await; + let user2 = create_test_user(&pool, "user2@example.com", "password123", false).await; + + let auth = AuthUser { + user_id: user1.id, + email: user1.email, + is_admin: false, + }; + + let update_request = UpdateUser { + email: Some("newemail@example.com".to_string()), + password: None, + is_admin: None, + }; + + let result = update_user(auth, Path(user2.id), Extension(pool), Json(update_request)).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN); + } + + #[sqlx::test] + async fn test_delete_user_as_admin(pool: PgPool) { + let admin_user = create_test_user(&pool, "admin@example.com", "password123", true).await; + let user_to_delete = create_test_user(&pool, "delete@example.com", "password123", false).await; + + let auth = AuthUser { + user_id: admin_user.id, + email: admin_user.email, + is_admin: true, + }; + + let result = delete_user(auth, Path(user_to_delete.id), Extension(pool)).await; + assert!(result.is_ok()); + + let response = result.unwrap().0; + assert!(response.success); + assert_eq!(response.message.unwrap(), "User deleted successfully"); + } + + #[sqlx::test] + async fn test_delete_user_forbidden_non_admin(pool: PgPool) { + let user1 = create_test_user(&pool, "user1@example.com", "password123", false).await; + let user2 = create_test_user(&pool, "user2@example.com", "password123", false).await; + + let auth = AuthUser { + user_id: user1.id, + email: user1.email, + is_admin: false, + }; + + let result = delete_user(auth, Path(user2.id), Extension(pool)).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN); + } + + #[sqlx::test] + async fn test_delete_user_self_forbidden(pool: PgPool) { + let admin_user = create_test_user(&pool, "admin@example.com", "password123", true).await; + + let auth = AuthUser { + user_id: admin_user.id, + email: admin_user.email.clone(), + is_admin: true, + }; + + let result = delete_user(auth, Path(admin_user.id), Extension(pool)).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN); + } + + #[sqlx::test] + async fn test_get_current_user(pool: PgPool) { + let user = create_test_user(&pool, "user@example.com", "password123", false).await; + + let auth = AuthUser { + user_id: user.id, + email: user.email.clone(), + is_admin: false, + }; + + let result = get_current_user(auth, Extension(pool)).await; + assert!(result.is_ok()); + + let response = result.unwrap().0; + assert!(response.success); + assert!(response.data.is_some()); + assert_eq!(response.data.unwrap().email, user.email); + } + + #[tokio::test] + async fn test_check_auth_status() { + let auth = AuthUser { + user_id: Uuid::new_v4(), + email: "test@example.com".to_string(), + is_admin: true, + }; + + let response = check_auth_status(auth.clone()).await.0; + assert!(response.success); + assert!(response.data.is_some()); + + let data = response.data.unwrap(); + assert_eq!(data["authenticated"], true); + assert_eq!(data["user_id"], auth.user_id.to_string()); + assert_eq!(data["email"], auth.email); + assert_eq!(data["is_admin"], auth.is_admin); + } +} diff --git a/package.json b/package.json index 47515755..13d8e94f 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "backend:dev": "cargo watch -x 'run --manifest-path backend/Cargo.toml'", "backend:build": "cargo build --release --manifest-path backend/Cargo.toml", "backend:run": "cargo run --manifest-path backend/Cargo.toml", + "backend:test": "cargo test --lib", "generate-types": "cd backend && cargo run --bin generate_types" }, "devDependencies": {