From 67cb864db723665318a1c5647c331671e6eb3a9b Mon Sep 17 00:00:00 2001 From: kodegreen70 Date: Mon, 1 Jun 2026 01:23:02 +0100 Subject: [PATCH 1/4] Add to pro_subscription contract --- .gitignore | 3 ++ .../pro_subscription/src/contract.rs | 11 ++++++ .../contracts/pro_subscription/src/test.rs | 34 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/.gitignore b/.gitignore index 2d9de1f0..ca38ce8a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ contract/target # Contract test snapshots **/test_snapshots/ + +# PR description drafts +pr-*.md diff --git a/contract/contracts/pro_subscription/src/contract.rs b/contract/contracts/pro_subscription/src/contract.rs index 3d0c0bf2..3e385a00 100644 --- a/contract/contracts/pro_subscription/src/contract.rs +++ b/contract/contracts/pro_subscription/src/contract.rs @@ -365,4 +365,15 @@ impl ProSubscriptionContract { pub fn get_payment_token(env: Env) -> Option
{ get_payment_token(&env) } + + /// Update the accepted payment token (admin only) + pub fn update_payment_token( + env: Env, + new_token: Address, + ) -> Result<(), ProSubscriptionError> { + require_admin(&env)?; + validate_address(&env, &new_token)?; + set_payment_token(&env, &new_token); + Ok(()) + } } diff --git a/contract/contracts/pro_subscription/src/test.rs b/contract/contracts/pro_subscription/src/test.rs index b1063731..cab58435 100644 --- a/contract/contracts/pro_subscription/src/test.rs +++ b/contract/contracts/pro_subscription/src/test.rs @@ -427,3 +427,37 @@ fn test_get_payment_token() { let result = client.get_payment_token(); assert_eq!(result, Some(usdc)); } + +#[test] +fn test_update_payment_token_success() { + let (env, client, _admin, _platform_wallet, _usdc) = setup(); + let new_token = env + .register_stellar_asset_contract_v2(Address::generate(&env)) + .address(); + + client.update_payment_token(&new_token); + + assert_eq!(client.get_payment_token(), Some(new_token)); +} + +#[test] +#[should_panic] +fn test_update_payment_token_unauthorized() { + let (env, client, contract_id, _admin, _platform_wallet, _usdc) = setup_without_auth_mock(); + let non_admin = Address::generate(&env); + let new_token = env + .register_stellar_asset_contract_v2(Address::generate(&env)) + .address(); + + env.mock_auths(&[MockAuth { + address: &non_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "update_payment_token", + args: (&new_token,).into_val(&env), + sub_invokes: &[], + }, + }]); + + client.update_payment_token(&new_token); +} From 17b7a0d85bedaeca7f316a7e61dc7a386401689e Mon Sep 17 00:00:00 2001 From: kodegreen70 Date: Mon, 1 Jun 2026 01:27:45 +0100 Subject: [PATCH 2/4] Emit ProMemberAdded and ProMemberRemoved events --- .../pro_subscription/src/contract.rs | 29 +++++- .../contracts/pro_subscription/src/events.rs | 20 ++++ .../contracts/pro_subscription/src/test.rs | 92 +++++++++++++++++++ 3 files changed, 139 insertions(+), 2 deletions(-) diff --git a/contract/contracts/pro_subscription/src/contract.rs b/contract/contracts/pro_subscription/src/contract.rs index 3e385a00..ce5b129a 100644 --- a/contract/contracts/pro_subscription/src/contract.rs +++ b/contract/contracts/pro_subscription/src/contract.rs @@ -3,8 +3,9 @@ use soroban_sdk::{contract, contractimpl, token, Address, Env}; use crate::{ error::ProSubscriptionError, events::{ - InitializationEvent, PriceUpdatedEvent, ProSubscriptionEvent, SubscriptionCancelledEvent, - SubscriptionCreatedEvent, SubscriptionRenewedEvent, + InitializationEvent, PriceUpdatedEvent, ProMemberAddedEvent, ProMemberRemovedEvent, + ProSubscriptionEvent, SubscriptionCancelledEvent, SubscriptionCreatedEvent, + SubscriptionRenewedEvent, }, storage::{ add_to_pro_members_list, decrement_total_pro_subscriptions, get_admin, @@ -132,6 +133,14 @@ impl ProSubscriptionContract { add_to_pro_members_list(&env, &organizer); increment_total_pro_subscriptions(&env); + env.events().publish( + (ProSubscriptionEvent::ProMemberAdded,), + ProMemberAddedEvent { + organizer: organizer.clone(), + timestamp: current_time, + }, + ); + env.events().publish( (ProSubscriptionEvent::SubscriptionCreated,), SubscriptionCreatedEvent { @@ -203,6 +212,14 @@ impl ProSubscriptionContract { // Ensure they're in the pro members list add_to_pro_members_list(&env, &organizer); + env.events().publish( + (ProSubscriptionEvent::ProMemberAdded,), + ProMemberAddedEvent { + organizer: organizer.clone(), + timestamp: current_time, + }, + ); + env.events().publish( (ProSubscriptionEvent::SubscriptionRenewed,), SubscriptionRenewedEvent { @@ -228,6 +245,14 @@ impl ProSubscriptionContract { remove_from_pro_members_list(&env, &organizer); decrement_total_pro_subscriptions(&env); + env.events().publish( + (ProSubscriptionEvent::ProMemberRemoved,), + ProMemberRemovedEvent { + organizer: organizer.clone(), + timestamp: env.ledger().timestamp(), + }, + ); + env.events().publish( (ProSubscriptionEvent::SubscriptionCancelled,), SubscriptionCancelledEvent { diff --git a/contract/contracts/pro_subscription/src/events.rs b/contract/contracts/pro_subscription/src/events.rs index daae6fe6..fb49d27b 100644 --- a/contract/contracts/pro_subscription/src/events.rs +++ b/contract/contracts/pro_subscription/src/events.rs @@ -18,6 +18,10 @@ pub enum ProSubscriptionEvent { SubscriptionExpired, /// Pro monthly price updated PriceUpdated, + /// Organizer added to the pro members list + ProMemberAdded, + /// Organizer removed from the pro members list + ProMemberRemoved, } /// Emitted when the contract is initialized @@ -70,3 +74,19 @@ pub struct PriceUpdatedEvent { pub updated_by: Address, pub timestamp: u64, } + +/// Emitted when an organizer is added to the pro members list +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProMemberAddedEvent { + pub organizer: Address, + pub timestamp: u64, +} + +/// Emitted when an organizer is removed from the pro members list +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProMemberRemovedEvent { + pub organizer: Address, + pub timestamp: u64, +} diff --git a/contract/contracts/pro_subscription/src/test.rs b/contract/contracts/pro_subscription/src/test.rs index cab58435..ad825049 100644 --- a/contract/contracts/pro_subscription/src/test.rs +++ b/contract/contracts/pro_subscription/src/test.rs @@ -461,3 +461,95 @@ fn test_update_payment_token_unauthorized() { client.update_payment_token(&new_token); } + +// ── Pro member list events ──────────────────────────────────────────────────── + +#[test] +fn test_pro_member_added_event_on_subscribe() { + use crate::events::{ProMemberAddedEvent, ProSubscriptionEvent}; + use soroban_sdk::IntoVal; + + let (env, client, _admin, _platform_wallet, usdc) = setup(); + let organizer = Address::generate(&env); + let monthly_price = 1000i128; + + token::StellarAssetClient::new(&env, &usdc).mint(&organizer, &monthly_price); + token::Client::new(&env, &usdc).approve(&organizer, &client.address, &monthly_price, &99999); + + client.subscribe_pro(&organizer, &1u32); + + // Find the ProMemberAdded event among all emitted events + let events = env.events().all(); + let member_added = events.iter().find(|(_, topics, _)| { + let topic: ProSubscriptionEvent = topics.get(0).unwrap().into_val(&env); + topic == ProSubscriptionEvent::ProMemberAdded + }); + + assert!(member_added.is_some(), "ProMemberAdded event not emitted"); + let (_, _, data) = member_added.unwrap(); + let payload: ProMemberAddedEvent = data.into_val(&env); + assert_eq!(payload.organizer, organizer); +} + +#[test] +fn test_pro_member_added_event_on_renew() { + use crate::events::{ProMemberAddedEvent, ProSubscriptionEvent}; + use soroban_sdk::IntoVal; + + let (env, client, _admin, _platform_wallet, usdc) = setup(); + let organizer = Address::generate(&env); + let monthly_price = 1000i128; + + // Initial subscription + token::StellarAssetClient::new(&env, &usdc).mint(&organizer, &monthly_price); + token::Client::new(&env, &usdc).approve(&organizer, &client.address, &monthly_price, &99999); + client.subscribe_pro(&organizer, &1u32); + + // Renewal + token::StellarAssetClient::new(&env, &usdc).mint(&organizer, &monthly_price); + token::Client::new(&env, &usdc).approve(&organizer, &client.address, &monthly_price, &99999); + client.renew_subscription(&organizer, &1u32); + + // The last ProMemberAdded event should correspond to the renewal + let events = env.events().all(); + let member_added_events: soroban_sdk::Vec<_> = events + .iter() + .filter(|(_, topics, _)| { + let topic: ProSubscriptionEvent = topics.get(0).unwrap().into_val(&env); + topic == ProSubscriptionEvent::ProMemberAdded + }) + .collect(); + + // One from subscribe, one from renew + assert_eq!(member_added_events.len(), 2); + let (_, _, data) = member_added_events.last().unwrap(); + let payload: ProMemberAddedEvent = data.into_val(&env); + assert_eq!(payload.organizer, organizer); +} + +#[test] +fn test_pro_member_removed_event_on_cancel() { + use crate::events::{ProMemberRemovedEvent, ProSubscriptionEvent}; + use soroban_sdk::IntoVal; + + let (env, client, _admin, _platform_wallet, usdc) = setup(); + let organizer = Address::generate(&env); + let monthly_price = 1000i128; + + token::StellarAssetClient::new(&env, &usdc).mint(&organizer, &monthly_price); + token::Client::new(&env, &usdc).approve(&organizer, &client.address, &monthly_price, &99999); + client.subscribe_pro(&organizer, &1u32); + + client.cancel_subscription(&organizer); + + let events = env.events().all(); + let member_removed = events.iter().find(|(_, topics, _)| { + let topic: ProSubscriptionEvent = topics.get(0).unwrap().into_val(&env); + topic == ProSubscriptionEvent::ProMemberRemoved + }); + + assert!(member_removed.is_some(), "ProMemberRemoved event not emitted"); + let (_, _, data) = member_removed.unwrap(); + let payload: ProMemberRemovedEvent = data.into_val(&env); + assert_eq!(payload.organizer, organizer); +} From 12998c866795ff9340b4f4bf1737c7109b01bece Mon Sep 17 00:00:00 2001 From: kodegreen70 Date: Mon, 1 Jun 2026 01:30:13 +0100 Subject: [PATCH 3/4] Add counter accounting tests for get_total_pro_subscriptions --- .../contracts/pro_subscription/src/test.rs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/contract/contracts/pro_subscription/src/test.rs b/contract/contracts/pro_subscription/src/test.rs index ad825049..b37e21f2 100644 --- a/contract/contracts/pro_subscription/src/test.rs +++ b/contract/contracts/pro_subscription/src/test.rs @@ -553,3 +553,64 @@ fn test_pro_member_removed_event_on_cancel() { let payload: ProMemberRemovedEvent = data.into_val(&env); assert_eq!(payload.organizer, organizer); } + +// ── Issue #640: get_total_pro_subscriptions accounting ─────────────────────── + +#[test] +fn test_total_subscriptions_increments_on_subscribe() { + let (env, client, _admin, _platform_wallet, usdc) = setup(); + let monthly_price = 1000i128; + + let org1 = Address::generate(&env); + let org2 = Address::generate(&env); + + token::StellarAssetClient::new(&env, &usdc).mint(&org1, &monthly_price); + token::Client::new(&env, &usdc).approve(&org1, &client.address, &monthly_price, &99999); + client.subscribe_pro(&org1, &1u32); + + assert_eq!(client.get_total_pro_subscriptions(), 1u32); + + token::StellarAssetClient::new(&env, &usdc).mint(&org2, &monthly_price); + token::Client::new(&env, &usdc).approve(&org2, &client.address, &monthly_price, &99999); + client.subscribe_pro(&org2, &1u32); + + assert_eq!(client.get_total_pro_subscriptions(), 2u32); +} + +#[test] +fn test_total_subscriptions_decrements_on_cancel() { + let (env, client, _admin, _platform_wallet, usdc) = setup(); + let monthly_price = 1000i128; + let organizer = Address::generate(&env); + + token::StellarAssetClient::new(&env, &usdc).mint(&organizer, &monthly_price); + token::Client::new(&env, &usdc).approve(&organizer, &client.address, &monthly_price, &99999); + client.subscribe_pro(&organizer, &1u32); + + assert_eq!(client.get_total_pro_subscriptions(), 1u32); + + client.cancel_subscription(&organizer); + + assert_eq!(client.get_total_pro_subscriptions(), 0u32); +} + +#[test] +fn test_total_subscriptions_no_double_count() { + let (env, client, _admin, _platform_wallet, usdc) = setup(); + let monthly_price = 1000i128; + let organizer = Address::generate(&env); + + // First subscription — succeeds + token::StellarAssetClient::new(&env, &usdc).mint(&organizer, &monthly_price); + token::Client::new(&env, &usdc).approve(&organizer, &client.address, &monthly_price, &99999); + client.subscribe_pro(&organizer, &1u32); + + // Second subscription while still active — must fail + token::StellarAssetClient::new(&env, &usdc).mint(&organizer, &monthly_price); + token::Client::new(&env, &usdc).approve(&organizer, &client.address, &monthly_price, &99999); + let res = client.try_subscribe_pro(&organizer, &1u32); + assert_eq!(res, Err(Ok(ProSubscriptionError::SubscriptionAlreadyActive))); + + // Counter must still be 1, not 2 + assert_eq!(client.get_total_pro_subscriptions(), 1u32); +} From e167f5afdcaadb3293549ec6af4a81cac8576369 Mon Sep 17 00:00:00 2001 From: kodegreen70 Date: Mon, 1 Jun 2026 01:35:39 +0100 Subject: [PATCH 4/4] Add cancel_subscription test coverage --- .../contracts/pro_subscription/src/test.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/contract/contracts/pro_subscription/src/test.rs b/contract/contracts/pro_subscription/src/test.rs index b37e21f2..565dc54e 100644 --- a/contract/contracts/pro_subscription/src/test.rs +++ b/contract/contracts/pro_subscription/src/test.rs @@ -614,3 +614,65 @@ fn test_total_subscriptions_no_double_count() { // Counter must still be 1, not 2 assert_eq!(client.get_total_pro_subscriptions(), 1u32); } + +// ── Issue #632: cancel_subscription coverage ───────────────────────────────── + +#[test] +fn test_cancel_subscription_success() { + let (env, client, _admin, _platform_wallet, usdc) = setup(); + let organizer = Address::generate(&env); + let monthly_price = 1000i128; + + token::StellarAssetClient::new(&env, &usdc).mint(&organizer, &monthly_price); + token::Client::new(&env, &usdc).approve(&organizer, &client.address, &monthly_price, &99999); + client.subscribe_pro(&organizer, &1u32); + + // Confirm active before cancel + assert!(client.is_pro_member(&organizer)); + + client.cancel_subscription(&organizer); + + // is_pro_member must return false after cancellation + assert!(!client.is_pro_member(&organizer)); + + // Subscription record should exist but be inactive + let sub = client.get_subscription(&organizer).unwrap(); + assert!(!sub.is_active); +} + +#[test] +fn test_cancel_subscription_not_found() { + let (env, client, _admin, _platform_wallet, _usdc) = setup(); + let never_subscribed = Address::generate(&env); + + let res = client.try_cancel_subscription(&never_subscribed); + assert_eq!(res, Err(Ok(ProSubscriptionError::SubscriptionNotFound))); +} + +#[test] +#[should_panic] +fn test_cancel_subscription_unauthorized() { + let (env, client, contract_id, _admin, _platform_wallet, usdc) = setup_without_auth_mock(); + let non_admin = Address::generate(&env); + let organizer = Address::generate(&env); + let monthly_price = 1000i128; + + // Subscribe the organizer first (mock all auths just for setup) + env.mock_all_auths(); + token::StellarAssetClient::new(&env, &usdc).mint(&organizer, &monthly_price); + token::Client::new(&env, &usdc).approve(&organizer, &client.address, &monthly_price, &99999); + client.subscribe_pro(&organizer, &1u32); + + // Now attempt cancel as a non-admin — should panic + env.mock_auths(&[MockAuth { + address: &non_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "cancel_subscription", + args: (&organizer,).into_val(&env), + sub_invokes: &[], + }, + }]); + + client.cancel_subscription(&organizer); +}