Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 188 additions & 1 deletion tests/integration/full_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use fixtures::{setup_chainlearn_env, create_sample_course, complete_full_course}
use learn_token::LearnTokenClient;
use credential_nft::CredentialNftClient;
use progress_tracker::ProgressTrackerClient;
use soroban_sdk::Symbol;
use soroban_sdk::{testutils::Address as _, Address, Symbol};

#[test]
fn test_full_learner_journey() {
Expand Down Expand Up @@ -54,3 +54,190 @@ fn test_full_learner_journey() {
assert_eq!(info.score, 80);
assert!(!info.revoked);
}

#[test]
fn test_claim_reward_before_quiz_submission_fails() {
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 progress_client = ProgressTrackerClient::new(env, &env_context.progress_contract_id);

env.mock_all_auths();

let course_id = create_sample_course(env, &progress_client);
progress_client.enroll(learner, &course_id);

// No quiz score has been submitted yet, so the tracker cannot verify a
// score and the reward claim must fail without minting anything.
let result = token_client.try_claim_reward(
learner,
&course_id,
&Symbol::new(env, "quiz_midterm"),
);
assert!(result.is_err());
assert_eq!(token_client.balance(learner), 0);
assert_eq!(token_client.total_supply(), 0);
}

#[test]
fn test_mint_credential_before_completing_all_modules_fails() {
let env_context = setup_chainlearn_env();
let env = &env_context.env;
let learner = &env_context.learner;

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);
progress_client.enroll(learner, &course_id);

// Finish both quizzes but leave every module incomplete.
progress_client.submit_quiz_score(learner, &course_id, &Symbol::new(env, "quiz_midterm"), &85);
progress_client.submit_quiz_score(learner, &course_id, &Symbol::new(env, "quiz_final"), &75);

assert!(!progress_client.is_eligible_for_credential(learner, &course_id));

let result = credential_client.try_mint_credential(
learner,
&course_id,
&80,
&Symbol::new(env, "ipfs_QmCredential123"),
);
assert!(result.is_err());
assert_eq!(credential_client.get_credential_count(learner), 0);
}

#[test]
fn test_mint_credential_with_score_below_minimum_fails() {
let env_context = setup_chainlearn_env();
let env = &env_context.env;
let learner = &env_context.learner;

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);
progress_client.enroll(learner, &course_id);
progress_client.complete_module(learner, &course_id, &Symbol::new(env, "mod_basics"));
progress_client.complete_module(learner, &course_id, &Symbol::new(env, "mod_ownership"));
progress_client.complete_module(learner, &course_id, &Symbol::new(env, "mod_traits"));

// Average of 40 is below the 50-point credential threshold, so the
// learner never becomes eligible.
progress_client.submit_quiz_score(learner, &course_id, &Symbol::new(env, "quiz_midterm"), &40);
progress_client.submit_quiz_score(learner, &course_id, &Symbol::new(env, "quiz_final"), &40);
assert!(!progress_client.is_eligible_for_credential(learner, &course_id));

let result = credential_client.try_mint_credential(
learner,
&course_id,
&40,
&Symbol::new(env, "ipfs_QmCredential123"),
);
assert!(result.is_err());
assert_eq!(credential_client.get_credential_count(learner), 0);
}

#[test]
fn test_operations_blocked_during_pause() {
let env_context = setup_chainlearn_env();
let env = &env_context.env;
let admin = &env_context.admin;
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);

// Claim one reward before pausing so transfer and burn have a balance to
// operate on; leave the final quiz unclaimed for the paused claim check.
token_client.claim_reward(learner, &course_id, &Symbol::new(env, "quiz_midterm"));
assert_eq!(token_client.balance(learner), 8500);

token_client.pause(admin);
progress_client.emergency_pause();
credential_client.emergency_pause();

// Token operations are rejected while paused.
let recipient = Address::generate(env);
assert!(token_client.try_transfer(learner, &recipient, &1_000).is_err());
assert!(token_client.try_burn(learner, &1_000).is_err());
assert!(token_client
.try_claim_reward(learner, &course_id, &Symbol::new(env, "quiz_final"))
.is_err());

// Progress-tracker writes are rejected while paused.
assert!(progress_client
.try_retake_quiz(learner, &course_id, &Symbol::new(env, "quiz_midterm"), &90)
.is_err());

// Credential minting is rejected while paused even though the learner is
// otherwise fully eligible.
assert!(credential_client
.try_mint_credential(
learner,
&course_id,
&80,
&Symbol::new(env, "ipfs_QmCredential123"),
)
.is_err());

// No paused call changed state.
assert_eq!(token_client.balance(learner), 8500);
assert_eq!(token_client.total_supply(), 8500);
assert_eq!(token_client.balance(&recipient), 0);
assert_eq!(credential_client.get_credential_count(learner), 0);
}

#[test]
fn test_operations_restored_after_unpause() {
let env_context = setup_chainlearn_env();
let env = &env_context.env;
let admin = &env_context.admin;
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);

token_client.pause(admin);
progress_client.emergency_pause();
credential_client.emergency_pause();

token_client.unpause(admin);
progress_client.unpause();
credential_client.unpause();

// The full learner journey works again once every contract is unpaused.
complete_full_course(env, learner, &course_id, &progress_client);

token_client.claim_reward(learner, &course_id, &Symbol::new(env, "quiz_midterm"));
token_client.claim_reward(learner, &course_id, &Symbol::new(env, "quiz_final"));
assert_eq!(token_client.balance(learner), 16000);

let cred_id = credential_client.mint_credential(
learner,
&course_id,
&80,
&Symbol::new(env, "ipfs_QmCredential123"),
);
let info = credential_client.verify_credential(&cred_id);
assert_eq!(info.learner, *learner);
assert_eq!(info.score, 80);
assert_eq!(credential_client.get_credential_count(learner), 1);
}
170 changes: 169 additions & 1 deletion tests/integration/token_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ mod fixtures;

use learn_token::LearnTokenClient;
use progress_tracker::ProgressTrackerClient;
use soroban_sdk::{testutils::Address as _, Address, Env, Symbol, Vec};
use soroban_sdk::{
testutils::{Address as _, Ledger as _},
Address, Env, Symbol, Vec,
};

/// Create a minimal course (one module, just the given quiz ids) and enroll
/// `learner` in it. `claim_reward` only requires enrollment plus a submitted
Expand Down Expand Up @@ -151,3 +154,168 @@ fn test_total_supply_consistency() {

assert_eq!(token_client.total_supply(), 13000);
}

#[test]
fn test_transfer_from_after_approve() {
let setup = fixtures::setup_chainlearn_env();
let env = &setup.env;
let learner = &setup.learner;
let spender = Address::generate(env);
let recipient = Address::generate(env);
env.mock_all_auths();

let token_client = LearnTokenClient::new(env, &setup.token_contract_id);
let progress_client = ProgressTrackerClient::new(env, &setup.progress_contract_id);

let course_id = Symbol::new(env, "course_1");
let quiz_id = Symbol::new(env, "quiz_1");
setup_course_and_enroll(env, &progress_client, learner, &course_id, &[quiz_id.clone()]);
progress_client.submit_quiz_score(learner, &course_id, &quiz_id, &80);
token_client.claim_reward(learner, &course_id, &quiz_id); // 8000

token_client.approve(learner, &spender, &5_000, &999_999);

// The approved spender moves tokens on the learner's behalf.
token_client.transfer_from(&spender, learner, &recipient, &3_000);

assert_eq!(token_client.balance(learner), 5_000);
assert_eq!(token_client.balance(&recipient), 3_000);
assert_eq!(token_client.allowance(learner, &spender), 2_000);
assert_eq!(token_client.total_supply(), 8_000);
}

#[test]
fn test_transfer_from_deducts_allowance_correctly() {
let setup = fixtures::setup_chainlearn_env();
let env = &setup.env;
let learner = &setup.learner;
let spender = Address::generate(env);
let recipient = Address::generate(env);
env.mock_all_auths();

let token_client = LearnTokenClient::new(env, &setup.token_contract_id);
let progress_client = ProgressTrackerClient::new(env, &setup.progress_contract_id);

let course_id = Symbol::new(env, "course_1");
let quiz_id = Symbol::new(env, "quiz_1");
setup_course_and_enroll(env, &progress_client, learner, &course_id, &[quiz_id.clone()]);
progress_client.submit_quiz_score(learner, &course_id, &quiz_id, &100);
token_client.claim_reward(learner, &course_id, &quiz_id); // 10000

token_client.approve(learner, &spender, &6_000, &999_999);

// Each spend reduces the remaining allowance by exactly its amount.
token_client.transfer_from(&spender, learner, &recipient, &2_000);
assert_eq!(token_client.allowance(learner, &spender), 4_000);

token_client.transfer_from(&spender, learner, &recipient, &4_000);
assert_eq!(token_client.allowance(learner, &spender), 0);
assert_eq!(token_client.balance(learner), 4_000);
assert_eq!(token_client.balance(&recipient), 6_000);
}

#[test]
fn test_transfer_from_with_expired_allowance_fails() {
let setup = fixtures::setup_chainlearn_env();
let env = &setup.env;
let learner = &setup.learner;
let spender = Address::generate(env);
let recipient = Address::generate(env);
env.mock_all_auths();

let token_client = LearnTokenClient::new(env, &setup.token_contract_id);
let progress_client = ProgressTrackerClient::new(env, &setup.progress_contract_id);

let course_id = Symbol::new(env, "course_1");
let quiz_id = Symbol::new(env, "quiz_1");
setup_course_and_enroll(env, &progress_client, learner, &course_id, &[quiz_id.clone()]);
progress_client.submit_quiz_score(learner, &course_id, &quiz_id, &80);
token_client.claim_reward(learner, &course_id, &quiz_id); // 8000

token_client.approve(learner, &spender, &2_000, &10);
env.ledger().with_mut(|ledger| ledger.sequence_number = 20);

// Reading the expired allowance returns zero rather than the stale value.
assert_eq!(token_client.allowance(learner, &spender), 0);

let result = token_client.try_transfer_from(&spender, learner, &recipient, &1_000);
assert!(result.is_err());
assert_eq!(token_client.balance(learner), 8_000);
assert_eq!(token_client.balance(&recipient), 0);
assert_eq!(token_client.total_supply(), 8_000);
}

#[test]
fn test_burn_reduces_balance_and_total_supply() {
let setup = fixtures::setup_chainlearn_env();
let env = &setup.env;
let learner = &setup.learner;
env.mock_all_auths();

let token_client = LearnTokenClient::new(env, &setup.token_contract_id);
let progress_client = ProgressTrackerClient::new(env, &setup.progress_contract_id);

let course_id = Symbol::new(env, "course_1");
let quiz_id = Symbol::new(env, "quiz_1");
setup_course_and_enroll(env, &progress_client, learner, &course_id, &[quiz_id.clone()]);
progress_client.submit_quiz_score(learner, &course_id, &quiz_id, &80);
token_client.claim_reward(learner, &course_id, &quiz_id); // 8000

token_client.burn(learner, &3_000);

assert_eq!(token_client.balance(learner), 5_000);
assert_eq!(token_client.total_supply(), 5_000);
}

#[test]
fn test_burn_from_with_allowance() {
let setup = fixtures::setup_chainlearn_env();
let env = &setup.env;
let learner = &setup.learner;
let spender = Address::generate(env);
env.mock_all_auths();

let token_client = LearnTokenClient::new(env, &setup.token_contract_id);
let progress_client = ProgressTrackerClient::new(env, &setup.progress_contract_id);

let course_id = Symbol::new(env, "course_1");
let quiz_id = Symbol::new(env, "quiz_1");
setup_course_and_enroll(env, &progress_client, learner, &course_id, &[quiz_id.clone()]);
progress_client.submit_quiz_score(learner, &course_id, &quiz_id, &80);
token_client.claim_reward(learner, &course_id, &quiz_id); // 8000

token_client.approve(learner, &spender, &5_000, &999_999);

// The spender destroys part of the owner's balance using the allowance.
token_client.burn_from(&spender, learner, &2_000);

assert_eq!(token_client.balance(learner), 6_000);
assert_eq!(token_client.allowance(learner, &spender), 3_000);
assert_eq!(token_client.total_supply(), 6_000);
assert_eq!(token_client.balance(&spender), 0);
}

#[test]
fn test_burn_after_full_learner_journey() {
let setup = fixtures::setup_chainlearn_env();
let env = &setup.env;
let learner = &setup.learner;
env.mock_all_auths();

let token_client = LearnTokenClient::new(env, &setup.token_contract_id);
let progress_client = ProgressTrackerClient::new(env, &setup.progress_contract_id);

let course_id = fixtures::create_sample_course(env, &progress_client);
fixtures::complete_full_course(env, learner, &course_id, &progress_client);

// Earn the rewards for both quizzes, then burn part of the balance.
token_client.claim_reward(learner, &course_id, &Symbol::new(env, "quiz_midterm"));
token_client.claim_reward(learner, &course_id, &Symbol::new(env, "quiz_final"));
assert_eq!(token_client.balance(learner), 16_000);
assert_eq!(token_client.total_supply(), 16_000);

token_client.burn(learner, &6_000);

assert_eq!(token_client.balance(learner), 10_000);
assert_eq!(token_client.total_supply(), 10_000);
}
3 changes: 3 additions & 0 deletions tests/unit/token_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,9 @@ mod token_unit_tests {
assert_eq!(proposal.vote_totals.get(1).unwrap(), 0);
}

// Regression guard (#581): these three pause-path tests must stay at
// module scope so the Rust test harness registers each as an independent
// test. Keep them out of any other test function's body.
#[test]
fn test_create_proposal_fails_while_paused() {
let env = Env::default();
Expand Down