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
12 changes: 4 additions & 8 deletions remittance_split/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ use soroban_sdk::{
Address, Env, Map, Symbol, Vec,
};

#[cfg(test)]
mod test;

// Event topics
const SPLIT_INITIALIZED: Symbol = symbol_short!("init");
Expand Down Expand Up @@ -360,18 +358,13 @@ impl RemittanceSplit {
.checked_mul(s2)
.and_then(|n| n.checked_div(100))
.ok_or(RemittanceSplitError::Overflow)?;
// Insurance gets the remainder to handle rounding
let insurance = total_amount
.checked_sub(spending)
.and_then(|n| n.checked_sub(savings))
.and_then(|n| n.checked_sub(bills))
.ok_or(RemittanceSplitError::Overflow)?;

let spending = (total_amount * split.get(0).unwrap() as i128) / 100;
let savings = (total_amount * split.get(1).unwrap() as i128) / 100;
let bills = (total_amount * split.get(2).unwrap() as i128) / 100;
// Insurance gets the remainder to handle rounding
let insurance = total_amount - spending - savings - bills;

// Emit SplitCalculated event
let event = SplitCalculatedEvent {
total_amount,
Expand Down Expand Up @@ -811,3 +804,6 @@ impl RemittanceSplit {
schedules.get(schedule_id)
}
}

#[cfg(test)]
mod test;
187 changes: 39 additions & 148 deletions savings_goals/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ use soroban_sdk::{

// Event topics
const GOAL_CREATED: Symbol = symbol_short!("created");
const FUNDS_ADDED: Symbol = symbol_short!("added");
const GOAL_COMPLETED: Symbol = symbol_short!("completed");

// Event data structures
#[derive(Clone)]
Expand Down Expand Up @@ -186,7 +184,7 @@ impl SavingsGoalContract {
let goal = SavingsGoal {
id: next_id,
owner: owner.clone(),
name,
name: name.clone(),
target_amount,
current_amount: 0,
target_date,
Expand Down Expand Up @@ -253,41 +251,47 @@ impl SavingsGoalContract {
.get(&symbol_short!("GOALS"))
.unwrap_or_else(|| Map::new(&env));

if let Some(mut goal) = goals.get(goal_id) {
goal.current_amount += amount;
let new_total = goal.current_amount;
let was_completed = goal.current_amount >= goal.target_amount;

goals.set(goal_id, goal.clone());
env.storage()
.instance()
.set(&symbol_short!("GOALS"), &goals);

// Emit FundsAdded event
let funds_event = FundsAddedEvent {
goal_id,
amount,
new_total,
timestamp: env.ledger().timestamp(),
};
env.events().publish((FUNDS_ADDED,), funds_event);

// Emit GoalCompleted event if goal just reached target
if was_completed && (new_total - amount) < goal.target_amount {
let completed_event = GoalCompletedEvent {
goal_id,
name: goal.name.clone(),
final_amount: new_total,
timestamp: env.ledger().timestamp(),
};
env.events().publish((GOAL_COMPLETED,), completed_event);
let mut goal = match goals.get(goal_id) {
Some(g) => g,
None => {
Self::append_audit(&env, symbol_short!("add"), &caller, false);
panic!("Goal not found");
}
};

goal.current_amount
} else {
// Access control: verify caller is the owner
if goal.owner != caller {
Self::append_audit(&env, symbol_short!("add"), &caller, false);
panic!("Goal not found");
panic!("Only the goal owner can add funds");
}

goal.current_amount = goal
.current_amount
.checked_add(amount)
.expect("overflow");
let new_total = goal.current_amount;
let was_completed = new_total >= goal.target_amount;
let just_completed = was_completed && (new_total - amount) < goal.target_amount;

goals.set(goal_id, goal.clone());
env.storage()
.instance()
.set(&symbol_short!("GOALS"), &goals);

Self::append_audit(&env, symbol_short!("add"), &caller, true);
env.events().publish(
(symbol_short!("savings"), SavingsEvent::FundsAdded),
(goal_id, caller, amount),
);

if just_completed {
env.events().publish(
(symbol_short!("savings"), SavingsEvent::GoalCompleted),
(goal_id, goal.owner),
);
}

new_total
}

/// Withdraw funds from a savings goal
Expand Down Expand Up @@ -1002,117 +1006,4 @@ impl SavingsGoalContract {
}

#[cfg(test)]
mod test {
use super::*;
use soroban_sdk::testutils::Events;

#[test]
fn test_create_goal_emits_event() {
let env = Env::default();
let contract_id = env.register_contract(None, SavingsGoalContract);
let client = SavingsGoalContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);

// Create a goal
let goal_id = client.create_goal(
&owner,
&String::from_str(&env, "Education"),
&10000,
&1735689600, // Future date
);
assert_eq!(goal_id, 1);

// Verify event was emitted
let events = env.events().all();
assert_eq!(events.len(), 1);
}

#[test]
fn test_add_to_goal_emits_event() {
let env = Env::default();
let contract_id = env.register_contract(None, SavingsGoalContract);
let client = SavingsGoalContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);

// Create a goal
let goal_id = client.create_goal(
&owner,
&String::from_str(&env, "Medical"),
&5000,
&1735689600,
);

// Get events before adding funds
let events_before = env.events().all().len();

env.mock_all_auths();

// Add funds
let new_amount = client.add_to_goal(&owner, &goal_id, &1000);
assert_eq!(new_amount, 1000);

// Verify 1 new event was emitted (FundsAdded event)
let events_after = env.events().all().len();
assert_eq!(events_after - events_before, 1);
}

#[test]
fn test_goal_completed_emits_event() {
let env = Env::default();
let contract_id = env.register_contract(None, SavingsGoalContract);
let client = SavingsGoalContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);

// Create a goal with small target
let goal_id = client.create_goal(
&owner,
&String::from_str(&env, "Emergency Fund"),
&1000,
&1735689600,
);

// Get events before adding funds
let events_before = env.events().all().len();

env.mock_all_auths();

// Add funds to complete the goal
client.add_to_goal(&owner, &goal_id, &1000);

// Verify both FundsAdded and GoalCompleted events were emitted (2 new events)
let events_after = env.events().all().len();
assert_eq!(events_after - events_before, 2);
}

#[test]
fn test_multiple_goals_emit_separate_events() {
let env = Env::default();
let contract_id = env.register_contract(None, SavingsGoalContract);
let client = SavingsGoalContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);

// Create multiple goals
client.create_goal(
&owner,
&String::from_str(&env, "Goal 1"),
&1000,
&1735689600,
);
client.create_goal(
&owner,
&String::from_str(&env, "Goal 2"),
&2000,
&1735689600,
);
client.create_goal(
&owner,
&String::from_str(&env, "Goal 3"),
&3000,
&1735689600,
);

// Should have 3 GoalCreated events
let events = env.events().all();
assert_eq!(events.len(), 3);
}
}
mod test;
54 changes: 40 additions & 14 deletions savings_goals/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,19 +205,17 @@ fn test_multiple_goals_management() {
}

#[test]
fn test_withdraw_from_goal() {
fn test_withdraw_from_goal_success() {
let env = Env::default();
let contract_id = env.register_contract(None, SavingsGoalContract);
let client = SavingsGoalContractClient::new(&env, &contract_id);
let user = Address::generate(&env);

client.init();
env.mock_all_auths();
let id = client.create_goal(&user, &String::from_str(&env, "W"), &1000, &2000000000);
let id = client.create_goal(&user, &String::from_str(&env, "Success"), &1000, &2000000000);

// Unlock first (created locked)
client.unlock_goal(&user, &id);

client.add_to_goal(&user, &id, &500);

let new_balance = client.withdraw_from_goal(&user, &id, &200);
Expand All @@ -229,15 +227,15 @@ fn test_withdraw_from_goal() {

#[test]
#[should_panic(expected = "Insufficient balance")]
fn test_withdraw_too_much() {
fn test_withdraw_from_goal_insufficient_balance_panics() {
let env = Env::default();
let contract_id = env.register_contract(None, SavingsGoalContract);
let client = SavingsGoalContractClient::new(&env, &contract_id);
let user = Address::generate(&env);

client.init();
env.mock_all_auths();
let id = client.create_goal(&user, &String::from_str(&env, "W"), &1000, &2000000000);
let id = client.create_goal(&user, &String::from_str(&env, "Insufficient"), &1000, &2000000000);

client.unlock_goal(&user, &id);
client.add_to_goal(&user, &id, &100);
Expand All @@ -247,24 +245,23 @@ fn test_withdraw_too_much() {

#[test]
#[should_panic(expected = "Cannot withdraw from a locked goal")]
fn test_withdraw_locked() {
fn test_withdraw_from_goal_locked_panics() {
let env = Env::default();
let contract_id = env.register_contract(None, SavingsGoalContract);
let client = SavingsGoalContractClient::new(&env, &contract_id);
let user = Address::generate(&env);

client.init();
env.mock_all_auths();
let id = client.create_goal(&user, &String::from_str(&env, "L"), &1000, &2000000000);
let id = client.create_goal(&user, &String::from_str(&env, "Locked"), &1000, &2000000000);

// Goal is locked by default
client.add_to_goal(&user, &id, &500);
client.withdraw_from_goal(&user, &id, &100);
}

#[test]
#[should_panic(expected = "Only the goal owner can withdraw funds")]
fn test_withdraw_unauthorized() {
fn test_withdraw_from_goal_unauthorized_panics() {
let env = Env::default();
let contract_id = env.register_contract(None, SavingsGoalContract);
let client = SavingsGoalContractClient::new(&env, &contract_id);
Expand All @@ -273,14 +270,44 @@ fn test_withdraw_unauthorized() {

client.init();
env.mock_all_auths();
let id = client.create_goal(&user, &String::from_str(&env, "Auth"), &1000, &2000000000);
let id = client.create_goal(&user, &String::from_str(&env, "Unauthorized"), &1000, &2000000000);

client.unlock_goal(&user, &id);
client.add_to_goal(&user, &id, &500);

client.withdraw_from_goal(&other, &id, &100);
}

#[test]
#[should_panic(expected = "Amount must be positive")]
fn test_withdraw_from_goal_zero_amount_panics() {
let env = Env::default();
let contract_id = env.register_contract(None, SavingsGoalContract);
let client = SavingsGoalContractClient::new(&env, &contract_id);
let user = Address::generate(&env);

client.init();
env.mock_all_auths();
let id = client.create_goal(&user, &String::from_str(&env, "Zero"), &1000, &2000000000);

client.unlock_goal(&user, &id);
client.add_to_goal(&user, &id, &500);
client.withdraw_from_goal(&user, &id, &0);
}

#[test]
#[should_panic(expected = "Goal not found")]
fn test_withdraw_from_goal_nonexistent_goal_panics() {
let env = Env::default();
let contract_id = env.register_contract(None, SavingsGoalContract);
let client = SavingsGoalContractClient::new(&env, &contract_id);
let user = Address::generate(&env);

client.init();
env.mock_all_auths();
client.withdraw_from_goal(&user, &999, &100);
}

#[test]
fn test_lock_unlock_goal() {
let env = Env::default();
Expand All @@ -305,20 +332,19 @@ fn test_lock_unlock_goal() {
}

#[test]
fn test_full_withdrawal() {
fn test_withdraw_full_balance() {
let env = Env::default();
let contract_id = env.register_contract(None, SavingsGoalContract);
let client = SavingsGoalContractClient::new(&env, &contract_id);
let user = Address::generate(&env);

client.init();
env.mock_all_auths();
let id = client.create_goal(&user, &String::from_str(&env, "W"), &1000, &2000000000);
let id = client.create_goal(&user, &String::from_str(&env, "Full"), &1000, &2000000000);

client.unlock_goal(&user, &id);
client.add_to_goal(&user, &id, &500);

// Withdraw everything
let new_balance = client.withdraw_from_goal(&user, &id, &500);
assert_eq!(new_balance, 0);

Expand Down
Loading
Loading