diff --git a/tests/integration/full_flow.rs b/tests/integration/full_flow.rs index 95d7bdd..ba17ca1 100644 --- a/tests/integration/full_flow.rs +++ b/tests/integration/full_flow.rs @@ -1,12 +1,12 @@ //! Full flow integration tests for cross-contract interactions. mod fixtures; -use fixtures::{setup_chainlearn_env, create_sample_course, complete_full_course}; +use fixtures::{complete_full_course, create_sample_course, setup_chainlearn_env}; -use learn_token::LearnTokenClient; use credential_nft::CredentialNftClient; +use learn_token::LearnTokenClient; use progress_tracker::ProgressTrackerClient; -use soroban_sdk::Symbol; +use soroban_sdk::{testutils::Address as _, Address, Symbol}; #[test] fn test_full_learner_journey() { @@ -14,7 +14,7 @@ fn test_full_learner_journey() { let env = env_context.env; let learner = env_context.learner; let _admin = env_context.admin; - + let token_client = LearnTokenClient::new(&env, &env_context.token_contract_id); let credential_client = CredentialNftClient::new(&env, &env_context.credential_contract_id); let progress_client = ProgressTrackerClient::new(&env, &env_context.progress_contract_id); @@ -23,7 +23,7 @@ fn test_full_learner_journey() { // 1. Create a course and enroll the learner let course_id = create_sample_course(&env, &progress_client); - + // 2. Complete the full course (enrolls, completes modules, submits quizzes) complete_full_course(&env, &learner, &course_id, &progress_client); @@ -54,3 +54,154 @@ fn test_full_learner_journey() { assert_eq!(info.score, 80); assert!(!info.revoked); } + +// ── Concurrent Credential and Reward Claim Integration Tests (#640) ──── + +/// Scope 1 & 4: Test claim_reward then mint_credential in sequence, +/// verifying that token reward claiming does not disrupt subsequent credential minting +/// and that token total_supply and user balance remain strictly consistent. +#[test] +fn test_claim_reward_then_mint_credential_sequence() { + let env_context = setup_chainlearn_env(); + let env = env_context.env; + let learner = env_context.learner; + + let token_client = LearnTokenClient::new(&env, &env_context.token_contract_id); + let credential_client = CredentialNftClient::new(&env, &env_context.credential_contract_id); + let progress_client = ProgressTrackerClient::new(&env, &env_context.progress_contract_id); + + env.mock_all_auths(); + + let course_id = create_sample_course(&env, &progress_client); + complete_full_course(&env, &learner, &course_id, &progress_client); + + // Initial state: 0 balance and 0 supply + assert_eq!(token_client.balance(&learner), 0); + assert_eq!(token_client.total_supply(), 0); + + // Step 1: Claim rewards first + token_client.claim_reward(&learner, &course_id, &Symbol::new(&env, "quiz_midterm")); + token_client.claim_reward(&learner, &course_id, &Symbol::new(&env, "quiz_final")); + + let expected_rewards = 85 * 100 + 75 * 100; // 16,000 + assert_eq!(token_client.balance(&learner), expected_rewards); + assert_eq!(token_client.total_supply(), expected_rewards); + + // Step 2: Mint credential after rewards are already claimed + let metadata_uri = Symbol::new(&env, "ipfs_hash_seq_1"); + let cred_id = credential_client.mint_credential(&learner, &course_id, &80, &metadata_uri); + + // Verify credential was successfully issued + let cred_info = credential_client.verify_credential(&cred_id); + assert_eq!(cred_info.learner, learner); + assert_eq!(cred_info.course_id, course_id); + assert_eq!(cred_info.score, 80); + assert!(!cred_info.revoked); + + // Step 3: Verify supply and balance consistency post-credential minting + assert_eq!(token_client.balance(&learner), expected_rewards); + assert_eq!(token_client.total_supply(), expected_rewards); +} + +/// Scope 2 & 4: Test mint_credential then claim_reward in reverse sequence, +/// verifying that minting a credential does not invalidate course progress +/// or block subsequent reward claiming. +#[test] +fn test_mint_credential_then_claim_reward_reverse_sequence() { + let env_context = setup_chainlearn_env(); + let env = env_context.env; + let learner = env_context.learner; + + let token_client = LearnTokenClient::new(&env, &env_context.token_contract_id); + let credential_client = CredentialNftClient::new(&env, &env_context.credential_contract_id); + let progress_client = ProgressTrackerClient::new(&env, &env_context.progress_contract_id); + + env.mock_all_auths(); + + let course_id = create_sample_course(&env, &progress_client); + complete_full_course(&env, &learner, &course_id, &progress_client); + + // Initial state check + assert_eq!(token_client.balance(&learner), 0); + assert_eq!(token_client.total_supply(), 0); + + // Step 1: Mint credential first + let metadata_uri = Symbol::new(&env, "ipfs_hash_rev_1"); + let cred_id = credential_client.mint_credential(&learner, &course_id, &80, &metadata_uri); + + let cred_info = credential_client.verify_credential(&cred_id); + assert_eq!(cred_info.learner, learner); + assert_eq!(cred_info.course_id, course_id); + assert_eq!(cred_info.score, 80); + + // Token balance is still 0 before claiming + assert_eq!(token_client.balance(&learner), 0); + assert_eq!(token_client.total_supply(), 0); + + // Step 2: Claim rewards after credential has already been minted + token_client.claim_reward(&learner, &course_id, &Symbol::new(&env, "quiz_midterm")); + token_client.claim_reward(&learner, &course_id, &Symbol::new(&env, "quiz_final")); + + let expected_rewards = 85 * 100 + 75 * 100; // 16,000 + assert_eq!(token_client.balance(&learner), expected_rewards); + assert_eq!(token_client.total_supply(), expected_rewards); + + // Step 3: Verify credential remains valid and untouched + let cred_info_after = credential_client.verify_credential(&cred_id); + assert_eq!(cred_info_after.score, 80); + assert!(!cred_info_after.revoked); +} + +/// Scope 3: Test interleaved / concurrent operations in the same session without state reset, +/// simulating multiple learners performing claims and mints concurrently. +#[test] +fn test_concurrent_multi_learner_claim_and_mint_same_session() { + let env_context = setup_chainlearn_env(); + let env = env_context.env; + let learner1 = env_context.learner; + let learner2 = Address::generate(&env); + + let token_client = LearnTokenClient::new(&env, &env_context.token_contract_id); + let credential_client = CredentialNftClient::new(&env, &env_context.credential_contract_id); + let progress_client = ProgressTrackerClient::new(&env, &env_context.progress_contract_id); + + env.mock_all_auths(); + + let course_id = create_sample_course(&env, &progress_client); + complete_full_course(&env, &learner1, &course_id, &progress_client); + complete_full_course(&env, &learner2, &course_id, &progress_client); + + // Interleaved execution in same session: + // 1. Learner 1 claims midterm reward + token_client.claim_reward(&learner1, &course_id, &Symbol::new(&env, "quiz_midterm")); + assert_eq!(token_client.balance(&learner1), 8500); + + // 2. Learner 2 mints credential before claiming any rewards + let uri2 = Symbol::new(&env, "ipfs_hash_learner2"); + let cred2_id = credential_client.mint_credential(&learner2, &course_id, &80, &uri2); + + // 3. Learner 1 mints credential + let uri1 = Symbol::new(&env, "ipfs_hash_learner1"); + let cred1_id = credential_client.mint_credential(&learner1, &course_id, &80, &uri1); + + // 4. Learner 2 claims both midterm and final rewards + token_client.claim_reward(&learner2, &course_id, &Symbol::new(&env, "quiz_midterm")); + token_client.claim_reward(&learner2, &course_id, &Symbol::new(&env, "quiz_final")); + + // 5. Learner 1 claims final reward + token_client.claim_reward(&learner1, &course_id, &Symbol::new(&env, "quiz_final")); + + // Verify final state consistency: + assert_eq!(token_client.balance(&learner1), 16_000); + assert_eq!(token_client.balance(&learner2), 16_000); + assert_eq!(token_client.total_supply(), 32_000); + + // Both credentials must exist, be distinct, and belong to the respective learners + assert_ne!(cred1_id, cred2_id); + let info1 = credential_client.verify_credential(&cred1_id); + let info2 = credential_client.verify_credential(&cred2_id); + assert_eq!(info1.learner, learner1); + assert_eq!(info2.learner, learner2); + assert!(!info1.revoked); + assert!(!info2.revoked); +}