diff --git a/dongle-smartcontract/src/events.rs b/dongle-smartcontract/src/events.rs index b841304..5119837 100644 --- a/dongle-smartcontract/src/events.rs +++ b/dongle-smartcontract/src/events.rs @@ -174,8 +174,7 @@ pub fn publish_review_event( project_id: u64, reviewer: Address, action: ReviewAction, - ipfs_cid: Option, - comment_cid: Option, + content_cid: Option, owner_response: Option, created_at: u64, updated_at: u64, @@ -185,10 +184,9 @@ pub fn publish_review_event( reviewer: reviewer.clone(), action: action.clone(), timestamp: env.ledger().timestamp(), - ipfs_cid, + content_cid, created_at, updated_at, - comment_cid, owner_response, }; diff --git a/dongle-smartcontract/src/review_registry.rs b/dongle-smartcontract/src/review_registry.rs index 30936b3..e9beb86 100644 --- a/dongle-smartcontract/src/review_registry.rs +++ b/dongle-smartcontract/src/review_registry.rs @@ -60,8 +60,7 @@ impl ReviewRegistry { project_id, reviewer: reviewer.clone(), rating, - ipfs_cid: comment_cid.clone(), - comment_cid: comment_cid.clone(), + content_cid: comment_cid.clone(), owner_response: None, created_at: now, updated_at: now, @@ -128,7 +127,6 @@ impl ReviewRegistry { reviewer, ReviewAction::Submitted, comment_cid.clone(), - comment_cid, None, now, now, @@ -185,8 +183,7 @@ impl ReviewRegistry { let old_rating = review.rating; let now = env.ledger().timestamp(); review.rating = rating; - review.ipfs_cid = comment_cid.clone(); - review.comment_cid = comment_cid.clone(); + review.content_cid = comment_cid.clone(); review.updated_at = now; // Get current stats @@ -225,7 +222,6 @@ impl ReviewRegistry { reviewer, ReviewAction::Updated, comment_cid.clone(), - comment_cid, review.owner_response.clone(), review.created_at, now, @@ -332,7 +328,6 @@ impl ReviewRegistry { reviewer, ReviewAction::Deleted, None, - None, existing.owner_response.clone(), existing.created_at, now, @@ -392,8 +387,7 @@ impl ReviewRegistry { project_id, reviewer, ReviewAction::Updated, - review.ipfs_cid.clone(), - review.comment_cid.clone(), + review.content_cid.clone(), review.owner_response.clone(), review.created_at, now, @@ -412,13 +406,7 @@ impl ReviewRegistry { } pub fn get_review_cid(env: &Env, project_id: u64, reviewer: Address) -> Option { - Self::get_review(env, project_id, reviewer).and_then(|review| { - if let Some(cid) = review.ipfs_cid { - Some(cid) - } else { - review.comment_cid - } - }) + Self::get_review(env, project_id, reviewer).and_then(|review| review.content_cid) } pub fn get_project_review_cids(env: &Env, project_id: u64) -> Vec<(Address, String)> { diff --git a/dongle-smartcontract/src/tests/canonical_cid_tests.rs b/dongle-smartcontract/src/tests/canonical_cid_tests.rs new file mode 100644 index 0000000..699e6a2 --- /dev/null +++ b/dongle-smartcontract/src/tests/canonical_cid_tests.rs @@ -0,0 +1,389 @@ +//! Tests for canonical CID field consolidation - ensuring no data loss + +use crate::errors::ContractError; +use crate::tests::fixtures::{create_test_env, register_test_project}; +use crate::types::Review; +use crate::DongleContract; +use soroban_sdk::{testutils::Address as _, Address, Env, String}; + +// ── CID Data Consolidation Tests ── + +#[test] +fn test_add_review_uses_canonical_content_cid() { + let (env, _admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + let cid = String::from_str(&env, "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"); + + let result = DongleContract::add_review( + env.clone(), + project_id, + reviewer.clone(), + 5, + Some(cid.clone()), + ); + + assert!(result.is_ok()); + + // Retrieve the review + let review = DongleContract::get_review(env.clone(), project_id, reviewer.clone()) + .expect("Review should exist"); + + // Verify that content_cid is set (consolidated field) + assert_eq!(review.content_cid, Some(cid.clone())); + + // Verify old duplicate fields no longer exist + // This ensures we can't accidentally use stale fields + let review_struct: Review = review; // Verify structure + assert_eq!(review_struct.content_cid, Some(cid)); +} + +#[test] +fn test_add_review_without_cid_works() { + let (env, _admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + + let result = DongleContract::add_review(env.clone(), project_id, reviewer.clone(), 4, None); + + assert!(result.is_ok()); + + let review = DongleContract::get_review(env.clone(), project_id, reviewer.clone()) + .expect("Review should exist"); + + // Verify content_cid is None + assert_eq!(review.content_cid, None); +} + +#[test] +fn test_update_review_updates_canonical_cid() { + let (env, _admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + let original_cid = String::from_str(&env, "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"); + + // Add review + DongleContract::add_review( + env.clone(), + project_id, + reviewer.clone(), + 5, + Some(original_cid.clone()), + ) + .unwrap(); + + // Update with different CID + let new_cid = String::from_str(&env, "QmXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"); + + let result = DongleContract::update_review( + env.clone(), + project_id, + reviewer.clone(), + 4, + Some(new_cid.clone()), + ); + + assert!(result.is_ok()); + + let review = DongleContract::get_review(env.clone(), project_id, reviewer.clone()) + .expect("Review should exist"); + + // Verify content_cid is updated + assert_eq!(review.content_cid, Some(new_cid)); + assert_ne!(review.content_cid, Some(original_cid)); +} + +#[test] +fn test_review_cid_getter_returns_content_cid() { + let (env, _admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + let cid = String::from_str(&env, "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"); + + DongleContract::add_review( + env.clone(), + project_id, + reviewer.clone(), + 5, + Some(cid.clone()), + ) + .unwrap(); + + // Verify internal getter works + let retrieved_cid = crate::review_registry::ReviewRegistry::get_review_cid( + &env, + project_id, + reviewer.clone(), + ); + + assert_eq!(retrieved_cid, Some(cid)); +} + +#[test] +fn test_multiple_reviews_each_has_own_cid() { + let (env, _admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer1 = Address::generate(&env); + let reviewer2 = Address::generate(&env); + + let cid1 = String::from_str(&env, "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"); + let cid2 = String::from_str(&env, "QmXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"); + + // Add first review + DongleContract::add_review( + env.clone(), + project_id, + reviewer1.clone(), + 5, + Some(cid1.clone()), + ) + .unwrap(); + + // Add second review + DongleContract::add_review( + env.clone(), + project_id, + reviewer2.clone(), + 4, + Some(cid2.clone()), + ) + .unwrap(); + + // Verify each review has correct CID + let review1 = + DongleContract::get_review(env.clone(), project_id, reviewer1.clone()).unwrap(); + let review2 = + DongleContract::get_review(env.clone(), project_id, reviewer2.clone()).unwrap(); + + assert_eq!(review1.content_cid, Some(cid1)); + assert_eq!(review2.content_cid, Some(cid2)); +} + +// ── Data Preservation Tests ── + +#[test] +fn test_review_preserves_all_data_across_operations() { + let (env, _admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + let cid = String::from_str(&env, "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"); + + // Add review + DongleContract::add_review( + env.clone(), + project_id, + reviewer.clone(), + 5, + Some(cid.clone()), + ) + .unwrap(); + + let review_v1 = DongleContract::get_review(env.clone(), project_id, reviewer.clone()) + .expect("Review should exist"); + + let created_at = review_v1.created_at; + let original_rating = review_v1.rating; + + // Update review + let new_cid = String::from_str(&env, "QmABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCD"); + DongleContract::update_review( + env.clone(), + project_id, + reviewer.clone(), + 4, + Some(new_cid.clone()), + ) + .unwrap(); + + let review_v2 = DongleContract::get_review(env.clone(), project_id, reviewer.clone()) + .expect("Review should exist"); + + // Verify immutable fields are preserved + assert_eq!(review_v2.project_id, project_id); + assert_eq!(review_v2.reviewer, reviewer); + assert_eq!(review_v2.created_at, created_at); + + // Verify mutable fields are updated + assert_eq!(review_v2.rating, 4); + assert_ne!(review_v2.rating, original_rating); + assert_eq!(review_v2.content_cid, Some(new_cid.clone())); + assert_ne!(review_v2.content_cid, Some(cid)); + + // Verify updated_at changed + assert!(review_v2.updated_at >= review_v1.updated_at); +} + +#[test] +fn test_review_listing_preserves_cids() { + let (env, _admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer1 = Address::generate(&env); + let reviewer2 = Address::generate(&env); + let reviewer3 = Address::generate(&env); + + let cid1 = String::from_str(&env, "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"); + let cid2 = String::from_str(&env, "QmXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"); + let cid3 = String::from_str(&env, "QmABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCD"); + + // Add multiple reviews + DongleContract::add_review( + env.clone(), + project_id, + reviewer1.clone(), + 5, + Some(cid1.clone()), + ) + .unwrap(); + DongleContract::add_review( + env.clone(), + project_id, + reviewer2.clone(), + 4, + Some(cid2.clone()), + ) + .unwrap(); + DongleContract::add_review( + env.clone(), + project_id, + reviewer3.clone(), + 3, + Some(cid3.clone()), + ) + .unwrap(); + + // List reviews + let reviews = DongleContract::list_reviews(env.clone(), project_id, 0, 10); + + assert_eq!(reviews.len(), 3); + + // Verify all CIDs are preserved in list + let mut found_cids = vec![]; + for review in reviews.iter() { + if let Some(cid) = review.content_cid.clone() { + found_cids.push(cid); + } + } + + assert!(found_cids.contains(&cid1)); + assert!(found_cids.contains(&cid2)); + assert!(found_cids.contains(&cid3)); +} + +// ── Migration Path Tests ── + +#[test] +fn test_get_review_still_works_after_consolidation() { + let (env, _admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + let cid = String::from_str(&env, "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"); + + // Add review using new consolidated flow + DongleContract::add_review( + env.clone(), + project_id, + reviewer.clone(), + 5, + Some(cid.clone()), + ) + .unwrap(); + + // Get review using public API (migration path) + let review = DongleContract::get_review(env.clone(), project_id, reviewer.clone()) + .expect("Get review should still work"); + + // Public API works seamlessly with new structure + assert_eq!(review.rating, 5); + assert_eq!(review.content_cid, Some(cid)); +} + +#[test] +fn test_delete_review_preserves_cid_until_deletion() { + let (env, _admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + let cid = String::from_str(&env, "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"); + + // Add review + DongleContract::add_review( + env.clone(), + project_id, + reviewer.clone(), + 5, + Some(cid.clone()), + ) + .unwrap(); + + // Verify review exists with CID + let review_before = DongleContract::get_review(env.clone(), project_id, reviewer.clone()) + .expect("Review should exist"); + assert_eq!(review_before.content_cid, Some(cid.clone())); + + // Delete review + let delete_result = DongleContract::delete_review(env.clone(), project_id, reviewer.clone()); + assert!(delete_result.is_ok()); + + // Verify review no longer exists + let review_after = DongleContract::get_review(env.clone(), project_id, reviewer.clone()); + assert!(review_after.is_none()); +} + +// ── Edge Case Tests ── + +#[test] +fn test_empty_cid_string_handled_correctly() { + let (env, _admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + + // Attempt to add review with None CID + let result = DongleContract::add_review( + env.clone(), + project_id, + reviewer.clone(), + 5, + None, + ); + + assert!(result.is_ok()); + + let review = DongleContract::get_review(env.clone(), project_id, reviewer.clone()).unwrap(); + assert_eq!(review.content_cid, None); +} + +#[test] +fn test_cid_can_be_cleared_on_update() { + let (env, _admin, owner) = create_test_env(); + let project_id = register_test_project(&env, &owner); + + let reviewer = Address::generate(&env); + let cid = String::from_str(&env, "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"); + + // Add review with CID + DongleContract::add_review( + env.clone(), + project_id, + reviewer.clone(), + 5, + Some(cid.clone()), + ) + .unwrap(); + + // Update review to remove CID + let result = DongleContract::update_review(env.clone(), project_id, reviewer.clone(), 4, None); + + assert!(result.is_ok()); + + let review = DongleContract::get_review(env.clone(), project_id, reviewer.clone()).unwrap(); + assert_eq!(review.content_cid, None); +} diff --git a/dongle-smartcontract/src/types.rs b/dongle-smartcontract/src/types.rs index 8f1e1ed..74243b5 100644 --- a/dongle-smartcontract/src/types.rs +++ b/dongle-smartcontract/src/types.rs @@ -45,8 +45,8 @@ pub struct Review { pub project_id: u64, pub reviewer: Address, pub rating: u32, - pub ipfs_cid: Option, - pub comment_cid: Option, + /// Canonical content CID - replaces the redundant ipfs_cid/comment_cid pair + pub content_cid: Option, pub owner_response: Option, /// Unix timestamp (seconds) when the review was first submitted. @@ -78,8 +78,8 @@ pub struct ReviewEventData { pub reviewer: Address, pub action: ReviewAction, pub timestamp: u64, - pub ipfs_cid: Option, - pub comment_cid: Option, + /// Canonical content CID - consolidates the review content + pub content_cid: Option, pub owner_response: Option, pub created_at: u64, pub updated_at: u64,