#![cfg(feature = "test-utils")]
use chrono::NaiveDate;
use things3_core::models::{
BulkCompleteRequest, BulkDeleteRequest, BulkMoveRequest, BulkUpdateDatesRequest,
};
use things3_core::test_utils::{create_test_database_and_connect, TaskRequestBuilder};
use things3_core::{ThingsError, ThingsId};
#[tokio::test]
async fn test_bulk_move_to_project() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let project_request = things3_core::models::CreateProjectRequest {
title: "Target Project".to_string(),
notes: None,
area_uuid: None,
start_date: None,
deadline: None,
tags: None,
};
let project_uuid = db.create_project(project_request).await.unwrap();
let mut task_uuids = Vec::new();
for i in 1..=3 {
let request = TaskRequestBuilder::new()
.title(format!("Task {}", i))
.build();
let task_uuid = db.create_task(request).await.unwrap();
task_uuids.push(task_uuid);
}
let bulk_request = BulkMoveRequest {
task_uuids: task_uuids.clone(),
project_uuid: Some(project_uuid.clone()),
area_uuid: None,
};
let result = db.bulk_move(bulk_request).await.unwrap();
assert!(result.success);
assert_eq!(result.processed_count, 3);
for uuid in &task_uuids {
let task = db.get_task_by_uuid(uuid).await.unwrap().unwrap();
assert_eq!(task.project_uuid, Some(project_uuid.clone()));
}
}
#[tokio::test]
async fn test_bulk_move_to_area() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let area_request = things3_core::models::CreateAreaRequest {
title: "Target Area".to_string(),
};
let area_uuid = db.create_area(area_request).await.unwrap();
let mut task_uuids = Vec::new();
for i in 1..=3 {
let request = TaskRequestBuilder::new()
.title(format!("Task {}", i))
.build();
let task_uuid = db.create_task(request).await.unwrap();
task_uuids.push(task_uuid);
}
let bulk_request = BulkMoveRequest {
task_uuids: task_uuids.clone(),
project_uuid: None,
area_uuid: Some(area_uuid.clone()),
};
let result = db.bulk_move(bulk_request).await.unwrap();
assert!(result.success);
assert_eq!(result.processed_count, 3);
for uuid in &task_uuids {
let task = db.get_task_by_uuid(uuid).await.unwrap().unwrap();
assert_eq!(task.area_uuid, Some(area_uuid.clone()));
}
}
#[tokio::test]
async fn test_bulk_move_empty_uuids() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let bulk_request = BulkMoveRequest {
task_uuids: vec![],
project_uuid: Some(ThingsId::new_v4()),
area_uuid: None,
};
let result = db.bulk_move(bulk_request).await;
assert!(result.is_err());
assert!(matches!(result, Err(ThingsError::Validation { .. })));
}
#[tokio::test]
async fn test_bulk_move_invalid_uuid() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let project_request = things3_core::models::CreateProjectRequest {
title: "Target Project".to_string(),
notes: None,
area_uuid: None,
start_date: None,
deadline: None,
tags: None,
};
let project_uuid = db.create_project(project_request).await.unwrap();
let request = TaskRequestBuilder::new().title("Valid Task").build();
let valid_uuid = db.create_task(request).await.unwrap();
let invalid_uuid = ThingsId::new_v4(); let bulk_request = BulkMoveRequest {
task_uuids: vec![valid_uuid.clone(), invalid_uuid],
project_uuid: Some(project_uuid),
area_uuid: None,
};
let result = db.bulk_move(bulk_request).await;
assert!(result.is_err());
assert!(matches!(result, Err(ThingsError::TaskNotFound { .. })));
let task = db.get_task_by_uuid(&valid_uuid).await.unwrap().unwrap();
assert_eq!(
task.project_uuid, None,
"Task should not be moved due to rollback"
);
}
#[tokio::test]
async fn test_bulk_move_nonexistent_project() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let request = TaskRequestBuilder::new().title("Test Task").build();
let task_uuid = db.create_task(request).await.unwrap();
let fake_project_uuid = ThingsId::new_v4();
let bulk_request = BulkMoveRequest {
task_uuids: vec![task_uuid],
project_uuid: Some(fake_project_uuid),
area_uuid: None,
};
let result = db.bulk_move(bulk_request).await;
assert!(result.is_err());
assert!(matches!(result, Err(ThingsError::ProjectNotFound { .. })));
}
#[tokio::test]
async fn test_bulk_update_dates_both() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let mut task_uuids = Vec::new();
for i in 1..=3 {
let request = TaskRequestBuilder::new()
.title(format!("Task {}", i))
.build();
let task_uuid = db.create_task(request).await.unwrap();
task_uuids.push(task_uuid);
}
let start_date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let deadline = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let bulk_request = BulkUpdateDatesRequest {
task_uuids: task_uuids.clone(),
start_date: Some(start_date),
deadline: Some(deadline),
clear_start_date: false,
clear_deadline: false,
};
let result = db.bulk_update_dates(bulk_request).await.unwrap();
assert!(result.success);
assert_eq!(result.processed_count, 3);
for uuid in &task_uuids {
let task = db.get_task_by_uuid(uuid).await.unwrap().unwrap();
assert_eq!(task.start_date, Some(start_date));
assert_eq!(task.deadline, Some(deadline));
}
}
#[tokio::test]
async fn test_bulk_update_dates_clear() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let start_date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let deadline = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let mut task_uuids = Vec::new();
for i in 1..=2 {
let request = TaskRequestBuilder::new()
.title(format!("Task {}", i))
.start_date(start_date)
.deadline(deadline)
.build();
let task_uuid = db.create_task(request).await.unwrap();
task_uuids.push(task_uuid);
}
let bulk_request = BulkUpdateDatesRequest {
task_uuids: task_uuids.clone(),
start_date: None,
deadline: None,
clear_start_date: true,
clear_deadline: true,
};
let result = db.bulk_update_dates(bulk_request).await.unwrap();
assert!(result.success);
assert_eq!(result.processed_count, 2);
for uuid in &task_uuids {
let task = db.get_task_by_uuid(uuid).await.unwrap().unwrap();
assert_eq!(task.start_date, None);
assert_eq!(task.deadline, None);
}
}
#[tokio::test]
async fn test_bulk_update_dates_invalid_range() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let request = TaskRequestBuilder::new().title("Test Task").build();
let task_uuid = db.create_task(request).await.unwrap();
let bulk_request = BulkUpdateDatesRequest {
task_uuids: vec![task_uuid],
start_date: Some(NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()),
deadline: Some(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()),
clear_start_date: false,
clear_deadline: false,
};
let result = db.bulk_update_dates(bulk_request).await;
assert!(result.is_err());
assert!(matches!(result, Err(ThingsError::DateValidation(_))));
}
#[tokio::test]
async fn test_bulk_update_dates_merge_validation() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let start_date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
let request = TaskRequestBuilder::new()
.title("Test Task")
.start_date(start_date)
.build();
let task_uuid = db.create_task(request).await.unwrap();
let bulk_request = BulkUpdateDatesRequest {
task_uuids: vec![task_uuid],
start_date: None, deadline: Some(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()), clear_start_date: false,
clear_deadline: false,
};
let result = db.bulk_update_dates(bulk_request).await;
assert!(result.is_err());
assert!(matches!(result, Err(ThingsError::DateValidation(_))));
}
#[tokio::test]
async fn test_bulk_complete_multiple() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let mut task_uuids = Vec::new();
for i in 1..=5 {
let request = TaskRequestBuilder::new()
.title(format!("Task {}", i))
.build();
let task_uuid = db.create_task(request).await.unwrap();
task_uuids.push(task_uuid);
}
let bulk_request = BulkCompleteRequest {
task_uuids: task_uuids.clone(),
};
let result = db.bulk_complete(bulk_request).await.unwrap();
assert!(result.success);
assert_eq!(result.processed_count, 5);
for uuid in &task_uuids {
let task = db.get_task_by_uuid(uuid).await.unwrap().unwrap();
assert_eq!(task.status, things3_core::models::TaskStatus::Completed);
assert!(task.stop_date.is_some());
}
}
#[tokio::test]
async fn test_bulk_complete_already_completed() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let request = TaskRequestBuilder::new().title("Task 1").build();
let task_uuid = db.create_task(request).await.unwrap();
db.complete_task(&task_uuid).await.unwrap();
let bulk_request = BulkCompleteRequest {
task_uuids: vec![task_uuid],
};
let result = db.bulk_complete(bulk_request).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_bulk_delete_soft_delete() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let mut task_uuids = Vec::new();
for i in 1..=3 {
let request = TaskRequestBuilder::new()
.title(format!("Task {}", i))
.build();
let task_uuid = db.create_task(request).await.unwrap();
task_uuids.push(task_uuid);
}
let bulk_request = BulkDeleteRequest {
task_uuids: task_uuids.clone(),
};
let result = db.bulk_delete(bulk_request).await.unwrap();
assert!(result.success);
assert_eq!(result.processed_count, 3);
for uuid in &task_uuids {
let task = db.get_task_by_uuid(uuid).await.unwrap();
assert!(task.is_none(), "Deleted task should not be returned");
}
}
#[tokio::test]
async fn test_bulk_delete_empty_uuids() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let bulk_request = BulkDeleteRequest { task_uuids: vec![] };
let result = db.bulk_delete(bulk_request).await;
assert!(result.is_err());
assert!(matches!(result, Err(ThingsError::Validation { .. })));
}
#[tokio::test]
async fn test_transaction_rollback_on_error() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let request1 = TaskRequestBuilder::new().title("Task 1").build();
let uuid1 = db.create_task(request1).await.unwrap();
let request2 = TaskRequestBuilder::new().title("Task 2").build();
let uuid2 = db.create_task(request2).await.unwrap();
let invalid_uuid = ThingsId::new_v4();
let bulk_request = BulkCompleteRequest {
task_uuids: vec![uuid1.clone(), uuid2.clone(), invalid_uuid],
};
let result = db.bulk_complete(bulk_request).await;
assert!(result.is_err());
let task1 = db.get_task_by_uuid(&uuid1).await.unwrap().unwrap();
let task2 = db.get_task_by_uuid(&uuid2).await.unwrap().unwrap();
assert_eq!(task1.status, things3_core::models::TaskStatus::Incomplete);
assert_eq!(task2.status, things3_core::models::TaskStatus::Incomplete);
}
#[tokio::test]
async fn test_bulk_operations_with_single_task() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let request = TaskRequestBuilder::new().title("Single Task").build();
let task_uuid = db.create_task(request).await.unwrap();
let bulk_request = BulkCompleteRequest {
task_uuids: vec![task_uuid],
};
let result = db.bulk_complete(bulk_request).await.unwrap();
assert!(result.success);
assert_eq!(result.processed_count, 1);
}
#[tokio::test]
async fn test_bulk_move_exceeds_max_batch_size() {
let (db, _temp_file) = create_test_database_and_connect().await.unwrap();
let mut task_uuids = Vec::new();
for _ in 0..1001 {
task_uuids.push(ThingsId::new_v4());
}
let bulk_request = BulkMoveRequest {
task_uuids,
project_uuid: Some(ThingsId::new_v4()),
area_uuid: None,
};
let result = db.bulk_move(bulk_request).await;
assert!(result.is_err());
assert!(matches!(result, Err(ThingsError::Validation { .. })));
if let Err(ThingsError::Validation { message }) = result {
assert!(message.contains("exceeds maximum"));
assert!(message.contains("1001"));
assert!(message.contains("1000"));
}
}