diff --git a/contracts/document/src/lib.rs b/contracts/document/src/lib.rs
index 81f7ed5d..88b1816f 100644
--- a/contracts/document/src/lib.rs
+++ b/contracts/document/src/lib.rs
@@ -23,6 +23,8 @@ pub enum DocumentError {
Unauthorized = 4,
AlreadyVerified = 5,
HashMismatch = 6,
+ AlreadyRevoked = 7,
+ DuplicateHash = 8,
}
// ── Types ─────────────────────────────────────────────────────────────────────
@@ -56,6 +58,7 @@ pub struct DocumentRecord {
pub is_verified: bool,
pub verified_by: Option
,
pub verified_at: u64,
+ pub is_revoked: bool,
}
#[contracttype]
@@ -64,6 +67,7 @@ pub enum DataKey {
Counter,
Document(u64),
ShipmentDocs(u64), // shipment_id → Vec of doc IDs
+ HashOwner(BytesN<32>, Address), // (content_hash, uploader) → already registered marker
}
const TTL_LEDGERS: u32 = 6_307_200; // ~1 year
@@ -102,6 +106,11 @@ impl DocumentContract {
) -> Result {
uploader.require_auth();
+ let hash_key = DataKey::HashOwner(content_hash.clone(), uploader.clone());
+ if env.storage().persistent().has(&hash_key) {
+ return Err(DocumentError::DuplicateHash);
+ }
+
let id = Self::next_id(&env);
let now = env.ledger().timestamp();
@@ -116,6 +125,7 @@ impl DocumentContract {
is_verified: false,
verified_by: None,
verified_at: 0,
+ is_revoked: false,
};
env.storage().persistent().set(&DataKey::Document(id), &doc);
@@ -123,6 +133,11 @@ impl DocumentContract {
.persistent()
.extend_ttl(&DataKey::Document(id), TTL_LEDGERS, TTL_LEDGERS);
+ env.storage().persistent().set(&hash_key, &true);
+ env.storage()
+ .persistent()
+ .extend_ttl(&hash_key, TTL_LEDGERS, TTL_LEDGERS);
+
// Append to shipment's document list.
let mut list: Vec = env
.storage()
@@ -167,18 +182,49 @@ impl DocumentContract {
Ok(())
}
+ // ── Revocation ─────────────────────────────────────────────────────────
+
+ /// Admin revokes a document — e.g. if it was registered in error or later
+ /// found to be fraudulent. Revocation is permanent and causes
+ /// `check_integrity` to always return `false` for this document.
+ pub fn revoke_document(env: Env, revoker: Address, doc_id: u64) -> Result<(), DocumentError> {
+ let admin: Address = env
+ .storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .ok_or(DocumentError::NotInitialized)?;
+
+ if revoker != admin {
+ return Err(DocumentError::Unauthorized);
+ }
+ revoker.require_auth();
+
+ let mut doc = Self::load(&env, doc_id)?;
+
+ if doc.is_revoked {
+ return Err(DocumentError::AlreadyRevoked);
+ }
+
+ doc.is_revoked = true;
+ Self::store(&env, &doc);
+ Ok(())
+ }
+
// ── Integrity check ───────────────────────────────────────────────────
/// Verify that a given hash matches the registered content_hash.
- /// Returns `true` if the hash matches, `false` otherwise.
- /// This lets anyone prove a document is untampered without downloading
- /// the full file from IPFS.
+ /// Returns `true` only if the hash matches AND the document has not
+ /// been revoked. Revoked documents always fail integrity checks,
+ /// regardless of hash, since they should no longer be trusted.
pub fn check_integrity(
env: Env,
doc_id: u64,
hash_to_check: BytesN<32>,
) -> Result {
let doc = Self::load(&env, doc_id)?;
+ if doc.is_revoked {
+ return Ok(false);
+ }
Ok(doc.content_hash == hash_to_check)
}
@@ -235,180 +281,5 @@ impl DocumentContract {
}
}
-// ── Tests ─────────────────────────────────────────────────────────────────────
-
#[cfg(test)]
-mod tests {
- use super::*;
- use soroban_sdk::{
- testutils::{Address as _, BytesN as _},
- Bytes, BytesN, Env,
- };
-
- fn setup() -> (Env, Address, DocumentContractClient<'static>) {
- let env = Env::default();
- env.mock_all_auths();
- let admin = Address::generate(&env);
- let id = env.register(DocumentContract {}, ());
- let client = DocumentContractClient::new(&env, &id);
- client.initialize(&admin);
- (env, admin, client)
- }
-
- fn fake_hash(env: &Env) -> BytesN<32> {
- BytesN::random(env)
- }
-
- fn fake_cid(env: &Env) -> Bytes {
- // Simulate a CIDv0 string encoded as bytes.
- Bytes::from_slice(env, b"QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG")
- }
-
- fn register(
- env: &Env,
- client: &DocumentContractClient,
- uploader: &Address,
- shipment_id: u64,
- ) -> (u64, BytesN<32>) {
- let hash = fake_hash(env);
- let id = client.register_document(
- uploader,
- &shipment_id,
- &DocumentType::BillOfLading,
- &hash,
- &fake_cid(env),
- );
- (id, hash)
- }
-
- #[test]
- fn test_register_document() {
- let (env, _, client) = setup();
- let uploader = Address::generate(&env);
-
- let (id, hash) = register(&env, &client, &uploader, 1);
-
- assert_eq!(id, 1);
- assert_eq!(client.get_total_documents(), 1);
-
- let doc = client.get_document(&id);
- assert_eq!(doc.id, 1);
- assert_eq!(doc.shipment_id, 1);
- assert_eq!(doc.uploader, uploader);
- assert_eq!(doc.doc_type, DocumentType::BillOfLading);
- assert_eq!(doc.content_hash, hash);
- assert!(!doc.is_verified);
- assert!(doc.verified_by.is_none());
- }
-
- #[test]
- fn test_verify_document() {
- let (env, admin, client) = setup();
- let uploader = Address::generate(&env);
-
- let (id, _) = register(&env, &client, &uploader, 1);
-
- client.verify_document(&admin, &id);
-
- let doc = client.get_document(&id);
- assert!(doc.is_verified);
- assert_eq!(doc.verified_by, Some(admin));
- }
-
- #[test]
- fn test_double_verify_fails() {
- let (env, admin, client) = setup();
- let uploader = Address::generate(&env);
- let (id, _) = register(&env, &client, &uploader, 1);
-
- client.verify_document(&admin, &id);
- let result = client.try_verify_document(&admin, &id);
- assert_eq!(result, Err(Ok(DocumentError::AlreadyVerified)));
- }
-
- #[test]
- fn test_non_admin_verify_fails() {
- let (env, _, client) = setup();
- let uploader = Address::generate(&env);
- let (id, _) = register(&env, &client, &uploader, 1);
-
- let stranger = Address::generate(&env);
- let result = client.try_verify_document(&stranger, &id);
- assert_eq!(result, Err(Ok(DocumentError::Unauthorized)));
- }
-
- #[test]
- fn test_integrity_check_pass() {
- let (env, _, client) = setup();
- let uploader = Address::generate(&env);
- let (id, original_hash) = register(&env, &client, &uploader, 1);
-
- assert!(client.check_integrity(&id, &original_hash));
- }
-
- #[test]
- fn test_integrity_check_tampered() {
- let (env, _, client) = setup();
- let uploader = Address::generate(&env);
- let (id, _) = register(&env, &client, &uploader, 1);
-
- let tampered_hash = BytesN::random(&env);
- assert!(!client.check_integrity(&id, &tampered_hash));
- }
-
- #[test]
- fn test_multiple_docs_per_shipment() {
- let (env, _, client) = setup();
- let uploader = Address::generate(&env);
-
- let (id1, _) = register(&env, &client, &uploader, 7);
- let hash2 = fake_hash(&env);
- let id2 = client.register_document(
- &uploader,
- &7u64,
- &DocumentType::ProofOfDelivery,
- &hash2,
- &fake_cid(&env),
- );
-
- let docs = client.get_documents_by_shipment(&7u64);
- assert_eq!(docs.len(), 2);
- assert_eq!(docs.get(0).unwrap(), id1);
- assert_eq!(docs.get(1).unwrap(), id2);
- }
-
- #[test]
- fn test_all_document_types() {
- let (env, _, client) = setup();
- let uploader = Address::generate(&env);
-
- let types = [
- DocumentType::BillOfLading,
- DocumentType::ProofOfDelivery,
- DocumentType::Invoice,
- DocumentType::CustomsDeclaration,
- DocumentType::InsuranceCertificate,
- DocumentType::Photo,
- DocumentType::Other,
- ];
-
- for doc_type in types {
- let id = client.register_document(
- &uploader,
- &1u64,
- &doc_type,
- &fake_hash(&env),
- &fake_cid(&env),
- );
- let doc = client.get_document(&id);
- assert_eq!(doc.doc_type, doc_type);
- }
- }
-
- #[test]
- fn test_not_found_error() {
- let (_, _, client) = setup();
- let result = client.try_get_document(&404u64);
- assert_eq!(result, Err(Ok(DocumentError::NotFound)));
- }
-}
+mod test;
diff --git a/contracts/document/src/test.rs b/contracts/document/src/test.rs
new file mode 100644
index 00000000..73eaef3b
--- /dev/null
+++ b/contracts/document/src/test.rs
@@ -0,0 +1,251 @@
+#![cfg(test)]
+
+use super::*;
+use soroban_sdk::{
+ testutils::{Address as _, BytesN as _},
+ Bytes, BytesN, Env,
+};
+
+fn setup() -> (Env, Address, DocumentContractClient<'static>) {
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let id = env.register(DocumentContract {}, ());
+ let client = DocumentContractClient::new(&env, &id);
+ client.initialize(&admin);
+ (env, admin, client)
+}
+
+fn fake_hash(env: &Env) -> BytesN<32> {
+ BytesN::random(env)
+}
+
+fn fake_cid(env: &Env) -> Bytes {
+ // Simulate a CIDv0 string encoded as bytes.
+ Bytes::from_slice(env, b"QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG")
+}
+
+fn register(
+ env: &Env,
+ client: &DocumentContractClient,
+ uploader: &Address,
+ shipment_id: u64,
+) -> (u64, BytesN<32>) {
+ let hash = fake_hash(env);
+ let id = client.register_document(
+ uploader,
+ &shipment_id,
+ &DocumentType::BillOfLading,
+ &hash,
+ &fake_cid(env),
+ );
+ (id, hash)
+}
+
+/// Register a document; get_document returns the correct hash and CID-bearing record.
+#[test]
+fn test_register_document_hash() {
+ let (env, _, client) = setup();
+ let uploader = Address::generate(&env);
+
+ let (id, hash) = register(&env, &client, &uploader, 1);
+
+ assert_eq!(id, 1);
+ assert_eq!(client.get_total_documents(), 1);
+
+ let doc = client.get_document(&id);
+ assert_eq!(doc.id, 1);
+ assert_eq!(doc.shipment_id, 1);
+ assert_eq!(doc.uploader, uploader);
+ assert_eq!(doc.doc_type, DocumentType::BillOfLading);
+ assert_eq!(doc.content_hash, hash);
+ assert!(!doc.is_verified);
+ assert!(doc.verified_by.is_none());
+ assert!(!doc.is_revoked);
+}
+
+/// Registering the same (hash, uploader) pair twice should fail.
+#[test]
+fn test_register_duplicate_hash_fails() {
+ let (env, _, client) = setup();
+ let uploader = Address::generate(&env);
+ let hash = fake_hash(&env);
+
+ client.register_document(
+ &uploader,
+ &1u64,
+ &DocumentType::BillOfLading,
+ &hash,
+ &fake_cid(&env),
+ );
+
+ let result = client.try_register_document(
+ &uploader,
+ &1u64,
+ &DocumentType::BillOfLading,
+ &hash,
+ &fake_cid(&env),
+ );
+ assert_eq!(result, Err(Ok(DocumentError::DuplicateHash)));
+}
+
+/// verify_integrity with the correct hash returns true.
+#[test]
+fn test_verify_integrity_correct_hash() {
+ let (env, _, client) = setup();
+ let uploader = Address::generate(&env);
+ let (id, original_hash) = register(&env, &client, &uploader, 1);
+
+ assert!(client.check_integrity(&id, &original_hash));
+}
+
+/// verify_integrity with a tampered hash returns false.
+#[test]
+fn test_verify_integrity_wrong_hash() {
+ let (env, _, client) = setup();
+ let uploader = Address::generate(&env);
+ let (id, _) = register(&env, &client, &uploader, 1);
+
+ let tampered_hash = BytesN::random(&env);
+ assert!(!client.check_integrity(&id, &tampered_hash));
+}
+
+/// Admin calls verify_document; document.is_verified becomes true.
+#[test]
+fn test_admin_verify_document() {
+ let (env, admin, client) = setup();
+ let uploader = Address::generate(&env);
+
+ let (id, _) = register(&env, &client, &uploader, 1);
+
+ client.verify_document(&admin, &id);
+
+ let doc = client.get_document(&id);
+ assert!(doc.is_verified);
+ assert_eq!(doc.verified_by, Some(admin));
+}
+
+#[test]
+fn test_double_verify_fails() {
+ let (env, admin, client) = setup();
+ let uploader = Address::generate(&env);
+ let (id, _) = register(&env, &client, &uploader, 1);
+
+ client.verify_document(&admin, &id);
+ let result = client.try_verify_document(&admin, &id);
+ assert_eq!(result, Err(Ok(DocumentError::AlreadyVerified)));
+}
+
+/// Non-admin calling verify_document should fail with Unauthorized.
+#[test]
+fn test_non_admin_cannot_verify() {
+ let (env, _, client) = setup();
+ let uploader = Address::generate(&env);
+ let (id, _) = register(&env, &client, &uploader, 1);
+
+ let stranger = Address::generate(&env);
+ let result = client.try_verify_document(&stranger, &id);
+ assert_eq!(result, Err(Ok(DocumentError::Unauthorized)));
+}
+
+/// Revoking a document sets is_revoked = true; subsequent integrity checks fail.
+#[test]
+fn test_revoke_document() {
+ let (env, admin, client) = setup();
+ let uploader = Address::generate(&env);
+ let (id, original_hash) = register(&env, &client, &uploader, 1);
+
+ // Sanity: integrity check passes before revocation.
+ assert!(client.check_integrity(&id, &original_hash));
+
+ client.revoke_document(&admin, &id);
+
+ let doc = client.get_document(&id);
+ assert!(doc.is_revoked);
+
+ // Even with the correct hash, a revoked document never passes integrity.
+ assert!(!client.check_integrity(&id, &original_hash));
+}
+
+/// Revoking the same document twice should fail.
+#[test]
+fn test_double_revoke_fails() {
+ let (env, admin, client) = setup();
+ let uploader = Address::generate(&env);
+ let (id, _) = register(&env, &client, &uploader, 1);
+
+ client.revoke_document(&admin, &id);
+ let result = client.try_revoke_document(&admin, &id);
+ assert_eq!(result, Err(Ok(DocumentError::AlreadyRevoked)));
+}
+
+/// Non-admin calling revoke_document should fail with Unauthorized.
+#[test]
+fn test_non_admin_cannot_revoke() {
+ let (env, _, client) = setup();
+ let uploader = Address::generate(&env);
+ let (id, _) = register(&env, &client, &uploader, 1);
+
+ let stranger = Address::generate(&env);
+ let result = client.try_revoke_document(&stranger, &id);
+ assert_eq!(result, Err(Ok(DocumentError::Unauthorized)));
+
+ // Document remains unrevoked after the failed attempt.
+ assert!(!client.get_document(&id).is_revoked);
+}
+
+#[test]
+fn test_multiple_docs_per_shipment() {
+ let (env, _, client) = setup();
+ let uploader = Address::generate(&env);
+
+ let (id1, _) = register(&env, &client, &uploader, 7);
+ let hash2 = fake_hash(&env);
+ let id2 = client.register_document(
+ &uploader,
+ &7u64,
+ &DocumentType::ProofOfDelivery,
+ &hash2,
+ &fake_cid(&env),
+ );
+
+ let docs = client.get_documents_by_shipment(&7u64);
+ assert_eq!(docs.len(), 2);
+ assert_eq!(docs.get(0).unwrap(), id1);
+ assert_eq!(docs.get(1).unwrap(), id2);
+}
+
+#[test]
+fn test_all_document_types() {
+ let (env, _, client) = setup();
+ let uploader = Address::generate(&env);
+
+ let types = [
+ DocumentType::BillOfLading,
+ DocumentType::ProofOfDelivery,
+ DocumentType::Invoice,
+ DocumentType::CustomsDeclaration,
+ DocumentType::InsuranceCertificate,
+ DocumentType::Photo,
+ DocumentType::Other,
+ ];
+
+ for doc_type in types {
+ let id = client.register_document(
+ &uploader,
+ &1u64,
+ &doc_type,
+ &fake_hash(&env),
+ &fake_cid(&env),
+ );
+ let doc = client.get_document(&id);
+ assert_eq!(doc.doc_type, doc_type);
+ }
+}
+
+#[test]
+fn test_not_found_error() {
+ let (_, _, client) = setup();
+ let result = client.try_get_document(&404u64);
+ assert_eq!(result, Err(Ok(DocumentError::NotFound)));
+}
diff --git a/contracts/identity/src/lib.rs b/contracts/identity/src/lib.rs
index 4a25071f..5ac3b0e7 100644
--- a/contracts/identity/src/lib.rs
+++ b/contracts/identity/src/lib.rs
@@ -104,85 +104,4 @@ impl IdentityContract {
}
#[cfg(test)]
-mod tests {
- use super::*;
- use soroban_sdk::{
- testutils::{Address as _, BytesN as _},
- Env,
- };
-
- #[test]
- fn test_register_and_verify() {
- let env = Env::default();
- env.mock_all_auths();
-
- let contract_id = env.register(IdentityContract {}, ());
- let client = IdentityContractClient::new(&env, &contract_id);
-
- let admin = Address::generate(&env);
- let wallet = Address::generate(&env);
- let hash = BytesN::random(&env);
-
- client.initialize(&admin);
- client.register_identity(&hash, &wallet);
-
- assert!(client.verify_identity(&wallet));
- assert_eq!(client.get_user_identity(&wallet), hash);
- }
-
- #[test]
- fn test_double_register_fails() {
- let env = Env::default();
- env.mock_all_auths();
-
- let contract_id = env.register(IdentityContract {}, ());
- let client = IdentityContractClient::new(&env, &contract_id);
-
- let admin = Address::generate(&env);
- let wallet = Address::generate(&env);
- let hash = BytesN::random(&env);
-
- client.initialize(&admin);
- client.register_identity(&hash, &wallet);
-
- let result = client.try_register_identity(&hash, &wallet);
- assert_eq!(result, Err(Ok(IdentityError::AlreadyRegistered)));
- }
-
- #[test]
- fn test_revoke_identity() {
- let env = Env::default();
- env.mock_all_auths();
-
- let contract_id = env.register(IdentityContract {}, ());
- let client = IdentityContractClient::new(&env, &contract_id);
-
- let admin = Address::generate(&env);
- let wallet = Address::generate(&env);
- let hash = BytesN::random(&env);
-
- client.initialize(&admin);
- client.register_identity(&hash, &wallet);
- assert!(client.verify_identity(&wallet));
-
- client.revoke_identity(&wallet);
- assert!(!client.verify_identity(&wallet));
- }
-
- #[test]
- fn test_get_unregistered_fails() {
- let env = Env::default();
- env.mock_all_auths();
-
- let contract_id = env.register(IdentityContract {}, ());
- let client = IdentityContractClient::new(&env, &contract_id);
-
- let admin = Address::generate(&env);
- let wallet = Address::generate(&env);
-
- client.initialize(&admin);
-
- let result = client.try_get_user_identity(&wallet);
- assert_eq!(result, Err(Ok(IdentityError::NotRegistered)));
- }
-}
+mod test;
diff --git a/contracts/identity/src/test.rs b/contracts/identity/src/test.rs
new file mode 100644
index 00000000..0e31562b
--- /dev/null
+++ b/contracts/identity/src/test.rs
@@ -0,0 +1,124 @@
+#![cfg(test)]
+
+use super::*;
+use soroban_sdk::{
+ testutils::{Address as _, BytesN as _},
+ Env, IntoVal,
+};
+
+fn setup() -> (Env, Address, IdentityContractClient<'static>) {
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let admin = Address::generate(&env);
+ let contract_id = env.register(IdentityContract {}, ());
+ let client = IdentityContractClient::new(&env, &contract_id);
+ client.initialize(&admin);
+
+ (env, admin, client)
+}
+
+/// Register a wallet, then get_user_identity — should return the hash.
+#[test]
+fn test_register_new_identity() {
+ let (env, _admin, client) = setup();
+ let wallet = Address::generate(&env);
+ let hash = BytesN::random(&env);
+
+ client.register_identity(&hash, &wallet);
+
+ assert!(client.verify_identity(&wallet));
+ assert_eq!(client.get_user_identity(&wallet), hash);
+}
+
+/// Registering the same wallet twice should fail with AlreadyRegistered.
+#[test]
+fn test_register_duplicate_wallet_fails() {
+ let (env, _admin, client) = setup();
+ let wallet = Address::generate(&env);
+ let hash = BytesN::random(&env);
+
+ client.register_identity(&hash, &wallet);
+
+ let result = client.try_register_identity(&hash, &wallet);
+ assert_eq!(result, Err(Ok(IdentityError::AlreadyRegistered)));
+}
+
+/// get_user_identity for an unknown wallet should return NotRegistered.
+#[test]
+fn test_get_unregistered_wallet_returns_none() {
+ let (env, _admin, client) = setup();
+ let wallet = Address::generate(&env);
+
+ let result = client.try_get_user_identity(&wallet);
+ assert_eq!(result, Err(Ok(IdentityError::NotRegistered)));
+}
+
+/// Admin revokes a registration; subsequent lookups treat the wallet as unregistered.
+#[test]
+fn test_revoke_identity_by_admin() {
+ let (env, _admin, client) = setup();
+ let wallet = Address::generate(&env);
+ let hash = BytesN::random(&env);
+
+ client.register_identity(&hash, &wallet);
+ assert!(client.verify_identity(&wallet));
+
+ client.revoke_identity(&wallet);
+
+ assert!(!client.verify_identity(&wallet));
+ let result = client.try_get_user_identity(&wallet);
+ assert_eq!(result, Err(Ok(IdentityError::NotRegistered)));
+}
+
+/// Revoking as a non-admin wallet should fail because `revoke_identity` only
+/// authorizes the address stored as `Admin`. We mock authorization for a
+/// non-admin signer (instead of mocking all auths), so the contract's
+/// `admin.require_auth()` call has no valid signature to satisfy and the
+/// invocation fails.
+#[test]
+fn test_revoke_by_non_admin_fails() {
+ let env = Env::default();
+
+ let admin = Address::generate(&env);
+ let non_admin = Address::generate(&env);
+ let wallet = Address::generate(&env);
+ let hash = BytesN::random(&env);
+
+ let contract_id = env.register(IdentityContract {}, ());
+ let client = IdentityContractClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+ client.initialize(&admin);
+ client.register_identity(&hash, &wallet);
+
+ // Only authorize `non_admin` for this call — `admin` never signs it.
+ env.mock_auths(&[soroban_sdk::testutils::MockAuth {
+ address: &non_admin,
+ invoke: &soroban_sdk::testutils::MockAuthInvoke {
+ contract: &contract_id,
+ fn_name: "revoke_identity",
+ args: (wallet.clone(),).into_val(&env),
+ sub_invokes: &[],
+ },
+ }]);
+
+ let result = client.try_revoke_identity(&wallet);
+ assert!(result.is_err());
+ assert!(client.verify_identity(&wallet));
+}
+
+/// is_registered (via verify_identity) returns true for a registered wallet
+/// and false for an unregistered one.
+#[test]
+fn test_is_registered_returns_correct_bool() {
+ let (env, _admin, client) = setup();
+ let registered = Address::generate(&env);
+ let unregistered = Address::generate(&env);
+ let hash = BytesN::random(&env);
+
+ client.register_identity(&hash, ®istered);
+
+ assert!(client.verify_identity(®istered));
+ assert!(!client.verify_identity(&unregistered));
+}
diff --git a/contracts/reputation/src/lib.rs b/contracts/reputation/src/lib.rs
index cc69980f..831da562 100644
--- a/contracts/reputation/src/lib.rs
+++ b/contracts/reputation/src/lib.rs
@@ -427,232 +427,5 @@ impl ReputationContract {
}
}
-// ── Tests ─────────────────────────────────────────────────────────────────────
-
#[cfg(test)]
-mod tests {
- use super::*;
- use soroban_sdk::{testutils::Address as _, Env};
-
- fn setup() -> (Env, Address, Address, ReputationContractClient<'static>) {
- let env = Env::default();
- env.mock_all_auths();
-
- let admin = Address::generate(&env);
- let auth_contract = Address::generate(&env); // simulates shipment contract
- let contract_id = env.register(ReputationContract {}, ());
- let client = ReputationContractClient::new(&env, &contract_id);
- client.initialize(&admin, &auth_contract);
-
- (env, admin, auth_contract, client)
- }
-
- #[test]
- fn test_register_user() {
- let (env, _, _, client) = setup();
- let user = Address::generate(&env);
-
- client.register_user(&user, &UserType::Carrier);
-
- let rep = client.get_reputation(&user);
- assert_eq!(rep.user, user);
- assert_eq!(rep.user_type, UserType::Carrier);
- assert_eq!(rep.rating_count, 0);
- assert_eq!(rep.average_rating, 0);
- }
-
- #[test]
- fn test_register_twice_fails() {
- let (env, _, _, client) = setup();
- let user = Address::generate(&env);
-
- client.register_user(&user, &UserType::Carrier);
- let result = client.try_register_user(&user, &UserType::Carrier);
- assert_eq!(result, Err(Ok(ReputationError::UserAlreadyRegistered)));
- }
-
- #[test]
- fn test_submit_rating_and_average() {
- let (env, _, _, client) = setup();
- let rater1 = Address::generate(&env);
- let rater2 = Address::generate(&env);
- let rater3 = Address::generate(&env);
- let carrier = Address::generate(&env);
-
- client.register_user(&rater1, &UserType::Shipper);
- client.register_user(&rater2, &UserType::Shipper);
- client.register_user(&rater3, &UserType::Shipper);
- client.register_user(&carrier, &UserType::Carrier);
-
- // Shipment 1: score 5
- client.submit_rating(&rater1, &1u64, &carrier, &5u32);
- // Shipment 2: score 4
- client.submit_rating(&rater2, &2u64, &carrier, &4u32);
- // Shipment 3: score 3
- client.submit_rating(&rater3, &3u64, &carrier, &3u32);
-
- let rep = client.get_reputation(&carrier);
- assert_eq!(rep.rating_count, 3);
- // (5+4+3)*100 / 3 = 400
- assert_eq!(rep.average_rating, 400);
- }
-
- #[test]
- fn test_cannot_rate_self() {
- let (env, _, _, client) = setup();
- let user = Address::generate(&env);
- client.register_user(&user, &UserType::Carrier);
-
- let result = client.try_submit_rating(&user, &1u64, &user, &5u32);
- assert_eq!(result, Err(Ok(ReputationError::CannotRateSelf)));
- }
-
- #[test]
- fn test_invalid_score() {
- let (env, _, _, client) = setup();
- let rater = Address::generate(&env);
- let rated = Address::generate(&env);
- client.register_user(&rater, &UserType::Shipper);
- client.register_user(&rated, &UserType::Carrier);
-
- assert_eq!(
- client.try_submit_rating(&rater, &1u64, &rated, &0u32),
- Err(Ok(ReputationError::InvalidScore))
- );
- assert_eq!(
- client.try_submit_rating(&rater, &1u64, &rated, &6u32),
- Err(Ok(ReputationError::InvalidScore))
- );
- }
-
- #[test]
- fn test_duplicate_rating_fails() {
- let (env, _, _, client) = setup();
- let rater = Address::generate(&env);
- let rated = Address::generate(&env);
- client.register_user(&rater, &UserType::Shipper);
- client.register_user(&rated, &UserType::Carrier);
-
- client.submit_rating(&rater, &1u64, &rated, &5u32);
- let result = client.try_submit_rating(&rater, &1u64, &rated, &4u32);
- assert_eq!(result, Err(Ok(ReputationError::AlreadyRatedShipment)));
- }
-
- #[test]
- fn test_update_stats_carrier() {
- let (env, _, auth_contract, client) = setup();
- let carrier = Address::generate(&env);
- client.register_user(&carrier, &UserType::Carrier);
-
- client.update_stats(&auth_contract, &carrier, &true, &false); // on-time
- client.update_stats(&auth_contract, &carrier, &true, &false); // on-time
- client.update_stats(&auth_contract, &carrier, &false, &false); // late
-
- let rep = client.get_reputation(&carrier);
- assert_eq!(rep.total_completed, 3);
- assert_eq!(rep.on_time_count, 2);
- assert_eq!(rep.late_count, 1);
- }
-
- #[test]
- fn test_update_stats_shipper() {
- let (env, _, auth_contract, client) = setup();
- let shipper = Address::generate(&env);
- client.register_user(&shipper, &UserType::Shipper);
-
- client.update_stats(&auth_contract, &shipper, &false, &true); // success
- client.update_stats(&auth_contract, &shipper, &false, &false); // cancelled
-
- let rep = client.get_reputation(&shipper);
- assert_eq!(rep.total_completed, 2);
- assert_eq!(rep.success_count, 1);
- assert_eq!(rep.cancel_count, 1);
- }
-
- #[test]
- fn test_unauthorized_update_stats_fails() {
- let (env, _, _, client) = setup();
- let random = Address::generate(&env);
- let carrier = Address::generate(&env);
- client.register_user(&carrier, &UserType::Carrier);
-
- let result = client.try_update_stats(&random, &carrier, &true, &false);
- assert_eq!(result, Err(Ok(ReputationError::Unauthorized)));
- }
-
- #[test]
- fn test_calculate_score_perfect_carrier() {
- let (env, _, auth_contract, client) = setup();
- let rater = Address::generate(&env);
- let carrier = Address::generate(&env);
- client.register_user(&rater, &UserType::Shipper);
- client.register_user(&carrier, &UserType::Carrier);
-
- // 5-star rating
- client.submit_rating(&rater, &1u64, &carrier, &5u32);
- // Perfect on-time record
- client.update_stats(&auth_contract, &carrier, &true, &false);
-
- let score = client.calculate_score(&carrier);
- // avg_rating = 500 (5 stars × 100), on_time_pct = 100%, rating/completed = 100%
- // rating_component = 500, rate_component = 300, completion_component = 200
- // total = 1000
- assert_eq!(score, 1000);
- }
-
- #[test]
- fn test_calculate_score_new_user() {
- let (env, _, _, client) = setup();
- let user = Address::generate(&env);
- client.register_user(&user, &UserType::Carrier);
-
- let score = client.calculate_score(&user);
- assert_eq!(score, 0); // no data yet
- }
-
- #[test]
- fn test_has_rated_shipment() {
- let (env, _, _, client) = setup();
- let rater = Address::generate(&env);
- let rated = Address::generate(&env);
- client.register_user(&rater, &UserType::Shipper);
- client.register_user(&rated, &UserType::Carrier);
-
- assert!(!client.has_rated_shipment(&1u64, &rater));
- client.submit_rating(&rater, &1u64, &rated, &4u32);
- assert!(client.has_rated_shipment(&1u64, &rater));
- }
-
- #[test]
- fn test_get_rating() {
- let (env, _, _, client) = setup();
- let rater = Address::generate(&env);
- let rated = Address::generate(&env);
- client.register_user(&rater, &UserType::Shipper);
- client.register_user(&rated, &UserType::Carrier);
-
- let rating_id = client.submit_rating(&rater, &5u64, &rated, &4u32);
- let record = client.get_rating(&rating_id);
-
- assert_eq!(record.shipment_id, 5);
- assert_eq!(record.rater, rater);
- assert_eq!(record.rated, rated);
- assert_eq!(record.score, 4);
- }
-
- #[test]
- fn test_total_ratings_counter() {
- let (env, _, _, client) = setup();
- let rater1 = Address::generate(&env);
- let rater2 = Address::generate(&env);
- let rated = Address::generate(&env);
- client.register_user(&rater1, &UserType::Shipper);
- client.register_user(&rater2, &UserType::Shipper);
- client.register_user(&rated, &UserType::Carrier);
-
- assert_eq!(client.get_total_ratings(), 0);
- client.submit_rating(&rater1, &1u64, &rated, &5u32);
- client.submit_rating(&rater2, &2u64, &rated, &3u32);
- assert_eq!(client.get_total_ratings(), 2);
- }
-}
+mod test;
diff --git a/contracts/reputation/src/test.rs b/contracts/reputation/src/test.rs
new file mode 100644
index 00000000..5a7573dc
--- /dev/null
+++ b/contracts/reputation/src/test.rs
@@ -0,0 +1,245 @@
+#![cfg(test)]
+
+use super::*;
+use soroban_sdk::{testutils::Address as _, Env};
+
+fn setup() -> (Env, Address, Address, ReputationContractClient<'static>) {
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let admin = Address::generate(&env);
+ let auth_contract = Address::generate(&env); // simulates shipment contract
+ let contract_id = env.register(ReputationContract {}, ());
+ let client = ReputationContractClient::new(&env, &contract_id);
+ client.initialize(&admin, &auth_contract);
+
+ (env, admin, auth_contract, client)
+}
+
+/// Record one rating; get_reputation should return the correct totals.
+#[test]
+fn test_record_single_rating() {
+ let (env, _, _, client) = setup();
+ let rater = Address::generate(&env);
+ let carrier = Address::generate(&env);
+ client.register_user(&rater, &UserType::Shipper);
+ client.register_user(&carrier, &UserType::Carrier);
+
+ client.submit_rating(&rater, &1u64, &carrier, &5u32);
+
+ let rep = client.get_reputation(&carrier);
+ assert_eq!(rep.rating_count, 1);
+ assert_eq!(rep.average_rating, 500);
+}
+
+#[test]
+fn test_register_user() {
+ let (env, _, _, client) = setup();
+ let user = Address::generate(&env);
+
+ client.register_user(&user, &UserType::Carrier);
+
+ let rep = client.get_reputation(&user);
+ assert_eq!(rep.user, user);
+ assert_eq!(rep.user_type, UserType::Carrier);
+ assert_eq!(rep.rating_count, 0);
+ assert_eq!(rep.average_rating, 0);
+}
+
+#[test]
+fn test_register_twice_fails() {
+ let (env, _, _, client) = setup();
+ let user = Address::generate(&env);
+
+ client.register_user(&user, &UserType::Carrier);
+ let result = client.try_register_user(&user, &UserType::Carrier);
+ assert_eq!(result, Err(Ok(ReputationError::UserAlreadyRegistered)));
+}
+
+/// Record 5 ratings (mix of star values); verify the running average.
+#[test]
+fn test_record_multiple_ratings_aggregates_correctly() {
+ let (env, _, _, client) = setup();
+ let rater1 = Address::generate(&env);
+ let rater2 = Address::generate(&env);
+ let rater3 = Address::generate(&env);
+ let carrier = Address::generate(&env);
+
+ client.register_user(&rater1, &UserType::Shipper);
+ client.register_user(&rater2, &UserType::Shipper);
+ client.register_user(&rater3, &UserType::Shipper);
+ client.register_user(&carrier, &UserType::Carrier);
+
+ client.submit_rating(&rater1, &1u64, &carrier, &5u32);
+ client.submit_rating(&rater2, &2u64, &carrier, &4u32);
+ client.submit_rating(&rater3, &3u64, &carrier, &3u32);
+
+ let rep = client.get_reputation(&carrier);
+ assert_eq!(rep.rating_count, 3);
+ // (5+4+3)*100 / 3 = 400
+ assert_eq!(rep.average_rating, 400);
+}
+
+#[test]
+fn test_cannot_rate_self() {
+ let (env, _, _, client) = setup();
+ let user = Address::generate(&env);
+ client.register_user(&user, &UserType::Carrier);
+
+ let result = client.try_submit_rating(&user, &1u64, &user, &5u32);
+ assert_eq!(result, Err(Ok(ReputationError::CannotRateSelf)));
+}
+
+/// Recording a rating of 0 or 6 should fail with InvalidScore.
+#[test]
+fn test_star_rating_out_of_range_fails() {
+ let (env, _, _, client) = setup();
+ let rater = Address::generate(&env);
+ let rated = Address::generate(&env);
+ client.register_user(&rater, &UserType::Shipper);
+ client.register_user(&rated, &UserType::Carrier);
+
+ assert_eq!(
+ client.try_submit_rating(&rater, &1u64, &rated, &0u32),
+ Err(Ok(ReputationError::InvalidScore))
+ );
+ assert_eq!(
+ client.try_submit_rating(&rater, &1u64, &rated, &6u32),
+ Err(Ok(ReputationError::InvalidScore))
+ );
+}
+
+/// Submitting the same shipment_id twice for the same ratee should fail.
+#[test]
+fn test_cannot_rate_same_shipment_twice() {
+ let (env, _, _, client) = setup();
+ let rater = Address::generate(&env);
+ let rated = Address::generate(&env);
+ client.register_user(&rater, &UserType::Shipper);
+ client.register_user(&rated, &UserType::Carrier);
+
+ client.submit_rating(&rater, &1u64, &rated, &5u32);
+ let result = client.try_submit_rating(&rater, &1u64, &rated, &4u32);
+ assert_eq!(result, Err(Ok(ReputationError::AlreadyRatedShipment)));
+}
+
+#[test]
+fn test_update_stats_carrier() {
+ let (env, _, auth_contract, client) = setup();
+ let carrier = Address::generate(&env);
+ client.register_user(&carrier, &UserType::Carrier);
+
+ client.update_stats(&auth_contract, &carrier, &true, &false); // on-time
+ client.update_stats(&auth_contract, &carrier, &true, &false); // on-time
+ client.update_stats(&auth_contract, &carrier, &false, &false); // late
+
+ let rep = client.get_reputation(&carrier);
+ assert_eq!(rep.total_completed, 3);
+ assert_eq!(rep.on_time_count, 2);
+ assert_eq!(rep.late_count, 1);
+}
+
+#[test]
+fn test_update_stats_shipper() {
+ let (env, _, auth_contract, client) = setup();
+ let shipper = Address::generate(&env);
+ client.register_user(&shipper, &UserType::Shipper);
+
+ client.update_stats(&auth_contract, &shipper, &false, &true); // success
+ client.update_stats(&auth_contract, &shipper, &false, &false); // cancelled
+
+ let rep = client.get_reputation(&shipper);
+ assert_eq!(rep.total_completed, 2);
+ assert_eq!(rep.success_count, 1);
+ assert_eq!(rep.cancel_count, 1);
+}
+
+#[test]
+fn test_unauthorized_update_stats_fails() {
+ let (env, _, _, client) = setup();
+ let random = Address::generate(&env);
+ let carrier = Address::generate(&env);
+ client.register_user(&carrier, &UserType::Carrier);
+
+ let result = client.try_update_stats(&random, &carrier, &true, &false);
+ assert_eq!(result, Err(Ok(ReputationError::Unauthorized)));
+}
+
+/// Record known ratings and verify the composite score matches the formula.
+#[test]
+fn test_composite_score_formula() {
+ let (env, _, auth_contract, client) = setup();
+ let rater = Address::generate(&env);
+ let carrier = Address::generate(&env);
+ client.register_user(&rater, &UserType::Shipper);
+ client.register_user(&carrier, &UserType::Carrier);
+
+ // 5-star rating
+ client.submit_rating(&rater, &1u64, &carrier, &5u32);
+ // Perfect on-time record
+ client.update_stats(&auth_contract, &carrier, &true, &false);
+
+ let score = client.calculate_score(&carrier);
+ // avg_rating = 500 (5 stars × 100), on_time_pct = 100%, rating/completed = 100%
+ // rating_component = 500, rate_component = 300, completion_component = 200
+ // total = 1000
+ assert_eq!(score, 1000);
+}
+
+/// get_score (calculate_score) for a wallet that has never been rated
+/// returns 0, the documented default.
+#[test]
+fn test_get_score_for_unrated_carrier() {
+ let (env, _, _, client) = setup();
+ let user = Address::generate(&env);
+ client.register_user(&user, &UserType::Carrier);
+
+ let score = client.calculate_score(&user);
+ assert_eq!(score, 0); // no data yet
+}
+
+#[test]
+fn test_has_rated_shipment() {
+ let (env, _, _, client) = setup();
+ let rater = Address::generate(&env);
+ let rated = Address::generate(&env);
+ client.register_user(&rater, &UserType::Shipper);
+ client.register_user(&rated, &UserType::Carrier);
+
+ assert!(!client.has_rated_shipment(&1u64, &rater));
+ client.submit_rating(&rater, &1u64, &rated, &4u32);
+ assert!(client.has_rated_shipment(&1u64, &rater));
+}
+
+#[test]
+fn test_get_rating() {
+ let (env, _, _, client) = setup();
+ let rater = Address::generate(&env);
+ let rated = Address::generate(&env);
+ client.register_user(&rater, &UserType::Shipper);
+ client.register_user(&rated, &UserType::Carrier);
+
+ let rating_id = client.submit_rating(&rater, &5u64, &rated, &4u32);
+ let record = client.get_rating(&rating_id);
+
+ assert_eq!(record.shipment_id, 5);
+ assert_eq!(record.rater, rater);
+ assert_eq!(record.rated, rated);
+ assert_eq!(record.score, 4);
+}
+
+#[test]
+fn test_total_ratings_counter() {
+ let (env, _, _, client) = setup();
+ let rater1 = Address::generate(&env);
+ let rater2 = Address::generate(&env);
+ let rated = Address::generate(&env);
+ client.register_user(&rater1, &UserType::Shipper);
+ client.register_user(&rater2, &UserType::Shipper);
+ client.register_user(&rated, &UserType::Carrier);
+
+ assert_eq!(client.get_total_ratings(), 0);
+ client.submit_rating(&rater1, &1u64, &rated, &5u32);
+ client.submit_rating(&rater2, &2u64, &rated, &3u32);
+ assert_eq!(client.get_total_ratings(), 2);
+}