Skip to content
Merged
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
22 changes: 22 additions & 0 deletions contracts/streaming/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1553,12 +1553,34 @@ fn test_remove_delegate_is_safe_noop_when_never_set() {
);
let stream_id = client.create_stream(&t.sender, &params);

// No delegate was ever set on this stream.
assert_eq!(client.get_delegate(&stream_id), None);

// Removing a delegate that was never set must succeed as a no-op.
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
client.remove_delegate(&stream_id);
}));
assert!(result.is_ok(), "remove_delegate should be a safe no-op");

// A DelegateRemovedEvent must still be emitted for the stream/recipient.
let events = t.env.events().all();
let mut found = false;
for (contract, topics, data) in events.iter() {
if contract != t.contract_id {
continue;
}
let topic: Result<Symbol, _> = topics.get(0).unwrap().try_into_val(&t.env);
if topic != Ok(symbol_short!("delegate_removed")) {
continue;
}
let event: DelegateRemovedEvent = data.try_into_val(&t.env).unwrap();
assert_eq!(event.stream_id, stream_id);
assert_eq!(event.recipient, t.recipient);
found = true;
}
assert!(found, "DelegateRemovedEvent must be emitted");

// Delegate must remain unset.
assert_eq!(client.get_delegate(&stream_id), None);
}

Expand Down
97 changes: 97 additions & 0 deletions contracts/streaming/src/test_features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,103 @@ fn test_bump_stream_event_emitted() {
assert_eq!(stream.id, stream_id);
}

#[test]
fn test_bump_stream_emits_event() {
let t = TestEnv::setup();
let now = 1_000_000u64;
t.set_time(now);

let client = t.client();
let params = t.default_params(now);
let total = params.total_amount;

t.token().approve(
&t.sender,
&t.contract_id,
&total,
&(t.env.ledger().sequence() + 500),
);
let stream_id = client.create_stream(&t.sender, &params);

// Advance the ledger so the emitted timestamp is unambiguous.
let bump_time = now + 250;
t.set_time(bump_time);

client.bump_stream(&stream_id);

// Find the StreamBumpedEvent among the emitted contract events.
let bumped = t
.env
.events()
.all()
.iter()
.find_map(|(_, topics, data)| {
if topics.len() != 2 {
return None;
}
let topic0 = Symbol::try_from_val(&t.env, &topics.get(0)?).ok()?;
if topic0 != Symbol::new(&t.env, "stream_bumped") {
return None;
}
StreamBumpedEvent::try_from_val(&t.env, &data).ok()
})
.expect("StreamBumpedEvent should be emitted");

assert_eq!(bumped.stream_id, stream_id);
assert_eq!(bumped.timestamp, bump_time);
}

#[test]
fn test_bump_stream_works_while_paused() {
let t = TestEnv::setup();
let now = 1_000_000u64;
t.set_time(now);

let client = t.client();
let params = t.default_params(now);
let total = params.total_amount;

client.initialize(&t.sender);

t.token().approve(
&t.sender,
&t.contract_id,
&total,
&(t.env.ledger().sequence() + 500),
);
let stream_id = client.create_stream(&t.sender, &params);

client.pause();

// Intentional: `bump_stream` does not call `require_not_paused` so that
// TTL upkeep can continue while the contract is paused. It only extends
// storage lifetimes and does not mutate stream state.
let bump_time = now + 100;
t.set_time(bump_time);
client.bump_stream(&stream_id);

// The bump still emits its event while paused.
let bumped = t
.env
.events()
.all()
.iter()
.find_map(|(_, topics, data)| {
if topics.len() != 2 {
return None;
}
let topic0 = Symbol::try_from_val(&t.env, &topics.get(0)?).ok()?;
if topic0 != Symbol::new(&t.env, "stream_bumped") {
return None;
}
StreamBumpedEvent::try_from_val(&t.env, &data).ok()
})
.expect("StreamBumpedEvent should be emitted while paused");

assert_eq!(bumped.stream_id, stream_id);
assert_eq!(bumped.timestamp, bump_time);
}

// ─── #71: Pause/unpause mechanism ─────────────────────────────────────────────

#[test]
Expand Down
50 changes: 50 additions & 0 deletions contracts/streaming/src/test_security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,56 @@ fn test_auth_recipient_cannot_withdraw_without_delegate_auth_when_delegate_set()
ctx.client().withdraw(&id, &withdrawable);
}

/// Non-recipient cannot remove the delegate; only the recipient may.
#[test]
#[should_panic]
fn test_remove_delegate_requires_recipient_auth() {
let ctx = Ctx::new();
let now = 1_000_000u64;
ctx.set_time(now);
let id = ctx.create_basic_stream(now);

let delegate = Address::generate(&ctx.env);
ctx.client().set_delegate(&id, &delegate);

ctx.env.mock_auths(&[MockAuth {
address: &ctx.attacker,
invoke: &MockAuthInvoke {
contract: &ctx.contract_id,
fn_name: "remove_delegate",
args: (id,).into_val(&ctx.env),
sub_invokes: &[],
},
}]);
ctx.client().remove_delegate(&id);
}

/// Recipient can remove the delegate.
#[test]
fn test_remove_delegate_by_recipient_succeeds() {
let ctx = Ctx::new();
let now = 1_000_000u64;
ctx.set_time(now);
let id = ctx.create_basic_stream(now);

let delegate = Address::generate(&ctx.env);
ctx.client().set_delegate(&id, &delegate);
assert_eq!(ctx.client().get_delegate(&id), Some(delegate));

ctx.env.mock_auths(&[MockAuth {
address: &ctx.recipient,
invoke: &MockAuthInvoke {
contract: &ctx.contract_id,
fn_name: "remove_delegate",
args: (id,).into_val(&ctx.env),
sub_invokes: &[],
},
}]);
ctx.client().remove_delegate(&id);

assert_eq!(ctx.client().get_delegate(&id), None);
}

/// Attacker cannot withdraw even when a delegate is set.
#[test]
#[should_panic]
Expand Down