From edfc1efcb5b54a478c1278293660a0e97e36cc26 Mon Sep 17 00:00:00 2001 From: Ijay Abby Date: Wed, 29 Apr 2026 12:31:45 +0300 Subject: [PATCH] metadata_update_function --- stellar-contracts/src/lib.rs | 35 +++++ stellar-contracts/src/test.rs | 112 +++++++++++++++ stellar-contracts/src/types.rs | 9 ++ .../src/update_metadata_uri_test.rs | 128 ++++++++++++++++++ 4 files changed, 284 insertions(+) create mode 100644 stellar-contracts/src/update_metadata_uri_test.rs diff --git a/stellar-contracts/src/lib.rs b/stellar-contracts/src/lib.rs index 81ce0dde7..09cddcc2c 100644 --- a/stellar-contracts/src/lib.rs +++ b/stellar-contracts/src/lib.rs @@ -20,6 +20,8 @@ pub use admin_multisig::*; mod admin_multisig_test; #[cfg(test)] mod multisig_test; +#[cfg(test)] +mod update_metadata_uri_test; #[contract] pub struct CertificateContract; @@ -212,6 +214,39 @@ impl CertificateContract { .set(&DataKey::Certificate(id.clone()), &cert); } + /// Update the metadata URI of an existing certificate. + /// Only the original issuer of the certificate may call this function. + pub fn update_metadata_uri(env: Env, id: String, new_uri: String) { + let mut cert: Certificate = env + .storage() + .instance() + .get(&DataKey::Certificate(id.clone())) + .expect("Certificate not found"); + + // Only the original issuer is authorised to update the metadata URI + cert.issuer.require_auth(); + + if new_uri.len() == 0 { + panic!("metadata_uri cannot be empty"); + } + + let old_uri = cert.metadata_uri.clone(); + cert.metadata_uri = new_uri.clone(); + env.storage() + .instance() + .set(&DataKey::Certificate(id.clone()), &cert); + + env.events().publish( + (symbol_short!("meta_upd"), id.clone()), + MetadataUriUpdatedEvent { + id, + issuer: cert.issuer, + old_uri, + new_uri, + }, + ); + } + /// Verify if a certificate is valid (active and not expired) pub fn is_valid(env: Env, id: String) -> bool { if let Some(cert) = env diff --git a/stellar-contracts/src/test.rs b/stellar-contracts/src/test.rs index 548b62440..02331790a 100644 --- a/stellar-contracts/src/test.rs +++ b/stellar-contracts/src/test.rs @@ -101,12 +101,124 @@ fn test_issue_and_revoke() { let revoked = client.is_revoked(&id); assert!(revoked); + let metadata_uri = String::from_str(&env, "ipfs://Qm..."); + + env.mock_all_auths(); + client.issue_certificate(&id, &issuer, &owner, &metadata_uri); + + let cert = client.get_certificate(&id); + assert_eq!(cert.id, id); + assert_eq!(cert.status, CertificateStatus::Active); + assert_eq!(cert.revoked, false); + + let reason = String::from_str(&env, "Violation of terms"); + client.revoke_certificate(&id, &reason); + + let revoked = client.is_revoked(&id); + assert!(revoked); + let cert_revoked = client.get_certificate(&id); assert_eq!(cert_revoked.status, CertificateStatus::Revoked); assert_eq!(cert_revoked.revoked, true); assert_eq!(cert_revoked.revocation_reason, Some(reason)); } +#[test] +fn test_update_metadata_uri_by_original_issuer() { + let env = Env::default(); + let contract_id = env.register_contract(None, CertificateContract); + let client = CertificateContractClient::new(&env, &contract_id); + + let issuer = Address::generate(&env); + let owner = Address::generate(&env); + let id = String::from_str(&env, "cert-meta-1"); + let original_uri = String::from_str(&env, "ipfs://QmOriginal"); + let new_uri = String::from_str(&env, "ipfs://QmMigrated"); + + env.mock_all_auths(); + client.initialize(&issuer); + client.add_issuer(&issuer); + client.issue_certificate(&id, &issuer, &owner, &original_uri, &None); + + client.update_metadata_uri(&id, &new_uri); + + let cert = client.get_certificate(&id).expect("Certificate not found"); + assert_eq!(cert.metadata_uri, new_uri); +} + +#[test] +fn test_update_metadata_uri_rejected_for_non_issuer() { + let env = Env::default(); + let contract_id = env.register_contract(None, CertificateContract); + let client = CertificateContractClient::new(&env, &contract_id); + + let issuer = Address::generate(&env); + let other = Address::generate(&env); + let owner = Address::generate(&env); + let id = String::from_str(&env, "cert-meta-2"); + let original_uri = String::from_str(&env, "ipfs://QmOriginal"); + let new_uri = String::from_str(&env, "ipfs://QmAttacker"); + + env.mock_all_auths(); + client.initialize(&issuer); + client.add_issuer(&issuer); + client.issue_certificate(&id, &issuer, &owner, &original_uri, &None); + + // Simulate auth as `other` only — require_auth on issuer will fail + env.set_auths(&[( + other.clone(), + soroban_sdk::testutils::AuthorizedInvocation { + function: soroban_sdk::testutils::AuthorizedFunction::Contract(( + contract_id.clone(), + soroban_sdk::symbol_short!("upd_meta"), + soroban_sdk::vec![&env], + )), + sub_invocations: soroban_sdk::vec![&env], + }, + )]); + + let result = client.try_update_metadata_uri(&id, &new_uri); + assert!(result.is_err(), "Non-issuer should not be able to update metadata_uri"); +} + +#[test] +fn test_update_metadata_uri_certificate_not_found() { + let env = Env::default(); + let contract_id = env.register_contract(None, CertificateContract); + let client = CertificateContractClient::new(&env, &contract_id); + + let issuer = Address::generate(&env); + let missing_id = String::from_str(&env, "cert-does-not-exist"); + let new_uri = String::from_str(&env, "ipfs://QmAnything"); + + env.mock_all_auths(); + client.initialize(&issuer); + + let result = client.try_update_metadata_uri(&missing_id, &new_uri); + assert!(result.is_err(), "Should panic when certificate is not found"); +} + +#[test] +fn test_update_metadata_uri_rejects_empty_uri() { + let env = Env::default(); + let contract_id = env.register_contract(None, CertificateContract); + let client = CertificateContractClient::new(&env, &contract_id); + + let issuer = Address::generate(&env); + let owner = Address::generate(&env); + let id = String::from_str(&env, "cert-meta-empty"); + let original_uri = String::from_str(&env, "ipfs://QmOriginal"); + let empty_uri = String::from_str(&env, ""); + + env.mock_all_auths(); + client.initialize(&issuer); + client.add_issuer(&issuer); + client.issue_certificate(&id, &issuer, &owner, &original_uri, &None); + + let result = client.try_update_metadata_uri(&id, &empty_uri); + assert!(result.is_err(), "Empty metadata_uri should be rejected"); +} + #[test] fn test_batch_verify_certificates_partial_failure_and_cost() { fn test_certificate_transfer_flow() { diff --git a/stellar-contracts/src/types.rs b/stellar-contracts/src/types.rs index d7079670b..bd2f4e374 100644 --- a/stellar-contracts/src/types.rs +++ b/stellar-contracts/src/types.rs @@ -50,6 +50,15 @@ pub struct CertificateRevokedEvent { pub reason: String, } +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MetadataUriUpdatedEvent { + pub id: String, + pub issuer: Address, + pub old_uri: String, + pub new_uri: String, +} + // Multisig Types #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/stellar-contracts/src/update_metadata_uri_test.rs b/stellar-contracts/src/update_metadata_uri_test.rs new file mode 100644 index 000000000..6bf5a5cf5 --- /dev/null +++ b/stellar-contracts/src/update_metadata_uri_test.rs @@ -0,0 +1,128 @@ +#![cfg(test)] + +use super::*; +use soroban_sdk::{testutils::Address as _, Address, Env, String}; + +fn setup(env: &Env) -> (CertificateContractClient, Address) { + let contract_id = env.register_contract(None, CertificateContract); + let client = CertificateContractClient::new(env, &contract_id); + let admin = Address::generate(env); + env.mock_all_auths(); + client.initialize(&admin); + (client, admin) +} + +fn issue( + env: &Env, + client: &CertificateContractClient, + id: &str, + issuer: &Address, + owner: &Address, + uri: &str, +) { + client.add_issuer(issuer); + client.issue_certificate( + &String::from_str(env, id), + issuer, + owner, + &String::from_str(env, uri), + &None, + ); +} + +// --------------------------------------------------------------------------- +// Happy path: original issuer updates the URI +// --------------------------------------------------------------------------- +#[test] +fn test_update_metadata_uri_by_original_issuer() { + let env = Env::default(); + let (client, _admin) = setup(&env); + + let issuer = Address::generate(&env); + let owner = Address::generate(&env); + let id = String::from_str(&env, "cert-meta-1"); + let new_uri = String::from_str(&env, "ipfs://QmMigrated"); + + issue(&env, &client, "cert-meta-1", &issuer, &owner, "ipfs://QmOriginal"); + + client.update_metadata_uri(&id, &new_uri); + + let cert = client.get_certificate(&id).expect("Certificate not found"); + assert_eq!(cert.metadata_uri, new_uri); +} + +// --------------------------------------------------------------------------- +// Auth failure: caller is NOT the original issuer +// --------------------------------------------------------------------------- +#[test] +fn test_update_metadata_uri_rejected_for_non_issuer() { + let env = Env::default(); + let contract_id = env.register_contract(None, CertificateContract); + let client = CertificateContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let issuer = Address::generate(&env); + let owner = Address::generate(&env); + let other = Address::generate(&env); + let id = String::from_str(&env, "cert-meta-2"); + let new_uri = String::from_str(&env, "ipfs://QmAttacker"); + + env.mock_all_auths(); + client.initialize(&admin); + issue(&env, &client, "cert-meta-2", &issuer, &owner, "ipfs://QmOriginal"); + + // Allow only `other` to authorise — issuer.require_auth() must fail + env.mock_auths(&[soroban_sdk::testutils::MockAuth { + address: &other, + invoke: &soroban_sdk::testutils::MockAuthInvoke { + contract: &contract_id, + fn_name: "update_metadata_uri", + args: soroban_sdk::vec![&env], + sub_invokes: &[], + }, + }]); + + let result = client.try_update_metadata_uri(&id, &new_uri); + assert!( + result.is_err(), + "Non-issuer should not be able to update metadata_uri" + ); +} + +// --------------------------------------------------------------------------- +// Certificate does not exist +// --------------------------------------------------------------------------- +#[test] +fn test_update_metadata_uri_certificate_not_found() { + let env = Env::default(); + let (client, _admin) = setup(&env); + + let missing_id = String::from_str(&env, "cert-does-not-exist"); + let new_uri = String::from_str(&env, "ipfs://QmAnything"); + + let result = client.try_update_metadata_uri(&missing_id, &new_uri); + assert!( + result.is_err(), + "Should return an error when certificate is not found" + ); +} + +// --------------------------------------------------------------------------- +// Empty URI must be rejected +// --------------------------------------------------------------------------- +#[test] +fn test_update_metadata_uri_rejects_empty_uri() { + let env = Env::default(); + let (client, _admin) = setup(&env); + + let issuer = Address::generate(&env); + let owner = Address::generate(&env); + let id = String::from_str(&env, "cert-meta-empty"); + + issue(&env, &client, "cert-meta-empty", &issuer, &owner, "ipfs://QmOriginal"); + + let result = client.try_update_metadata_uri(&id, &String::from_str(&env, "")); + assert!( + result.is_err(), + "Empty metadata_uri should be rejected" + ); +}