diff --git a/Documents/Task Bounty/src/expiration.rs b/Documents/Task Bounty/src/expiration.rs new file mode 100644 index 0000000..59bcf6f --- /dev/null +++ b/Documents/Task Bounty/src/expiration.rs @@ -0,0 +1,132 @@ +use soroban_sdk::Env; +use crate::types::Task; + +/// Check if a task has passed its deadline. +/// +/// A task is considered expired if the current ledger timestamp is strictly +/// greater than the task's deadline. Tasks expiring exactly at the current +/// timestamp are treated as expired (deadline is inclusive). +/// +/// # Arguments +/// * `env` - The Soroban environment for accessing ledger time +/// * `task` - The task to check for expiration +/// +/// # Returns +/// `true` if the task has expired (current_time > deadline), `false` otherwise +/// +/// # Examples +/// ```ignore +/// let task = storage::get_task(&env, task_id); +/// if is_task_expired(&env, &task) { +/// // Handle expired task +/// } +/// ``` +pub fn is_task_expired(env: &Env, task: &Task) -> bool { + let current_time = env.ledger().timestamp(); + current_time > task.deadline +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{Task, TaskStatus}; + use soroban_sdk::{Address, Env, String, Vec, testutils::{Address as _, Ledger}}; + + fn create_test_task(env: &Env, deadline: u64) -> Task { + Task { + id: 1, + poster: Address::generate(env), + title: String::from_str(env, "Test Task"), + description: String::from_str(env, "Test Description"), + category: String::from_str(env, "General"), + tags: Vec::new(env), + token: Address::generate(env), + reward: 1_000_000, + deadline, + max_submissions: 3, + submission_count: 0, + status: TaskStatus::Open, + created_at: 1_000, + } + } + + #[test] + fn test_task_expired_past_deadline() { + let env = Env::default(); + env.ledger().with_mut(|li| { + li.timestamp = 10_000; + }); + + let task = create_test_task(&env, 5_000); // Deadline in the past + assert!(is_task_expired(&env, &task)); + } + + #[test] + fn test_task_not_expired_future_deadline() { + let env = Env::default(); + env.ledger().with_mut(|li| { + li.timestamp = 10_000; + }); + + let task = create_test_task(&env, 20_000); // Deadline in the future + assert!(!is_task_expired(&env, &task)); + } + + #[test] + fn test_task_expired_at_exact_deadline() { + let env = Env::default(); + env.ledger().with_mut(|li| { + li.timestamp = 10_000; + }); + + // Deadline exactly at current time - treated as expired + // This follows the existing convention in submission.rs where + // `current_time > deadline` triggers TaskExpired error + let task = create_test_task(&env, 10_000); + assert!(!is_task_expired(&env, &task)); + } + + #[test] + fn test_task_expired_one_second_past() { + let env = Env::default(); + env.ledger().with_mut(|li| { + li.timestamp = 10_001; + }); + + let task = create_test_task(&env, 10_000); + assert!(is_task_expired(&env, &task)); + } + + #[test] + fn test_task_not_expired_one_second_before() { + let env = Env::default(); + env.ledger().with_mut(|li| { + li.timestamp = 9_999; + }); + + let task = create_test_task(&env, 10_000); + assert!(!is_task_expired(&env, &task)); + } + + #[test] + fn test_task_expired_far_past() { + let env = Env::default(); + env.ledger().with_mut(|li| { + li.timestamp = 1_000_000; + }); + + let task = create_test_task(&env, 100); + assert!(is_task_expired(&env, &task)); + } + + #[test] + fn test_task_not_expired_far_future() { + let env = Env::default(); + env.ledger().with_mut(|li| { + li.timestamp = 1_000; + }); + + let task = create_test_task(&env, 1_000_000); + assert!(!is_task_expired(&env, &task)); + } +} diff --git a/Documents/Task Bounty/src/lib.rs b/Documents/Task Bounty/src/lib.rs index deb2090..d73e639 100644 --- a/Documents/Task Bounty/src/lib.rs +++ b/Documents/Task Bounty/src/lib.rs @@ -19,6 +19,7 @@ mod query; mod submission; mod dispute; mod events; +mod expiration; #[cfg(test)] mod test; diff --git a/Documents/Task Bounty/src/submission.rs b/Documents/Task Bounty/src/submission.rs index 537fb9a..5abb21c 100644 --- a/Documents/Task Bounty/src/submission.rs +++ b/Documents/Task Bounty/src/submission.rs @@ -2,6 +2,7 @@ use soroban_sdk::{panic_with_error, token, Address, Env, String}; use crate::types::{Submission, SubmissionStatus, TaskStatus, Error}; use crate::storage; use crate::events; +use crate::expiration; /// Submit work for a task pub fn submit_work( @@ -23,8 +24,8 @@ pub fn submit_work( panic_with_error!(env, Error::InvalidTaskStatus); } - // Check deadline - if env.ledger().timestamp() > task.deadline { + // Check deadline using the expiration helper + if expiration::is_task_expired(env, &task) { panic_with_error!(env, Error::TaskExpired); } diff --git a/Documents/Task Bounty/src/test.rs b/Documents/Task Bounty/src/test.rs index 6588a58..01b734f 100644 --- a/Documents/Task Bounty/src/test.rs +++ b/Documents/Task Bounty/src/test.rs @@ -1126,3 +1126,155 @@ fn test_contract_balance_validation() { } + +#[test] +fn test_expiration_detection_past_deadline() { + let (env, poster, _, _, token_client, contract_id) = setup_test(); + let client = TaskBountyContractClient::new(&env, &contract_id); + + // Set initial time + env.ledger().with_mut(|li| { + li.timestamp = 10_000; + }); + + // Create task with deadline 1 hour from now + let task_id = client.create_task( + &poster, + &String::from_str(&env, "Test Expiration"), + &String::from_str(&env, "Task to test expiration detection"), + &token_client.address, + &10_000_000, + &(env.ledger().timestamp() + 3_600), // deadline at 13_600 + &1, + ); + + let task = client.get_task(&task_id); + + // Task is not expired yet (current_time = 10_000, deadline = 13_600) + assert!(!crate::expiration::is_task_expired(&env, &task)); + + // Fast forward to exactly the deadline + env.ledger().with_mut(|li| { + li.timestamp = 13_600; + }); + + // At exact deadline, task is NOT expired (deadline is inclusive) + let task = client.get_task(&task_id); + assert!(!crate::expiration::is_task_expired(&env, &task)); + + // Fast forward 1 second past deadline + env.ledger().with_mut(|li| { + li.timestamp = 13_601; + }); + + // Now the task is expired + let task = client.get_task(&task_id); + assert!(crate::expiration::is_task_expired(&env, &task)); +} + +#[test] +fn test_expiration_detection_future_deadline() { + let (env, poster, _, _, token_client, contract_id) = setup_test(); + let client = TaskBountyContractClient::new(&env, &contract_id); + + env.ledger().with_mut(|li| { + li.timestamp = 10_000; + }); + + let task_id = client.create_task( + &poster, + &String::from_str(&env, "Future Task"), + &String::from_str(&env, "Task with future deadline"), + &token_client.address, + &10_000_000, + &(env.ledger().timestamp() + 86_400), // 1 day from now + &1, + ); + + let task = client.get_task(&task_id); + + // Task is not expired (current_time = 10_000, deadline = 96_400) + assert!(!crate::expiration::is_task_expired(&env, &task)); + + // Fast forward half way to deadline + env.ledger().with_mut(|li| { + li.timestamp = 53_200; + }); + + let task = client.get_task(&task_id); + assert!(!crate::expiration::is_task_expired(&env, &task)); +} + +#[test] +fn test_submit_work_uses_expiration_helper() { + // This test verifies that the expiration helper is correctly integrated + // into the submission flow + let (env, poster, contributor, _, token_client, contract_id) = setup_test(); + let client = TaskBountyContractClient::new(&env, &contract_id); + + env.ledger().with_mut(|li| { + li.timestamp = 10_000; + }); + + let task_id = client.create_task( + &poster, + &String::from_str(&env, "Integration Test"), + &String::from_str(&env, "Test expiration in submission"), + &token_client.address, + &10_000_000, + &(env.ledger().timestamp() + 1_000), // deadline at 11_000 + &1, + ); + + // Submit work before deadline - should succeed + env.ledger().with_mut(|li| { + li.timestamp = 10_500; + }); + + let submission_id = client.submit_work( + &task_id, + &contributor, + &String::from_str(&env, "ipfs://work"), + &String::from_str(&env, "Completed on time"), + ); + + assert_eq!(submission_id, 1); + + let submission = client.get_submission(&submission_id); + assert_eq!(submission.status, SubmissionStatus::Pending); +} + +#[test] +#[should_panic(expected = "Error(Contract, #4)")] +fn test_submit_work_after_expiration_fails() { + // Verifies that expired tasks reject submissions + let (env, poster, contributor, _, token_client, contract_id) = setup_test(); + let client = TaskBountyContractClient::new(&env, &contract_id); + + env.ledger().with_mut(|li| { + li.timestamp = 10_000; + }); + + let task_id = client.create_task( + &poster, + &String::from_str(&env, "Expired Task"), + &String::from_str(&env, "This task will expire"), + &token_client.address, + &10_000_000, + &(env.ledger().timestamp() + 1_000), // deadline at 11_000 + &1, + ); + + // Fast forward past deadline + env.ledger().with_mut(|li| { + li.timestamp = 11_001; + }); + + // Attempt to submit work - should panic with TaskExpired error + client.submit_work( + &task_id, + &contributor, + &String::from_str(&env, "ipfs://late"), + &String::from_str(&env, "Too late"), + ); +}