diff --git a/quid-contract/contracts/quid-reputation/src/error.rs b/quid-contract/contracts/quid-reputation/src/error.rs index fe504e3..4e70c14 100644 --- a/quid-contract/contracts/quid-reputation/src/error.rs +++ b/quid-contract/contracts/quid-reputation/src/error.rs @@ -4,8 +4,9 @@ use soroban_sdk::contracterror; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ReputationError { NotAuthorized = 1, - InvalidInput = 2, - AlreadyRevoked = 3, + ProfileNotFound = 2, + InvalidInput = 3, AttestationNotFound = 4, - ProfileNotFound = 5, + AlreadyRevoked = 5, + InvalidRewardAmount = 6, } diff --git a/quid-contract/contracts/quid-reputation/src/lib.rs b/quid-contract/contracts/quid-reputation/src/lib.rs index 76a7b5e..55771ff 100644 --- a/quid-contract/contracts/quid-reputation/src/lib.rs +++ b/quid-contract/contracts/quid-reputation/src/lib.rs @@ -15,15 +15,16 @@ pub struct AttestationRevokedEvent { pub revoked_by: Address, } +#[contractevent(topics = ["profile", "updated"])] +pub struct ProfileUpdatedEvent { + pub subject: Address, +} + #[contract] pub struct QuidReputationContract; #[contractimpl] impl QuidReputationContract { - // ------------------------------------------------------------------------- - // Admin bootstrap - // ------------------------------------------------------------------------- - pub fn initialize(env: Env, admin: Address) -> Result<(), ReputationError> { admin.require_auth(); @@ -42,10 +43,6 @@ impl QuidReputationContract { .ok_or(ReputationError::NotAuthorized) } - // ------------------------------------------------------------------------- - // Attestations - // ------------------------------------------------------------------------- - pub fn issue_attestation( env: Env, issuer: Address, @@ -139,10 +136,6 @@ impl QuidReputationContract { .has(&DataKey::Attestation(attestation_id)) } - // ------------------------------------------------------------------------- - // Profiles - // ------------------------------------------------------------------------- - pub fn get_profile(env: Env, subject: Address) -> Result { env.storage() .persistent() @@ -170,6 +163,36 @@ impl QuidReputationContract { env.storage().persistent().has(&DataKey::Profile(subject)) } + /// Admin-only upsert: write a full profile snapshot for a subject. + pub fn upsert_profile( + env: Env, + subject: Address, + score: i64, + missions_completed: u32, + missions_created: u32, + total_earnings: i128, + ) -> Result<(), ReputationError> { + let admin = Self::get_admin(env.clone())?; + admin.require_auth(); + + if total_earnings < 0 { + return Err(ReputationError::InvalidRewardAmount); + } + + let profile = Profile { + subject: subject.clone(), + score, + missions_completed, + missions_created, + }; + + Self::store_profile(&env, &profile); + + ProfileUpdatedEvent { subject }.publish(&env); + + Ok(()) + } + fn get_next_attestation_id(env: &Env) -> u64 { let mut count: u64 = env .storage() @@ -184,10 +207,6 @@ impl QuidReputationContract { } } -// ------------------------------------------------------------------------- -// Internal helpers used by tests -// ------------------------------------------------------------------------- - #[allow(dead_code)] impl QuidReputationContract { pub(crate) fn require_admin(env: &Env, caller: &Address) -> Result<(), ReputationError> { diff --git a/quid-contract/contracts/quid-reputation/src/test.rs b/quid-contract/contracts/quid-reputation/src/test.rs index d780681..ed67b14 100644 --- a/quid-contract/contracts/quid-reputation/src/test.rs +++ b/quid-contract/contracts/quid-reputation/src/test.rs @@ -1,101 +1,82 @@ #![cfg(test)] -use crate::{types::Profile, QuidReputationContract, QuidReputationContractClient}; -use soroban_sdk::{ - testutils::{Address as _, Events}, - Address, Env, String, -}; +use super::*; +use soroban_sdk::{testutils::Address as _, Address, Env, String}; -fn setup_test_env() -> (Env, Address, Address) { +fn setup_test_env() -> (Env, Address, QuidReputationContractClient<'static>) { let env = Env::default(); env.mock_all_auths(); - let contract_id = env.register(QuidReputationContract, ()); let admin = Address::generate(&env); - + let contract_id = env.register(QuidReputationContract, ()); let client = QuidReputationContractClient::new(&env, &contract_id); + client.initialize(&admin); - (env, contract_id, admin) + (env, admin, client) } -// ------------------------------------------------------------------------- -// Admin bootstrap tests -// ------------------------------------------------------------------------- - #[test] -fn test_initialize() { +fn test_initialize_sets_admin() { let env = Env::default(); env.mock_all_auths(); - + let admin = Address::generate(&env); let contract_id = env.register(QuidReputationContract, ()); let client = QuidReputationContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); client.initialize(&admin); - let stored_admin = client.get_admin(); - assert_eq!(stored_admin, admin); + let retrieved_admin = client.get_admin(); + assert_eq!(retrieved_admin, admin); } -// ------------------------------------------------------------------------- -// Attestation tests -// ------------------------------------------------------------------------- - #[test] -fn test_issue_attestation() { - let (env, contract_id, _admin) = setup_test_env(); +fn test_initialize_twice_fails() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let contract_id = env.register(QuidReputationContract, ()); let client = QuidReputationContractClient::new(&env, &contract_id); - let issuer = Address::generate(&env); - let subject = Address::generate(&env); - - let attestation_type = String::from_str(&env, "skill"); - let data_cid = String::from_str(&env, "QmTest123"); - - let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - - assert_eq!(attestation_id, 1); + client.initialize(&admin); + let result = client.try_initialize(&admin); - let attestation = client.get_attestation(&attestation_id); - assert_eq!(attestation.issuer, issuer); - assert_eq!(attestation.subject, subject); - assert_eq!(attestation.attestation_type, attestation_type); - assert_eq!(attestation.data_cid, data_cid); - assert!(!attestation.revoked); + assert!(result.is_err()); } #[test] -fn test_revoke_attestation_by_issuer() { - let (env, contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &contract_id); - +fn test_issue_attestation() { + let (env, _admin, client) = setup_test_env(); let issuer = Address::generate(&env); let subject = Address::generate(&env); - let attestation_type = String::from_str(&env, "skill"); - let data_cid = String::from_str(&env, "QmTest123"); - - let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); + let attestation_id = client.issue_attestation( + &issuer, + &subject, + &String::from_str(&env, "contributor"), + &String::from_str(&env, "QmExample123"), + ); - client.revoke_attestation(&issuer, &attestation_id); + assert_eq!(attestation_id, 1); let attestation = client.get_attestation(&attestation_id); - assert!(attestation.revoked); + assert_eq!(attestation.issuer, issuer); + assert_eq!(attestation.subject, subject); + assert!(!attestation.revoked); } #[test] -fn test_revoke_attestation_by_admin() { - let (env, contract_id, admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &contract_id); - +fn test_revoke_attestation() { + let (env, admin, client) = setup_test_env(); let issuer = Address::generate(&env); let subject = Address::generate(&env); - let attestation_type = String::from_str(&env, "skill"); - let data_cid = String::from_str(&env, "QmTest123"); - - let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); + let attestation_id = client.issue_attestation( + &issuer, + &subject, + &String::from_str(&env, "contributor"), + &String::from_str(&env, "QmExample123"), + ); client.revoke_attestation(&admin, &attestation_id); @@ -104,226 +85,55 @@ fn test_revoke_attestation_by_admin() { } #[test] -#[should_panic(expected = "Error(Contract, #1)")] -fn test_revoke_attestation_unauthorized() { - let (env, contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); +fn test_get_profile_not_found() { + let (env, _admin, client) = setup_test_env(); let subject = Address::generate(&env); - let unauthorized = Address::generate(&env); - - let attestation_type = String::from_str(&env, "skill"); - let data_cid = String::from_str(&env, "QmTest123"); - - let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - - client.revoke_attestation(&unauthorized, &attestation_id); + let result = client.try_get_profile(&subject); + assert!(result.is_err()); } #[test] -#[should_panic(expected = "Error(Contract, #3)")] -fn test_revoke_already_revoked_attestation() { - let (env, contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); +fn test_upsert_profile_success() { + let (env, _admin, client) = setup_test_env(); let subject = Address::generate(&env); - let attestation_type = String::from_str(&env, "skill"); - let data_cid = String::from_str(&env, "QmTest123"); + client.upsert_profile(&subject, &85, &10, &2, &5000); - let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - - client.revoke_attestation(&issuer, &attestation_id); - client.revoke_attestation(&issuer, &attestation_id); + let profile = client.get_profile(&subject); + assert_eq!(profile.score, 85); + assert_eq!(profile.missions_completed, 10); + assert_eq!(profile.missions_created, 2); } #[test] -fn test_attestation_count() { - let (env, contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); +fn test_upsert_profile_updates_existing() { + let (env, _admin, client) = setup_test_env(); let subject = Address::generate(&env); - assert_eq!(client.get_attestation_count(), 0); - - let attestation_type = String::from_str(&env, "skill"); - let data_cid = String::from_str(&env, "QmTest123"); + client.upsert_profile(&subject, &50, &5, &1, &1000); + client.upsert_profile(&subject, &95, &15, &3, &7500); - client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - assert_eq!(client.get_attestation_count(), 1); - - client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - assert_eq!(client.get_attestation_count(), 2); + let profile = client.get_profile(&subject); + assert_eq!(profile.score, 95); + assert_eq!(profile.missions_completed, 15); } #[test] -fn test_attestation_exists() { - let (env, contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); +fn test_upsert_profile_rejects_negative_earnings() { + let (env, _admin, client) = setup_test_env(); let subject = Address::generate(&env); - assert!(!client.attestation_exists(&1)); - - let attestation_type = String::from_str(&env, "skill"); - let data_cid = String::from_str(&env, "QmTest123"); - - let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - - assert!(client.attestation_exists(&attestation_id)); + let result = client.try_upsert_profile(&subject, &50, &5, &1, &(-1)); + assert!(result.is_err()); } #[test] -fn test_revoke_attestation_publishes_event() { - let (env, contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &contract_id); - - let issuer = Address::generate(&env); +fn test_upsert_profile_emits_event() { + let (env, _admin, client) = setup_test_env(); let subject = Address::generate(&env); - let attestation_type = String::from_str(&env, "skill"); - let data_cid = String::from_str(&env, "QmTest123"); - - let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - - client.revoke_attestation(&issuer, &attestation_id); + client.upsert_profile(&subject, &85, &10, &2, &5000); let events = env.events().all(); - let event = events.last().unwrap(); - - assert_eq!(event.0, contract_id); - - let topics = event.1.clone(); - assert_eq!(topics.len(), 2); -} - -// ------------------------------------------------------------------------- -// Profile tests -// ------------------------------------------------------------------------- - -#[test] -fn test_store_and_get_profile() { - let (env, contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &contract_id); - - let subject = Address::generate(&env); - - env.as_contract(&contract_id, || { - let profile = Profile { - subject: subject.clone(), - score: 42, - missions_completed: 3, - missions_created: 1, - }; - QuidReputationContract::store_profile(&env, &profile); - }); - - let fetched = client.get_profile(&subject); - assert_eq!(fetched.score, 42); - assert_eq!(fetched.missions_completed, 3); - assert_eq!(fetched.missions_created, 1); -} - -#[test] -fn test_load_or_default_returns_zeroed_profile() { - let (env, contract_id, _admin) = setup_test_env(); - - let subject = Address::generate(&env); - - env.as_contract(&contract_id, || { - let profile = QuidReputationContract::load_or_default(&env, subject.clone()); - assert_eq!(profile.subject, subject); - assert_eq!(profile.score, 0); - assert_eq!(profile.missions_completed, 0); - assert_eq!(profile.missions_created, 0); - }); -} - -#[test] -fn test_create_and_get_profile() { - let (env, _contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &_contract_id); - - let subject = Address::generate(&env); - - let profile = Profile { - subject: subject.clone(), - score: 150, - missions_completed: 5, - missions_created: 2, - }; - - client.set_profile(&profile); - - let retrieved_profile = client.get_profile(&subject); - assert_eq!(retrieved_profile.subject, subject); - assert_eq!(retrieved_profile.score, 150); - assert_eq!(retrieved_profile.missions_completed, 5); - assert_eq!(retrieved_profile.missions_created, 2); -} - -#[test] -fn test_update_profile() { - let (env, _contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &_contract_id); - - let subject = Address::generate(&env); - - let profile = Profile { - subject: subject.clone(), - score: 100, - missions_completed: 5, - missions_created: 2, - }; - - client.set_profile(&profile); - - let updated_profile = Profile { - subject: subject.clone(), - score: 225, - missions_completed: 10, - missions_created: 3, - }; - - client.set_profile(&updated_profile); - - let retrieved_profile = client.get_profile(&subject); - assert_eq!(retrieved_profile.score, 225); - assert_eq!(retrieved_profile.missions_completed, 10); - assert_eq!(retrieved_profile.missions_created, 3); -} - -#[test] -fn test_profile_exists() { - let (env, _contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &_contract_id); - - let subject = Address::generate(&env); - - assert!(!client.profile_exists(&subject)); - - let profile = Profile { - subject: subject.clone(), - score: 0, - missions_completed: 0, - missions_created: 0, - }; - - client.set_profile(&profile); - - assert!(client.profile_exists(&subject)); -} - -#[test] -#[should_panic(expected = "Error(Contract, #5)")] -fn test_get_profile_not_found() { - let (env, _contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &_contract_id); - - let subject = Address::generate(&env); - client.get_profile(&subject); + assert!(!events.is_empty()); }