diff --git a/contracts/src/profile_nft.rs b/contracts/src/profile_nft.rs index 49fa9628..6e34a9dd 100644 --- a/contracts/src/profile_nft.rs +++ b/contracts/src/profile_nft.rs @@ -6,10 +6,10 @@ use crate::utils::pause::PauseUtils; use crate::utils::storage::StorageUtils; use crate::utils::validation::{ - validate_non_zero_address, validate_string_length, - MAX_DESCRIPTION_LENGTH, MAX_SHORT_TEXT_LENGTH, MAX_TITLE_LENGTH, MAX_URI_LENGTH, + validate_non_zero_address, validate_string_length, MAX_DESCRIPTION_LENGTH, MAX_TITLE_LENGTH, + MAX_URI_LENGTH, }; -use soroban_sdk::{contracttype, symbol_short, Address, Env, String, Symbol, Vec}; +use soroban_sdk::{contracttype, symbol_short, Address, Env, String, Vec}; /// Maximum number of skills per profile NFT pub const MAX_SKILLS: u32 = 20; @@ -192,18 +192,14 @@ pub fn get_profile_nft(env: &Env, token_id: u64) -> ProfileNFT { env.storage() .persistent() .get(&ProfileNFTKey::Token(token_id)) - .unwrap_or_else(|| panic!("Profile NFT not found")); + .unwrap_or_else(|| panic!("Profile NFT not found")) } /// Get a profile NFT by owner address. /// /// Returns `None` if the owner has not minted a profile NFT yet. pub fn get_profile_nft_by_owner(env: &Env, owner: Address) -> Option { - if let Some(token_id) = get_token_id_for_owner(env, owner) { - Some(get_profile_nft(env, token_id)) - } else { - None - } + get_token_id_for_owner(env, owner).map(|token_id| get_profile_nft(env, token_id)) } /// Get the token ID for an owner, if one exists. @@ -218,7 +214,7 @@ pub fn owner_of(env: &Env, token_id: u64) -> Address { env.storage() .persistent() .get(&ProfileNFTKey::TokenOwner(token_id)) - .unwrap_or_else(|| panic!("Profile NFT not found")); + .unwrap_or_else(|| panic!("Profile NFT not found")) } /// Check whether a profile NFT exists for a given token ID. @@ -298,7 +294,7 @@ pub fn unverify_profile_nft(env: &Env, admin: Address, token_id: u64) -> bool { .set(&ProfileNFTKey::Token(token_id), &nft); env.events().publish( - (symbol_short!("profile"), symbol_short!("unverified")), + (symbol_short!("profile"), symbol_short!("unverify")), (token_id, admin), ); diff --git a/contracts/src/profile_nft_test.rs b/contracts/src/profile_nft_test.rs index f04cc576..659b11ae 100644 --- a/contracts/src/profile_nft_test.rs +++ b/contracts/src/profile_nft_test.rs @@ -1,398 +1,459 @@ #[cfg(test)] mod profile_nft_test { -use soroban_sdk::testutils::{Address as _, Ledger}; -use soroban_sdk::{vec, Address, Env, String}; + use soroban_sdk::testutils::{Address as _, Ledger}; + use soroban_sdk::{vec, Address, Env, String}; -use crate::profile_nft::*; -use crate::utils::pause::PauseUtils; + use crate::profile_nft::*; + use crate::AetherMintContract; // ── Helpers ───────────────────────────────────────────────────────────── - fn setup() -> (Env, Address) { + fn setup() -> (Env, Address, Address) { let env = Env::default(); env.mock_all_auths(); + // Set a non-zero ledger timestamp so minted_at > 0 assertions pass + let mut ledger_info = env.ledger().get(); + ledger_info.timestamp = 1700000000; + env.ledger().set(ledger_info); + + let cid = env.register(AetherMintContract, ()); let user = Address::generate(&env); - (env, user) + (env, cid, user) } // ── Mint Tests ─────────────────────────────────────────────────────────── #[test] fn test_mint_profile_nft() { - let (env, user) = setup(); - - let name = String::from_str(&env, "Alice"); - let bio = String::from_str(&env, "Blockchain developer"); - let avatar = String::from_str(&env, "ipfs://QmAvatarHash"); - let skills = vec![ - &env, - String::from_str(&env, "Rust"), - String::from_str(&env, "Soroban"), - String::from_str(&env, "Solidity"), - ]; - let website = Some(String::from_str(&env, "https://alice.dev")); - - let token_id = mint_profile_nft( - &env, - user.clone(), - name.clone(), - bio.clone(), - avatar.clone(), - skills.clone(), - website.clone(), - ); - - assert_eq!(token_id, 1); - - let nft = get_profile_nft(&env, token_id); - assert_eq!(nft.owner, user); - assert_eq!(nft.name, name); - assert_eq!(nft.bio, bio); - assert_eq!(nft.avatar_url, avatar); - assert_eq!(nft.skills.len(), 3); - assert_eq!(nft.website, website); - assert!(!nft.verified); - assert_eq!(nft.schema_version, 1); - assert!(nft.minted_at > 0); - assert_eq!(nft.minted_at, nft.updated_at); + let (env, cid, user) = setup(); + + env.as_contract(&cid, || { + let name = String::from_str(&env, "Alice"); + let bio = String::from_str(&env, "Blockchain developer"); + let avatar = String::from_str(&env, "ipfs://QmAvatarHash"); + let skills = vec![ + &env, + String::from_str(&env, "Rust"), + String::from_str(&env, "Soroban"), + String::from_str(&env, "Solidity"), + ]; + let website = Some(String::from_str(&env, "https://alice.dev")); + + let token_id = mint_profile_nft( + &env, + user.clone(), + name.clone(), + bio.clone(), + avatar.clone(), + skills.clone(), + website.clone(), + ); + + assert_eq!(token_id, 1); + + let nft = get_profile_nft(&env, token_id); + assert_eq!(nft.owner, user); + assert_eq!(nft.name, name); + assert_eq!(nft.bio, bio); + assert_eq!(nft.avatar_url, avatar); + assert_eq!(nft.skills.len(), 3); + assert_eq!(nft.website, website); + assert!(!nft.verified); + assert_eq!(nft.schema_version, 1); + assert!(nft.minted_at > 0); + assert_eq!(nft.minted_at, nft.updated_at); + }); } #[test] + #[should_panic(expected = "Profile NFT already exists")] fn test_cannot_mint_duplicate() { - let (env, user) = setup(); - - let name = String::from_str(&env, "Alice"); - let bio = String::from_str(&env, "Bio"); - let avatar = String::from_str(&env, "ipfs://avatar"); - let skills = vec![&env, String::from_str(&env, "Rust")]; - - mint_profile_nft( - &env, - user.clone(), - name.clone(), - bio.clone(), - avatar.clone(), - skills.clone(), - None, - ); - - // Second mint should panic - mint_profile_nft( - &env, - user.clone(), - name.clone(), - bio.clone(), - avatar.clone(), - skills.clone(), - None, - ); + let (env, cid, user) = setup(); + + // First mint in its own contract frame (one require_auth) + env.as_contract(&cid, || { + let name = String::from_str(&env, "Alice"); + let bio = String::from_str(&env, "Bio"); + let avatar = String::from_str(&env, "ipfs://avatar"); + let skills = vec![&env, String::from_str(&env, "Rust")]; + + mint_profile_nft(&env, user.clone(), name, bio, avatar, skills, None); + }); + + // Second mint in a separate frame — should panic with duplicate error + env.as_contract(&cid, || { + let name = String::from_str(&env, "Alice"); + let bio = String::from_str(&env, "Bio"); + let avatar = String::from_str(&env, "ipfs://avatar"); + let skills = vec![&env, String::from_str(&env, "Rust")]; + + mint_profile_nft(&env, user.clone(), name, bio, avatar, skills, None); + }); } // ── Update Tests ───────────────────────────────────────────────────────── #[test] fn test_update_profile_nft() { - let (env, user) = setup(); - - // Mint first - let name = String::from_str(&env, "Alice"); - let bio = String::from_str(&env, "Original bio"); - let avatar = String::from_str(&env, "ipfs://old"); - let skills = vec![&env, String::from_str(&env, "Rust")]; - - let token_id = mint_profile_nft( - &env, - user.clone(), - name.clone(), - bio.clone(), - avatar.clone(), - skills.clone(), - None, - ); - - // Advance ledger timestamp - let info = env.ledger().get(); - env.ledger().set(soroban_sdk::testutils::LedgerInfo { - timestamp: info.timestamp + 3600, - ..info + let (env, cid, user) = setup(); + + // Mint first (requires user auth) + let mut token_id = 0; + let mut new_name = String::from_str(&env, ""); + let mut new_bio = String::from_str(&env, ""); + let mut new_avatar = String::from_str(&env, ""); + let mut new_skills = vec![&env]; + let mut new_website: Option = None; + env.as_contract(&cid, || { + let name = String::from_str(&env, "Alice"); + let bio = String::from_str(&env, "Original bio"); + let avatar = String::from_str(&env, "ipfs://old"); + let skills = vec![&env, String::from_str(&env, "Rust")]; + + token_id = mint_profile_nft( + &env, + user.clone(), + name.clone(), + bio.clone(), + avatar.clone(), + skills.clone(), + None, + ); + + // Advance ledger timestamp + let info = env.ledger().get(); + env.ledger().set(soroban_sdk::testutils::LedgerInfo { + timestamp: info.timestamp + 3600, + ..info + }); + + // Prepare update values (they're String/Vec which are moved out of closure) + new_name = String::from_str(&env, "Alice Updated"); + new_bio = String::from_str(&env, "Updated bio"); + new_avatar = String::from_str(&env, "ipfs://new"); + new_skills = vec![ + &env, + String::from_str(&env, "Rust"), + String::from_str(&env, "Go"), + ]; + new_website = Some(String::from_str(&env, "https://alice-updated.dev")); }); - // Update - let new_name = String::from_str(&env, "Alice Updated"); - let new_bio = String::from_str(&env, "Updated bio"); - let new_avatar = String::from_str(&env, "ipfs://new"); - let new_skills = vec![ - &env, - String::from_str(&env, "Rust"), - String::from_str(&env, "Go"), - ]; - let new_website = Some(String::from_str(&env, "https://alice-updated.dev")); - - let result = update_profile_nft( - &env, - user.clone(), - new_name.clone(), - new_bio.clone(), - new_avatar.clone(), - new_skills.clone(), - new_website.clone(), - ); - - assert!(result); - - let nft = get_profile_nft(&env, token_id); - assert_eq!(nft.name, new_name); - assert_eq!(nft.bio, new_bio); - assert_eq!(nft.avatar_url, new_avatar); - assert_eq!(nft.skills.len(), 2); - assert_eq!(nft.website, new_website); - assert!(nft.updated_at > nft.minted_at); + // Update in a separate frame (requires user auth again) + env.as_contract(&cid, || { + let result = update_profile_nft( + &env, + user.clone(), + new_name.clone(), + new_bio.clone(), + new_avatar.clone(), + new_skills.clone(), + new_website.clone(), + ); + + assert!(result); + + let nft = get_profile_nft(&env, token_id); + assert_eq!(nft.name, new_name); + assert_eq!(nft.bio, new_bio); + assert_eq!(nft.avatar_url, new_avatar); + assert_eq!(nft.skills.len(), 2); + assert_eq!(nft.website, new_website); + assert!(nft.updated_at > nft.minted_at); + }); } #[test] #[should_panic(expected = "No profile NFT found")] fn test_cannot_update_nonexistent() { - let (env, user) = setup(); - - update_profile_nft( - &env, - user.clone(), - String::from_str(&env, "Alice"), - String::from_str(&env, "Bio"), - String::from_str(&env, "ipfs://avatar"), - vec![&env, String::from_str(&env, "Rust")], - None, - ); + let (env, cid, user) = setup(); + + env.as_contract(&cid, || { + update_profile_nft( + &env, + user.clone(), + String::from_str(&env, "Alice"), + String::from_str(&env, "Bio"), + String::from_str(&env, "ipfs://avatar"), + vec![&env, String::from_str(&env, "Rust")], + None, + ); + }); } // ── Getter Tests ───────────────────────────────────────────────────────── #[test] fn test_get_profile_nft_by_owner() { - let (env, user) = setup(); - - let name = String::from_str(&env, "Alice"); - let bio = String::from_str(&env, "Bio"); - let avatar = String::from_str(&env, "ipfs://avatar"); - let skills = vec![&env, String::from_str(&env, "Rust")]; - - mint_profile_nft( - &env, - user.clone(), - name, - bio, - avatar, - skills, - None, - ); - - let nft = get_profile_nft_by_owner(&env, user.clone()).unwrap(); - assert_eq!(nft.owner, user); + let (env, cid, user) = setup(); + + env.as_contract(&cid, || { + let name = String::from_str(&env, "Alice"); + let bio = String::from_str(&env, "Bio"); + let avatar = String::from_str(&env, "ipfs://avatar"); + let skills = vec![&env, String::from_str(&env, "Rust")]; + + mint_profile_nft(&env, user.clone(), name, bio, avatar, skills, None); + + let nft = get_profile_nft_by_owner(&env, user.clone()).unwrap(); + assert_eq!(nft.owner, user); + }); } #[test] fn test_get_profile_nft_by_owner_not_found() { - let (env, user) = setup(); + let (env, cid, user) = setup(); - let result = get_profile_nft_by_owner(&env, user.clone()); - assert!(result.is_none()); + env.as_contract(&cid, || { + let result = get_profile_nft_by_owner(&env, user.clone()); + assert!(result.is_none()); + }); } #[test] fn test_has_profile_nft() { - let (env, user) = setup(); - - assert!(!has_profile_nft(&env, user.clone())); - - mint_profile_nft( - &env, - user.clone(), - String::from_str(&env, "Alice"), - String::from_str(&env, "Bio"), - String::from_str(&env, "ipfs://avatar"), - vec![&env, String::from_str(&env, "Rust")], - None, - ); - - assert!(has_profile_nft(&env, user.clone())); + let (env, cid, user) = setup(); + + env.as_contract(&cid, || { + assert!(!has_profile_nft(&env, user.clone())); + + mint_profile_nft( + &env, + user.clone(), + String::from_str(&env, "Alice"), + String::from_str(&env, "Bio"), + String::from_str(&env, "ipfs://avatar"), + vec![&env, String::from_str(&env, "Rust")], + None, + ); + + assert!(has_profile_nft(&env, user.clone())); + }); } #[test] fn test_profile_nft_exists() { - let (env, user) = setup(); - - assert!(!profile_nft_exists(&env, 1)); - - mint_profile_nft( - &env, - user.clone(), - String::from_str(&env, "Alice"), - String::from_str(&env, "Bio"), - String::from_str(&env, "ipfs://avatar"), - vec![&env], - None, - ); - - assert!(profile_nft_exists(&env, 1)); - assert!(!profile_nft_exists(&env, 999)); + let (env, cid, user) = setup(); + + env.as_contract(&cid, || { + assert!(!profile_nft_exists(&env, 1)); + + mint_profile_nft( + &env, + user.clone(), + String::from_str(&env, "Alice"), + String::from_str(&env, "Bio"), + String::from_str(&env, "ipfs://avatar"), + vec![&env], + None, + ); + + assert!(profile_nft_exists(&env, 1)); + assert!(!profile_nft_exists(&env, 999)); + }); } // ── Burn Tests ─────────────────────────────────────────────────────────── #[test] fn test_burn_profile_nft() { - let (env, user) = setup(); - - mint_profile_nft( - &env, - user.clone(), - String::from_str(&env, "Alice"), - String::from_str(&env, "Bio"), - String::from_str(&env, "ipfs://avatar"), - vec![&env], - None, - ); - - assert!(has_profile_nft(&env, user.clone())); + let (env, cid, user) = setup(); + + // Mint (requires user auth) + env.as_contract(&cid, || { + mint_profile_nft( + &env, + user.clone(), + String::from_str(&env, "Alice"), + String::from_str(&env, "Bio"), + String::from_str(&env, "ipfs://avatar"), + vec![&env], + None, + ); + + assert!(has_profile_nft(&env, user.clone())); + }); - let result = burn_profile_nft(&env, user.clone()); - assert!(result); + // Burn in a separate frame (requires user auth again) + env.as_contract(&cid, || { + let result = burn_profile_nft(&env, user.clone()); + assert!(result); - assert!(!has_profile_nft(&env, user.clone())); - assert!(!profile_nft_exists(&env, 1)); + assert!(!has_profile_nft(&env, user.clone())); + assert!(!profile_nft_exists(&env, 1)); + }); } #[test] #[should_panic(expected = "No profile NFT found")] fn test_cannot_burn_nonexistent() { - let (env, user) = setup(); + let (env, cid, user) = setup(); - burn_profile_nft(&env, user.clone()); + env.as_contract(&cid, || { + burn_profile_nft(&env, user.clone()); + }); } // ── Verify / Unverify Tests ────────────────────────────────────────────── #[test] fn test_verify_profile_nft() { - let (env, user) = setup(); + let (env, cid, user) = setup(); let admin = Address::generate(&env); - let token_id = mint_profile_nft( - &env, - user.clone(), - String::from_str(&env, "Alice"), - String::from_str(&env, "Bio"), - String::from_str(&env, "ipfs://avatar"), - vec![&env], - None, - ); - - assert!(!get_profile_nft(&env, token_id).verified); + // Mint (requires user auth) + let mut token_id = 0; + env.as_contract(&cid, || { + token_id = mint_profile_nft( + &env, + user.clone(), + String::from_str(&env, "Alice"), + String::from_str(&env, "Bio"), + String::from_str(&env, "ipfs://avatar"), + vec![&env], + None, + ); + + assert!(!get_profile_nft(&env, token_id).verified); + }); - let result = verify_profile_nft(&env, admin.clone(), token_id); - assert!(result); + // Verify in a separate frame (requires admin auth) + env.as_contract(&cid, || { + let result = verify_profile_nft(&env, admin.clone(), token_id); + assert!(result); - assert!(get_profile_nft(&env, token_id).verified); + assert!(get_profile_nft(&env, token_id).verified); + }); } #[test] fn test_unverify_profile_nft() { - let (env, user) = setup(); + let (env, cid, user) = setup(); let admin = Address::generate(&env); - let token_id = mint_profile_nft( - &env, - user.clone(), - String::from_str(&env, "Alice"), - String::from_str(&env, "Bio"), - String::from_str(&env, "ipfs://avatar"), - vec![&env], - None, - ); + // Mint (user auth) + let mut token_id = 0; + env.as_contract(&cid, || { + token_id = mint_profile_nft( + &env, + user.clone(), + String::from_str(&env, "Alice"), + String::from_str(&env, "Bio"), + String::from_str(&env, "ipfs://avatar"), + vec![&env], + None, + ); + }); - verify_profile_nft(&env, admin.clone(), token_id); - assert!(get_profile_nft(&env, token_id).verified); + // Verify (admin auth) + env.as_contract(&cid, || { + verify_profile_nft(&env, admin.clone(), token_id); + assert!(get_profile_nft(&env, token_id).verified); + }); - let result = unverify_profile_nft(&env, admin.clone(), token_id); - assert!(result); + // Unverify in a separate frame (admin auth again) + env.as_contract(&cid, || { + let result = unverify_profile_nft(&env, admin.clone(), token_id); + assert!(result); - assert!(!get_profile_nft(&env, token_id).verified); + assert!(!get_profile_nft(&env, token_id).verified); + }); } // ── Supply & Pagination Tests ──────────────────────────────────────────── #[test] fn test_get_total_supply() { - let (env, user1) = setup(); + let (env, cid, user1) = setup(); let user2 = Address::generate(&env); - assert_eq!(get_total_supply(&env), 0); - - mint_profile_nft( - &env, - user1.clone(), - String::from_str(&env, "Alice"), - String::from_str(&env, "Bio1"), - String::from_str(&env, "ipfs://a"), - vec![&env], - None, - ); - - assert_eq!(get_total_supply(&env), 1); - - mint_profile_nft( - &env, - user2.clone(), - String::from_str(&env, "Bob"), - String::from_str(&env, "Bio2"), - String::from_str(&env, "ipfs://b"), - vec![&env], - None, - ); - - assert_eq!(get_total_supply(&env), 2); + // Mint user1 (user1 auth) + env.as_contract(&cid, || { + assert_eq!(get_total_supply(&env), 0); + + mint_profile_nft( + &env, + user1.clone(), + String::from_str(&env, "Alice"), + String::from_str(&env, "Bio1"), + String::from_str(&env, "ipfs://a"), + vec![&env], + None, + ); + + assert_eq!(get_total_supply(&env), 1); + }); + + // Mint user2 (user2 auth — different user, separate frame) + env.as_contract(&cid, || { + mint_profile_nft( + &env, + user2.clone(), + String::from_str(&env, "Bob"), + String::from_str(&env, "Bio2"), + String::from_str(&env, "ipfs://b"), + vec![&env], + None, + ); + + assert_eq!(get_total_supply(&env), 2); + }); } #[test] fn test_get_all_token_ids() { - let (env, user1) = setup(); + let (env, cid, user1) = setup(); let user2 = Address::generate(&env); let user3 = Address::generate(&env); - mint_profile_nft( - &env, - user1.clone(), - String::from_str(&env, "A"), - String::from_str(&env, "A"), - String::from_str(&env, "ipfs://a"), - vec![&env], - None, - ); - mint_profile_nft( - &env, - user2.clone(), - String::from_str(&env, "B"), - String::from_str(&env, "B"), - String::from_str(&env, "ipfs://b"), - vec![&env], - None, - ); - mint_profile_nft( - &env, - user3.clone(), - String::from_str(&env, "C"), - String::from_str(&env, "C"), - String::from_str(&env, "ipfs://c"), - vec![&env], - None, - ); - - let all = get_all_token_ids(&env, 0, 10); - assert_eq!(all.len(), 3); - - let page = get_all_token_ids(&env, 0, 2); - assert_eq!(page.len(), 2); + // Mint user1 + env.as_contract(&cid, || { + mint_profile_nft( + &env, + user1.clone(), + String::from_str(&env, "A"), + String::from_str(&env, "A"), + String::from_str(&env, "ipfs://a"), + vec![&env], + None, + ); + }); + + // Mint user2 + env.as_contract(&cid, || { + mint_profile_nft( + &env, + user2.clone(), + String::from_str(&env, "B"), + String::from_str(&env, "B"), + String::from_str(&env, "ipfs://b"), + vec![&env], + None, + ); + }); + + // Mint user3 + env.as_contract(&cid, || { + mint_profile_nft( + &env, + user3.clone(), + String::from_str(&env, "C"), + String::from_str(&env, "C"), + String::from_str(&env, "ipfs://c"), + vec![&env], + None, + ); + }); + + // Query all + env.as_contract(&cid, || { + let all = get_all_token_ids(&env, 0, 10); + assert_eq!(all.len(), 3); + + let page = get_all_token_ids(&env, 0, 2); + assert_eq!(page.len(), 2); + }); } // ── Skills Validation Tests ────────────────────────────────────────────── @@ -400,92 +461,102 @@ use crate::utils::pause::PauseUtils; #[test] #[should_panic(expected = "Too many skills")] fn test_too_many_skills() { - let (env, user) = setup(); - - let mut skills = vec![&env]; - for _ in 0..(MAX_SKILLS + 1) { - skills.push_back(String::from_str(&env, "skill")); - } - - mint_profile_nft( - &env, - user.clone(), - String::from_str(&env, "Alice"), - String::from_str(&env, "Bio"), - String::from_str(&env, "ipfs://avatar"), - skills, - None, - ); + let (env, cid, user) = setup(); + + env.as_contract(&cid, || { + let mut skills = vec![&env]; + for _ in 0..(MAX_SKILLS + 1) { + skills.push_back(String::from_str(&env, "skill")); + } + + mint_profile_nft( + &env, + user.clone(), + String::from_str(&env, "Alice"), + String::from_str(&env, "Bio"), + String::from_str(&env, "ipfs://avatar"), + skills, + None, + ); + }); } // ── Event Tests ────────────────────────────────────────────────────────── #[test] fn test_mint_emits_event() { - let (env, user) = setup(); - - let start = env.ledger().timestamp(); - - mint_profile_nft( - &env, - user.clone(), - String::from_str(&env, "Alice"), - String::from_str(&env, "Bio"), - String::from_str(&env, "ipfs://avatar"), - vec![&env], - None, - ); - - // Events are scoped to the contract, but our functions run outside - // the contract wrapper. For contract-level tests this would use - // env.as_contract(). In module-level unit tests we verify that - // no panics occurred and the state is consistent instead. - assert!(has_profile_nft(&env, user)); + let (env, cid, user) = setup(); + + env.as_contract(&cid, || { + mint_profile_nft( + &env, + user.clone(), + String::from_str(&env, "Alice"), + String::from_str(&env, "Bio"), + String::from_str(&env, "ipfs://avatar"), + vec![&env], + None, + ); + + assert!(has_profile_nft(&env, user)); + }); } #[test] fn test_update_emits_event() { - let (env, user) = setup(); - - mint_profile_nft( - &env, - user.clone(), - String::from_str(&env, "Alice"), - String::from_str(&env, "Bio"), - String::from_str(&env, "ipfs://avatar"), - vec![&env], - None, - ); - - let result = update_profile_nft( - &env, - user.clone(), - String::from_str(&env, "Alice V2"), - String::from_str(&env, "New Bio"), - String::from_str(&env, "ipfs://new"), - vec![&env], - None, - ); - - assert!(result); + let (env, cid, user) = setup(); + + // Mint (user auth) + env.as_contract(&cid, || { + mint_profile_nft( + &env, + user.clone(), + String::from_str(&env, "Alice"), + String::from_str(&env, "Bio"), + String::from_str(&env, "ipfs://avatar"), + vec![&env], + None, + ); + }); + + // Update in separate frame (user auth again) + env.as_contract(&cid, || { + let result = update_profile_nft( + &env, + user.clone(), + String::from_str(&env, "Alice V2"), + String::from_str(&env, "New Bio"), + String::from_str(&env, "ipfs://new"), + vec![&env], + None, + ); + + assert!(result); + }); } #[test] fn test_burn_emits_event() { - let (env, user) = setup(); - - mint_profile_nft( - &env, - user.clone(), - String::from_str(&env, "Alice"), - String::from_str(&env, "Bio"), - String::from_str(&env, "ipfs://avatar"), - vec![&env], - None, - ); - - let result = burn_profile_nft(&env, user.clone()); - assert!(result); - assert!(!has_profile_nft(&env, user)); + let (env, cid, user) = setup(); + + // Mint (user auth) + env.as_contract(&cid, || { + mint_profile_nft( + &env, + user.clone(), + String::from_str(&env, "Alice"), + String::from_str(&env, "Bio"), + String::from_str(&env, "ipfs://avatar"), + vec![&env], + None, + ); + }); + + // Burn in separate frame (user auth again) + env.as_contract(&cid, || { + let result = burn_profile_nft(&env, user.clone()); + assert!(result); + assert!(!has_profile_nft(&env, user)); + }); } } diff --git a/frontend/src/app/enroll/[courseId]/page.tsx b/frontend/src/app/enroll/[courseId]/page.tsx index 433dd836..d8c0c607 100644 --- a/frontend/src/app/enroll/[courseId]/page.tsx +++ b/frontend/src/app/enroll/[courseId]/page.tsx @@ -364,8 +364,8 @@ const EnrollmentPage: React.FC = () => { onEnrollmentError={handleEnrollmentError} /> - + ); }; diff --git a/frontend/src/components/CredentialList.tsx b/frontend/src/components/CredentialList.tsx index 4679755f..f016f81d 100644 --- a/frontend/src/components/CredentialList.tsx +++ b/frontend/src/components/CredentialList.tsx @@ -479,7 +479,7 @@ export function CredentialList({ )} - + ); } diff --git a/frontend/src/components/CredentialMarketplace.tsx b/frontend/src/components/CredentialMarketplace.tsx index 72bb5a38..b612b71e 100644 --- a/frontend/src/components/CredentialMarketplace.tsx +++ b/frontend/src/components/CredentialMarketplace.tsx @@ -16,58 +16,58 @@ const CredentialMarketplace = () => { ]; return ( -
-
-

MICRO-CREDENTIAL MARKETPLACE

-
- {categories.map(c => ( - - ))} + +
+
+

MICRO-CREDENTIAL MARKETPLACE

+
+ {categories.map(c => ( + + ))} +
-
-
- {mockCredentials.filter(c => filter === 'All' || c.category === filter).map(cred => ( -
-
-
-
🏅
+
+ {mockCredentials.filter(c => filter === 'All' || c.category === filter).map(cred => ( +
+
+
+
🏅
+
+
+ NFT / Stellar Asset +
-
- NFT / Stellar Asset + +
+

{cred.title}

+

Issuer: {cred.issuer}

-
-
-

{cred.title}

-

Issuer: {cred.issuer}

-
+
+
+

Dynamic Price

+ {cred.price} XLM +
+
+

Supply

+ {cred.supply} left +
+
-
-
-

Dynamic Price

- {cred.price} XLM -
-
-

Supply

- {cred.supply} left -
-
+
+ Trend: {cred.trend} (Last 24h) +
-
- Trend: {cred.trend} (Last 24h) + +
- - - -
- ))} -
+ ))} +
- - ); + ); }; diff --git a/frontend/src/components/EnrollmentForm.tsx b/frontend/src/components/EnrollmentForm.tsx index 4aa5faa5..e3ac84e2 100644 --- a/frontend/src/components/EnrollmentForm.tsx +++ b/frontend/src/components/EnrollmentForm.tsx @@ -410,6 +410,7 @@ const EnrollmentForm: React.FC = ({
+ ); }; @@ -578,7 +579,6 @@ const ConfirmationStep: React.FC = ({ course, wallet, personalInfo, transac Please review your information above and click "Complete Enrollment" to finalize your registration.

-
); }; diff --git a/frontend/src/components/Wallet/WalletConnector.tsx b/frontend/src/components/Wallet/WalletConnector.tsx index 1faa8681..0e44aa70 100644 --- a/frontend/src/components/Wallet/WalletConnector.tsx +++ b/frontend/src/components/Wallet/WalletConnector.tsx @@ -67,40 +67,38 @@ export const WalletConnector: React.FC = () => { )} {isConnecting ? 'Connecting...' : 'Connect Wallet'} - )} - - {isConnected && ( -
- -
-
- Select Network -
- - + )} {isConnected && ( +
+ +
+
+ Select Network
+ +
- )} +
+ )}
-
+ ); }; diff --git a/frontend/src/components/collaboration/CollaborationRoom.tsx b/frontend/src/components/collaboration/CollaborationRoom.tsx new file mode 100644 index 00000000..49f14edb --- /dev/null +++ b/frontend/src/components/collaboration/CollaborationRoom.tsx @@ -0,0 +1,46 @@ +'use client'; + +import React from 'react'; + +interface CollaborationRoomProps { + roomId: string; + userId: string; + username: string; + role: 'instructor' | 'student'; +} + +const CollaborationRoom: React.FC = ({ + roomId, + userId, + username, + role, +}) => { + return ( +
+
+
+

Collaboration Room

+

Room ID: {roomId}

+
+
+ + {username} ({role}) + + + Connected + +
+
+
+
+

Collaboration features coming soon

+

+ Real-time editing, whiteboard, and video chat will be available here. +

+
+
+
+ ); +}; + +export default CollaborationRoom;