Add tests

This commit is contained in:
Louis Knight-Webb
2025-06-15 17:02:56 -04:00
parent 839f5faba2
commit deff546493
2 changed files with 672 additions and 0 deletions

View File

@@ -177,3 +177,325 @@ pub fn projects_router() -> Router {
.route("/projects", get(get_projects).post(create_project))
.route("/projects/:id", get(get_project).put(update_project).delete(delete_project))
}
#[cfg(test)]
mod tests {
use super::*;
use axum::extract::Extension;
use sqlx::PgPool;
use uuid::Uuid;
use chrono::Utc;
use crate::models::{user::User, project::{CreateProject, UpdateProject}};
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()
}
async fn create_test_project(pool: &PgPool, name: &str, owner_id: Uuid) -> Project {
let id = Uuid::new_v4();
let now = Utc::now();
sqlx::query_as!(
Project,
"INSERT INTO projects (id, name, owner_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5) RETURNING id, name, owner_id, created_at, updated_at",
id,
name,
owner_id,
now,
now
)
.fetch_one(pool)
.await
.unwrap()
}
#[sqlx::test]
async fn test_get_projects_success(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
// Create multiple projects
create_test_project(&pool, "Project 1", user.id).await;
create_test_project(&pool, "Project 2", user.id).await;
create_test_project(&pool, "Project 3", user.id).await;
let auth = AuthUser {
user_id: user.id,
email: user.email,
is_admin: false,
};
let result = get_projects(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_projects_empty(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let auth = AuthUser {
user_id: user.id,
email: user.email,
is_admin: false,
};
let result = get_projects(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(), 0);
}
#[sqlx::test]
async fn test_get_project_success(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let auth = AuthUser {
user_id: user.id,
email: user.email,
is_admin: false,
};
let result = get_project(auth, Path(project.id), Extension(pool)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let returned_project = response.data.unwrap();
assert_eq!(returned_project.id, project.id);
assert_eq!(returned_project.name, project.name);
assert_eq!(returned_project.owner_id, project.owner_id);
}
#[sqlx::test]
async fn test_get_project_not_found(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let nonexistent_project_id = Uuid::new_v4();
let auth = AuthUser {
user_id: user.id,
email: user.email,
is_admin: false,
};
let result = get_project(auth, Path(nonexistent_project_id), Extension(pool)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_create_project_success(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 create_request = CreateProject {
name: "New Project".to_string(),
};
let result = create_project(auth.clone(), Extension(pool), Json(create_request)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let created_project = response.data.unwrap();
assert_eq!(created_project.name, "New Project");
assert_eq!(created_project.owner_id, auth.user_id);
assert_eq!(response.message.unwrap(), "Project created successfully");
}
#[sqlx::test]
async fn test_create_project_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.clone(),
is_admin: true,
};
let create_request = CreateProject {
name: "Admin Project".to_string(),
};
let result = create_project(auth.clone(), Extension(pool), Json(create_request)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let created_project = response.data.unwrap();
assert_eq!(created_project.name, "Admin Project");
assert_eq!(created_project.owner_id, auth.user_id);
}
#[sqlx::test]
async fn test_update_project_success(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Original Name", user.id).await;
let update_request = UpdateProject {
name: Some("Updated Name".to_string()),
};
let result = update_project(Path(project.id), Extension(pool), Json(update_request)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let updated_project = response.data.unwrap();
assert_eq!(updated_project.name, "Updated Name");
assert_eq!(updated_project.owner_id, project.owner_id);
assert_eq!(response.message.unwrap(), "Project updated successfully");
}
#[sqlx::test]
async fn test_update_project_partial(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Original Name", user.id).await;
// Update with no changes (None for name should keep existing name)
let update_request = UpdateProject {
name: None,
};
let result = update_project(Path(project.id), Extension(pool), Json(update_request)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let updated_project = response.data.unwrap();
assert_eq!(updated_project.name, "Original Name"); // Should remain unchanged
assert_eq!(updated_project.owner_id, project.owner_id);
}
#[sqlx::test]
async fn test_update_project_not_found(pool: PgPool) {
let nonexistent_project_id = Uuid::new_v4();
let update_request = UpdateProject {
name: Some("Updated Name".to_string()),
};
let result = update_project(Path(nonexistent_project_id), Extension(pool), Json(update_request)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_delete_project_success(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Project to Delete", user.id).await;
let result = delete_project(Path(project.id), Extension(pool)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert_eq!(response.message.unwrap(), "Project deleted successfully");
}
#[sqlx::test]
async fn test_delete_project_not_found(pool: PgPool) {
let nonexistent_project_id = Uuid::new_v4();
let result = delete_project(Path(nonexistent_project_id), Extension(pool)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_delete_project_cascades_to_tasks(pool: PgPool) {
use crate::models::task::{Task, TaskStatus};
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Project with Tasks", user.id).await;
// Create a task in the project
let task_id = Uuid::new_v4();
let now = Utc::now();
sqlx::query!(
"INSERT INTO tasks (id, project_id, title, description, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7)",
task_id,
project.id,
"Test Task",
Some("Test Description"),
TaskStatus::Todo as TaskStatus,
now,
now
)
.execute(&pool)
.await
.unwrap();
// Verify task exists
let task_count_before = sqlx::query!(
"SELECT COUNT(*) as count FROM tasks WHERE project_id = $1",
project.id
)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(task_count_before.count.unwrap(), 1);
// Delete the project
let result = delete_project(Path(project.id), Extension(pool.clone())).await;
assert!(result.is_ok());
// Verify tasks were cascaded (deleted)
let task_count_after = sqlx::query!(
"SELECT COUNT(*) as count FROM tasks WHERE project_id = $1",
project.id
)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(task_count_after.count.unwrap(), 0);
}
#[sqlx::test]
async fn test_projects_belong_to_users(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 project1 = create_test_project(&pool, "User 1 Project", user1.id).await;
let project2 = create_test_project(&pool, "User 2 Project", user2.id).await;
// Verify project ownership
assert_eq!(project1.owner_id, user1.id);
assert_eq!(project2.owner_id, user2.id);
assert_ne!(project1.owner_id, project2.owner_id);
}
}

View File

@@ -224,3 +224,353 @@ pub fn tasks_router() -> Router {
.route("/projects/:project_id/tasks", get(get_project_tasks).post(create_task))
.route("/projects/:project_id/tasks/:task_id", get(get_task).put(update_task).delete(delete_task))
}
#[cfg(test)]
mod tests {
use super::*;
use axum::extract::Extension;
use sqlx::PgPool;
use uuid::Uuid;
use chrono::Utc;
use crate::models::{user::User, project::Project, task::{CreateTask, UpdateTask, TaskStatus}};
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()
}
async fn create_test_project(pool: &PgPool, name: &str, owner_id: Uuid) -> Project {
let id = Uuid::new_v4();
let now = Utc::now();
sqlx::query_as!(
Project,
"INSERT INTO projects (id, name, owner_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5) RETURNING id, name, owner_id, created_at, updated_at",
id,
name,
owner_id,
now,
now
)
.fetch_one(pool)
.await
.unwrap()
}
async fn create_test_task(pool: &PgPool, project_id: Uuid, title: &str, description: Option<String>, status: TaskStatus) -> Task {
let id = Uuid::new_v4();
let now = Utc::now();
sqlx::query_as!(
Task,
r#"INSERT INTO tasks (id, project_id, title, description, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, project_id, title, description, status as "status!: TaskStatus", created_at, updated_at"#,
id,
project_id,
title,
description,
status as TaskStatus,
now,
now
)
.fetch_one(pool)
.await
.unwrap()
}
#[sqlx::test]
async fn test_get_project_tasks_success(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
// Create multiple tasks
create_test_task(&pool, project.id, "Task 1", Some("Description 1".to_string()), TaskStatus::Todo).await;
create_test_task(&pool, project.id, "Task 2", None, TaskStatus::InProgress).await;
create_test_task(&pool, project.id, "Task 3", Some("Description 3".to_string()), TaskStatus::Done).await;
let auth = AuthUser {
user_id: user.id,
email: user.email,
is_admin: false,
};
let result = get_project_tasks(auth, Path(project.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().len(), 3);
}
#[sqlx::test]
async fn test_get_project_tasks_empty_project(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Empty Project", user.id).await;
let auth = AuthUser {
user_id: user.id,
email: user.email,
is_admin: false,
};
let result = get_project_tasks(auth, Path(project.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().len(), 0);
}
#[sqlx::test]
async fn test_get_task_success(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let task = create_test_task(&pool, project.id, "Test Task", Some("Test Description".to_string()), TaskStatus::Todo).await;
let auth = AuthUser {
user_id: user.id,
email: user.email,
is_admin: false,
};
let result = get_task(auth, Path((project.id, task.id)), Extension(pool)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let returned_task = response.data.unwrap();
assert_eq!(returned_task.id, task.id);
assert_eq!(returned_task.title, task.title);
assert_eq!(returned_task.description, task.description);
assert_eq!(returned_task.status, task.status);
}
#[sqlx::test]
async fn test_get_task_not_found(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let nonexistent_task_id = Uuid::new_v4();
let auth = AuthUser {
user_id: user.id,
email: user.email,
is_admin: false,
};
let result = get_task(auth, Path((project.id, nonexistent_task_id)), Extension(pool)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_get_task_wrong_project(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project1 = create_test_project(&pool, "Project 1", user.id).await;
let project2 = create_test_project(&pool, "Project 2", user.id).await;
let task = create_test_task(&pool, project1.id, "Test Task", None, TaskStatus::Todo).await;
let auth = AuthUser {
user_id: user.id,
email: user.email,
is_admin: false,
};
// Try to get task from wrong project
let result = get_task(auth, Path((project2.id, task.id)), Extension(pool)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_create_task_success(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let auth = AuthUser {
user_id: user.id,
email: user.email,
is_admin: false,
};
let create_request = CreateTask {
project_id: project.id, // This will be overridden by the path parameter
title: "New Task".to_string(),
description: Some("Task description".to_string()),
};
let result = create_task(Path(project.id), auth, Extension(pool), Json(create_request)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let created_task = response.data.unwrap();
assert_eq!(created_task.title, "New Task");
assert_eq!(created_task.description, Some("Task description".to_string()));
assert_eq!(created_task.status, TaskStatus::Todo);
assert_eq!(created_task.project_id, project.id);
}
#[sqlx::test]
async fn test_create_task_project_not_found(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let nonexistent_project_id = Uuid::new_v4();
let auth = AuthUser {
user_id: user.id,
email: user.email,
is_admin: false,
};
let create_request = CreateTask {
project_id: nonexistent_project_id,
title: "New Task".to_string(),
description: None,
};
let result = create_task(Path(nonexistent_project_id), auth, Extension(pool), Json(create_request)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_update_task_success(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let task = create_test_task(&pool, project.id, "Original Title", Some("Original Description".to_string()), TaskStatus::Todo).await;
let update_request = UpdateTask {
title: Some("Updated Title".to_string()),
description: Some("Updated Description".to_string()),
status: Some(TaskStatus::InProgress),
};
let result = update_task(Path((project.id, task.id)), Extension(pool), Json(update_request)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let updated_task = response.data.unwrap();
assert_eq!(updated_task.title, "Updated Title");
assert_eq!(updated_task.description, Some("Updated Description".to_string()));
assert_eq!(updated_task.status, TaskStatus::InProgress);
}
#[sqlx::test]
async fn test_update_task_partial(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let task = create_test_task(&pool, project.id, "Original Title", Some("Original Description".to_string()), TaskStatus::Todo).await;
// Only update status
let update_request = UpdateTask {
title: None,
description: None,
status: Some(TaskStatus::Done),
};
let result = update_task(Path((project.id, task.id)), Extension(pool), Json(update_request)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert!(response.data.is_some());
let updated_task = response.data.unwrap();
assert_eq!(updated_task.title, "Original Title"); // Should remain unchanged
assert_eq!(updated_task.description, Some("Original Description".to_string())); // Should remain unchanged
assert_eq!(updated_task.status, TaskStatus::Done); // Should be updated
}
#[sqlx::test]
async fn test_update_task_not_found(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let nonexistent_task_id = Uuid::new_v4();
let update_request = UpdateTask {
title: Some("Updated Title".to_string()),
description: None,
status: None,
};
let result = update_task(Path((project.id, nonexistent_task_id)), Extension(pool), Json(update_request)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_update_task_wrong_project(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project1 = create_test_project(&pool, "Project 1", user.id).await;
let project2 = create_test_project(&pool, "Project 2", user.id).await;
let task = create_test_task(&pool, project1.id, "Test Task", None, TaskStatus::Todo).await;
let update_request = UpdateTask {
title: Some("Updated Title".to_string()),
description: None,
status: None,
};
// Try to update task in wrong project
let result = update_task(Path((project2.id, task.id)), Extension(pool), Json(update_request)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_delete_task_success(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let task = create_test_task(&pool, project.id, "Task to Delete", None, TaskStatus::Todo).await;
let result = delete_task(Path((project.id, task.id)), Extension(pool)).await;
assert!(result.is_ok());
let response = result.unwrap().0;
assert!(response.success);
assert_eq!(response.message.unwrap(), "Task deleted successfully");
}
#[sqlx::test]
async fn test_delete_task_not_found(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project = create_test_project(&pool, "Test Project", user.id).await;
let nonexistent_task_id = Uuid::new_v4();
let result = delete_task(Path((project.id, nonexistent_task_id)), Extension(pool)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_delete_task_wrong_project(pool: PgPool) {
let user = create_test_user(&pool, "test@example.com", "password123", false).await;
let project1 = create_test_project(&pool, "Project 1", user.id).await;
let project2 = create_test_project(&pool, "Project 2", user.id).await;
let task = create_test_task(&pool, project1.id, "Task to Delete", None, TaskStatus::Todo).await;
// Try to delete task from wrong project
let result = delete_task(Path((project2.id, task.id)), Extension(pool)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err(), StatusCode::NOT_FOUND);
}
}