use ed25519_dalek::SigningKey;
use freenet_scaffold::ComposableState;
use rand::rngs::OsRng;
use river_core::room_state::ban::{AuthorizedUserBan, BansV1, UserBan};
use river_core::room_state::member::{AuthorizedMember, Member, MemberId, MembersDelta, MembersV1};
use river_core::room_state::message::{
AuthorizedMessageV1, MessageId, MessageV1, MessagesV1, RoomMessageBody,
};
use river_core::room_state::{ChatRoomParametersV1, ChatRoomStateV1};
use std::time::SystemTime;
fn create_test_member(owner_id: MemberId, invited_by: MemberId) -> (Member, SigningKey) {
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let member = Member {
owner_member_id: owner_id,
invited_by,
member_vk: verifying_key,
};
(member, signing_key)
}
fn create_authorized_member(member: Member, inviter_signing_key: &SigningKey) -> AuthorizedMember {
AuthorizedMember::new(member, inviter_signing_key)
}
fn create_test_msg(
owner_id: MemberId,
author_id: MemberId,
author_sk: &SigningKey,
time_offset_secs: u64,
) -> AuthorizedMessageV1 {
AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: author_id,
time: SystemTime::now() + std::time::Duration::from_secs(time_offset_secs),
content: RoomMessageBody::public(format!("msg from {:?}", author_id)),
},
author_sk,
)
}
#[test]
fn test_member_add_order_convergence() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let (member_a, _) = create_test_member(owner_id, owner_id);
let (member_b, _) = create_test_member(owner_id, owner_id);
let auth_member_a = create_authorized_member(member_a.clone(), &owner_signing_key);
let auth_member_b = create_authorized_member(member_b.clone(), &owner_signing_key);
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 1;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut state_a = MembersV1::default();
let delta_ab = MembersDelta::new(vec![auth_member_a.clone(), auth_member_b.clone()]);
state_a
.apply_delta(&parent_state, ¶meters, &Some(delta_ab))
.expect("apply_delta should succeed");
let mut state_b = MembersV1::default();
let delta_ba = MembersDelta::new(vec![auth_member_b.clone(), auth_member_a.clone()]);
state_b
.apply_delta(&parent_state, ¶meters, &Some(delta_ba))
.expect("apply_delta should succeed");
assert_eq!(state_a.members.len(), 1, "State A should have 1 member");
assert_eq!(state_b.members.len(), 1, "State B should have 1 member");
let member_a_in_state_a = state_a
.members
.iter()
.any(|m| m.member.id() == member_a.id());
let member_b_in_state_a = state_a
.members
.iter()
.any(|m| m.member.id() == member_b.id());
let member_a_in_state_b = state_b
.members
.iter()
.any(|m| m.member.id() == member_a.id());
let member_b_in_state_b = state_b
.members
.iter()
.any(|m| m.member.id() == member_b.id());
assert_eq!(
member_a_in_state_a, member_a_in_state_b,
"Member A presence should be the same in both states. \
State A has member A: {}, State B has member A: {}",
member_a_in_state_a, member_a_in_state_b
);
assert_eq!(
member_b_in_state_a, member_b_in_state_b,
"Member B presence should be the same in both states. \
State A has member B: {}, State B has member B: {}",
member_b_in_state_a, member_b_in_state_b
);
assert_eq!(
state_a
.members
.iter()
.map(|m| m.member.id())
.collect::<Vec<_>>(),
state_b
.members
.iter()
.map(|m| m.member.id())
.collect::<Vec<_>>(),
"CONVERGENCE FAILURE: Different delta orders produced different final states!\n\
State A members: {:?}\n\
State B members: {:?}",
state_a
.members
.iter()
.map(|m| m.member.id())
.collect::<Vec<_>>(),
state_b
.members
.iter()
.map(|m| m.member.id())
.collect::<Vec<_>>()
);
}
#[test]
fn test_member_removal_tiebreak_convergence() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let (member_a, _) = create_test_member(owner_id, owner_id);
let (member_b, _) = create_test_member(owner_id, owner_id);
let (member_c, _) = create_test_member(owner_id, owner_id);
let auth_member_a = create_authorized_member(member_a.clone(), &owner_signing_key);
let auth_member_b = create_authorized_member(member_b.clone(), &owner_signing_key);
let auth_member_c = create_authorized_member(member_c.clone(), &owner_signing_key);
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 2;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut state_a = MembersV1 {
members: vec![
auth_member_a.clone(),
auth_member_b.clone(),
auth_member_c.clone(),
],
};
state_a
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut state_b = MembersV1 {
members: vec![
auth_member_c.clone(),
auth_member_b.clone(),
auth_member_a.clone(),
],
};
state_b
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
assert_eq!(state_a.members.len(), 2, "State A should have 2 members");
assert_eq!(state_b.members.len(), 2, "State B should have 2 members");
let mut ids_a: Vec<_> = state_a.members.iter().map(|m| m.member.id()).collect();
let mut ids_b: Vec<_> = state_b.members.iter().map(|m| m.member.id()).collect();
ids_a.sort();
ids_b.sort();
assert_eq!(
ids_a, ids_b,
"CONVERGENCE FAILURE: Different iteration orders produced different member sets!\n\
State A members: {:?}\n\
State B members: {:?}\n\
All members had the same invite chain length, so tie-breaking was needed.",
ids_a, ids_b
);
}
#[test]
fn test_ban_excess_order_convergence() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let (member_a, _) = create_test_member(owner_id, owner_id);
let (member_b, _) = create_test_member(owner_id, owner_id);
let (member_c, _) = create_test_member(owner_id, owner_id);
let auth_member_a = create_authorized_member(member_a.clone(), &owner_signing_key);
let auth_member_b = create_authorized_member(member_b.clone(), &owner_signing_key);
let auth_member_c = create_authorized_member(member_c.clone(), &owner_signing_key);
let same_time = SystemTime::now();
let ban_a = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: same_time,
banned_user: member_a.id(),
},
owner_id,
&owner_signing_key,
);
let ban_b = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: same_time,
banned_user: member_b.id(),
},
owner_id,
&owner_signing_key,
);
let ban_c = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: same_time,
banned_user: member_c.id(),
},
owner_id,
&owner_signing_key,
);
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_user_bans = 2;
parent_state.members = MembersV1 {
members: vec![auth_member_a, auth_member_b, auth_member_c],
};
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let bans_a = BansV1(vec![ban_a.clone(), ban_b.clone(), ban_c.clone()]);
let bans_b = BansV1(vec![ban_c.clone(), ban_b.clone(), ban_a.clone()]);
let result_a = bans_a.verify(&parent_state, ¶meters);
let result_b = bans_b.verify(&parent_state, ¶meters);
assert!(result_a.is_err(), "State A should fail verification");
assert!(result_b.is_err(), "State B should fail verification");
let err_a = result_a.unwrap_err();
let err_b = result_b.unwrap_err();
assert_eq!(
err_a, err_b,
"CONVERGENCE FAILURE: Different ban orders identified different excess bans!\n\
Error A: {}\n\
Error B: {}\n\
All bans had the same timestamp, so tie-breaking was needed.",
err_a, err_b
);
}
#[test]
fn test_message_prune_order_convergence() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let same_time = SystemTime::now();
let msg_a = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: owner_id,
time: same_time,
content: RoomMessageBody::public("Message A".to_string()),
},
&owner_signing_key,
);
let msg_b = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: owner_id,
time: same_time,
content: RoomMessageBody::public("Message B".to_string()),
},
&owner_signing_key,
);
let msg_c = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: owner_id,
time: same_time,
content: RoomMessageBody::public("Message C".to_string()),
},
&owner_signing_key,
);
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_recent_messages = 2;
parent_state.configuration.configuration.max_message_size = 1000;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut state_a = MessagesV1 {
messages: vec![msg_a.clone(), msg_b.clone(), msg_c.clone()],
..Default::default()
};
state_a
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut state_b = MessagesV1 {
messages: vec![msg_c.clone(), msg_b.clone(), msg_a.clone()],
..Default::default()
};
state_b
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
assert_eq!(state_a.messages.len(), 2, "State A should have 2 messages");
assert_eq!(state_b.messages.len(), 2, "State B should have 2 messages");
let mut ids_a: Vec<_> = state_a.messages.iter().map(|m| m.id()).collect();
let mut ids_b: Vec<_> = state_b.messages.iter().map(|m| m.id()).collect();
ids_a.sort();
ids_b.sort();
assert_eq!(
ids_a, ids_b,
"CONVERGENCE FAILURE: Different message orders produced different message sets!\n\
State A messages: {:?}\n\
State B messages: {:?}\n\
All messages had the same timestamp, so tie-breaking was needed.",
ids_a, ids_b
);
}
#[test]
fn test_member_delta_idempotency() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let (member_a, _) = create_test_member(owner_id, owner_id);
let auth_member_a = create_authorized_member(member_a.clone(), &owner_signing_key);
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 10;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut state = MembersV1::default();
let delta = MembersDelta::new(vec![auth_member_a.clone()]);
state
.apply_delta(&parent_state, ¶meters, &Some(delta.clone()))
.expect("first apply_delta should succeed");
let state_after_first = state.clone();
state
.apply_delta(&parent_state, ¶meters, &Some(delta))
.expect("second apply_delta should succeed");
assert_eq!(
state.members.len(),
state_after_first.members.len(),
"Applying delta twice should be idempotent"
);
assert_eq!(
state
.members
.iter()
.map(|m| m.member.id())
.collect::<Vec<_>>(),
state_after_first
.members
.iter()
.map(|m| m.member.id())
.collect::<Vec<_>>(),
"Applying delta twice should produce identical state"
);
}
#[test]
fn test_message_delta_idempotency() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let msg = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: owner_id,
time: SystemTime::now(),
content: RoomMessageBody::public("Test message".to_string()),
},
&owner_signing_key,
);
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_recent_messages = 100;
parent_state.configuration.configuration.max_message_size = 1000;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut state = MessagesV1::default();
let delta = vec![msg.clone()];
state
.apply_delta(&parent_state, ¶meters, &Some(delta.clone()))
.expect("first apply_delta should succeed");
let state_after_first = state.clone();
state
.apply_delta(&parent_state, ¶meters, &Some(delta))
.expect("second apply_delta should succeed");
assert_eq!(
state.messages.len(),
state_after_first.messages.len(),
"Applying delta twice should be idempotent"
);
}
#[test]
fn test_member_interleaved_deltas_convergence() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let (member_a, _) = create_test_member(owner_id, owner_id);
let (member_b, _) = create_test_member(owner_id, owner_id);
let (member_c, _) = create_test_member(owner_id, owner_id);
let (member_d, _) = create_test_member(owner_id, owner_id);
let auth_a = create_authorized_member(member_a.clone(), &owner_signing_key);
let auth_b = create_authorized_member(member_b.clone(), &owner_signing_key);
let auth_c = create_authorized_member(member_c.clone(), &owner_signing_key);
let auth_d = create_authorized_member(member_d.clone(), &owner_signing_key);
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 2;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut state_1 = MembersV1::default();
let delta_1 = MembersDelta::new(vec![auth_a.clone(), auth_b.clone()]);
state_1
.apply_delta(&parent_state, ¶meters, &Some(delta_1))
.expect("apply_delta should succeed");
let delta_2 = MembersDelta::new(vec![auth_c.clone(), auth_d.clone()]);
state_1
.apply_delta(&parent_state, ¶meters, &Some(delta_2))
.expect("apply_delta should succeed");
let mut state_2 = MembersV1::default();
let delta_1 = MembersDelta::new(vec![auth_c.clone(), auth_d.clone()]);
state_2
.apply_delta(&parent_state, ¶meters, &Some(delta_1))
.expect("apply_delta should succeed");
let delta_2 = MembersDelta::new(vec![auth_a.clone(), auth_b.clone()]);
state_2
.apply_delta(&parent_state, ¶meters, &Some(delta_2))
.expect("apply_delta should succeed");
let mut ids_1: Vec<_> = state_1.members.iter().map(|m| m.member.id()).collect();
let mut ids_2: Vec<_> = state_2.members.iter().map(|m| m.member.id()).collect();
ids_1.sort();
ids_2.sort();
assert_eq!(
ids_1, ids_2,
"CONVERGENCE FAILURE: Different delta application orders produced different states!\n\
Peer 1 applied [A,B] then [C,D]: {:?}\n\
Peer 2 applied [C,D] then [A,B]: {:?}",
ids_1, ids_2
);
}
#[test]
fn test_member_convergence_stress_50_members() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let mut all_members: Vec<(AuthorizedMember, SigningKey)> = Vec::new();
let mut level_0_members: Vec<(AuthorizedMember, SigningKey)> = Vec::new();
for _ in 0..15 {
let (member, signing_key) = create_test_member(owner_id, owner_id);
let auth_member = create_authorized_member(member, &owner_signing_key);
level_0_members.push((auth_member.clone(), signing_key.clone()));
all_members.push((auth_member, signing_key));
}
let mut level_1_members: Vec<(AuthorizedMember, SigningKey)> = Vec::new();
for i in 0..20 {
let inviter_idx = i % level_0_members.len();
let inviter = &level_0_members[inviter_idx];
let (member, signing_key) = create_test_member(owner_id, inviter.0.member.id());
let auth_member = create_authorized_member(member, &inviter.1);
level_1_members.push((auth_member.clone(), signing_key.clone()));
all_members.push((auth_member, signing_key));
}
for i in 0..15 {
let inviter_idx = i % level_1_members.len();
let inviter = &level_1_members[inviter_idx];
let (member, signing_key) = create_test_member(owner_id, inviter.0.member.id());
let auth_member = create_authorized_member(member, &inviter.1);
all_members.push((auth_member, signing_key));
}
assert_eq!(all_members.len(), 50);
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 30;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let auth_members: Vec<AuthorizedMember> = all_members.iter().map(|(m, _)| m.clone()).collect();
let mut state_a = MembersV1 {
members: auth_members.clone(),
};
state_a
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut reversed = auth_members.clone();
reversed.reverse();
let mut state_b = MembersV1 { members: reversed };
state_b
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut rotated = auth_members.clone();
rotated.rotate_left(17);
let mut state_c = MembersV1 { members: rotated };
state_c
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
assert_eq!(state_a.members.len(), 30, "State A should have 30 members");
assert_eq!(state_b.members.len(), 30, "State B should have 30 members");
assert_eq!(state_c.members.len(), 30, "State C should have 30 members");
let mut ids_a: Vec<_> = state_a.members.iter().map(|m| m.member.id()).collect();
let mut ids_b: Vec<_> = state_b.members.iter().map(|m| m.member.id()).collect();
let mut ids_c: Vec<_> = state_c.members.iter().map(|m| m.member.id()).collect();
ids_a.sort();
ids_b.sort();
ids_c.sort();
assert_eq!(
ids_a, ids_b,
"CONVERGENCE FAILURE: State A and B have different members"
);
assert_eq!(
ids_b, ids_c,
"CONVERGENCE FAILURE: State B and C have different members"
);
}
#[test]
fn test_message_convergence_stress_100_messages() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let base_time = SystemTime::now();
let mut messages: Vec<AuthorizedMessageV1> = Vec::new();
for i in 0..100 {
let time_offset = (i / 5) as u64; let time = base_time + std::time::Duration::from_secs(time_offset);
let msg = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: owner_id,
time,
content: RoomMessageBody::public(format!("Message {}", i)),
},
&owner_signing_key,
);
messages.push(msg);
}
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_recent_messages = 50;
parent_state.configuration.configuration.max_message_size = 1000;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut state_a = MessagesV1 {
messages: messages.clone(),
..Default::default()
};
state_a
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut reversed = messages.clone();
reversed.reverse();
let mut state_b = MessagesV1 {
messages: reversed,
..Default::default()
};
state_b
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut interleaved: Vec<AuthorizedMessageV1> = Vec::new();
let half = messages.len() / 2;
for i in 0..half {
interleaved.push(messages[i].clone());
interleaved.push(messages[half + i].clone());
}
let mut state_c = MessagesV1 {
messages: interleaved,
..Default::default()
};
state_c
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
assert_eq!(state_a.messages.len(), 50);
assert_eq!(state_b.messages.len(), 50);
assert_eq!(state_c.messages.len(), 50);
let ids_a: Vec<_> = state_a.messages.iter().map(|m| m.id()).collect();
let ids_b: Vec<_> = state_b.messages.iter().map(|m| m.id()).collect();
let ids_c: Vec<_> = state_c.messages.iter().map(|m| m.id()).collect();
assert_eq!(
ids_a, ids_b,
"CONVERGENCE FAILURE: State A and B have different message order"
);
assert_eq!(
ids_b, ids_c,
"CONVERGENCE FAILURE: State B and C have different message order"
);
}
#[test]
fn test_ban_convergence_stress_same_timestamps() {
use std::collections::HashSet;
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let mut members: Vec<AuthorizedMember> = Vec::new();
for _ in 0..20 {
let (member, _) = create_test_member(owner_id, owner_id);
let auth_member = create_authorized_member(member, &owner_signing_key);
members.push(auth_member);
}
let same_time = SystemTime::now();
let mut bans: Vec<AuthorizedUserBan> = Vec::new();
for member in members.iter().take(15) {
let ban = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: same_time,
banned_user: member.member.id(),
},
owner_id,
&owner_signing_key,
);
bans.push(ban);
}
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_user_bans = 10;
parent_state.members = MembersV1 {
members: members.clone(),
};
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
fn extract_ban_ids(err: &str) -> HashSet<String> {
err.split("BanId(FastHash(")
.skip(1)
.filter_map(|s| s.split("))").next())
.map(|s| s.to_string())
.collect()
}
let bans_a = BansV1(bans.clone());
let mut reversed = bans.clone();
reversed.reverse();
let bans_b = BansV1(reversed);
let mut rotated = bans.clone();
rotated.rotate_left(7);
let bans_c = BansV1(rotated);
let err_a = bans_a.verify(&parent_state, ¶meters).unwrap_err();
let err_b = bans_b.verify(&parent_state, ¶meters).unwrap_err();
let err_c = bans_c.verify(&parent_state, ¶meters).unwrap_err();
let ids_a = extract_ban_ids(&err_a);
let ids_b = extract_ban_ids(&err_b);
let ids_c = extract_ban_ids(&err_c);
assert_eq!(
ids_a, ids_b,
"CONVERGENCE FAILURE: State A and B identified different excess bans"
);
assert_eq!(
ids_b, ids_c,
"CONVERGENCE FAILURE: State B and C identified different excess bans"
);
assert_eq!(ids_a.len(), 5, "Should identify exactly 5 excess bans");
}
#[test]
fn test_member_permutation_convergence() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let mut members: Vec<AuthorizedMember> = Vec::new();
for _ in 0..10 {
let (member, _) = create_test_member(owner_id, owner_id);
let auth_member = create_authorized_member(member, &owner_signing_key);
members.push(auth_member);
}
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 5;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let permutations: Vec<Vec<AuthorizedMember>> = vec![
members.clone(),
members.iter().rev().cloned().collect(),
{
let mut p = members.clone();
p.rotate_left(3);
p
},
{
let mut p = members.clone();
p.rotate_right(5);
p
},
{
let mut p: Vec<AuthorizedMember> = Vec::new();
for i in (0..members.len()).step_by(2) {
p.push(members[i].clone());
}
for i in (1..members.len()).step_by(2) {
p.push(members[i].clone());
}
p
},
];
let mut results: Vec<Vec<MemberId>> = Vec::new();
for perm in permutations {
let mut state = MembersV1 { members: perm };
state
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut ids: Vec<_> = state.members.iter().map(|m| m.member.id()).collect();
ids.sort();
results.push(ids);
}
let first = &results[0];
for (i, result) in results.iter().enumerate().skip(1) {
assert_eq!(
first, result,
"CONVERGENCE FAILURE: Permutation {} produced different result than permutation 0",
i
);
}
}
#[test]
fn test_random_operation_sequence_convergence() {
use rand::seq::SliceRandom;
use rand::SeedableRng;
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let mut member_pool: Vec<(AuthorizedMember, SigningKey)> = Vec::new();
for _ in 0..20 {
let (member, signing_key) = create_test_member(owner_id, owner_id);
let auth_member = create_authorized_member(member, &owner_signing_key);
member_pool.push((auth_member, signing_key));
}
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 8;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let member_refs: Vec<&AuthorizedMember> = member_pool.iter().map(|(m, _)| m).collect();
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let mut sequences: Vec<Vec<AuthorizedMember>> = Vec::new();
for _ in 0..5 {
let mut seq: Vec<&AuthorizedMember> = member_refs.clone();
seq.shuffle(&mut rng);
sequences.push(seq.into_iter().cloned().collect());
}
let mut final_states: Vec<Vec<MemberId>> = Vec::new();
for seq in sequences {
let mut state = MembersV1::default();
for chunk in seq.chunks(4) {
let delta = MembersDelta::new(chunk.to_vec());
state
.apply_delta(&parent_state, ¶meters, &Some(delta))
.expect("apply_delta should succeed");
}
let mut ids: Vec<_> = state.members.iter().map(|m| m.member.id()).collect();
ids.sort();
final_states.push(ids);
}
let first = &final_states[0];
for (i, state) in final_states.iter().enumerate().skip(1) {
assert_eq!(
first, state,
"CONVERGENCE FAILURE: Sequence {} produced different final state",
i
);
}
}
#[test]
fn test_message_varying_limits_convergence() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let base_time = SystemTime::now();
let mut messages: Vec<AuthorizedMessageV1> = Vec::new();
for i in 0..30 {
let msg = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: owner_id,
time: base_time + std::time::Duration::from_secs(i as u64),
content: RoomMessageBody::public(format!("Message {}", i)),
},
&owner_signing_key,
);
messages.push(msg);
}
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
for max_messages in [5, 10, 15, 20, 25] {
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_recent_messages = max_messages;
parent_state.configuration.configuration.max_message_size = 1000;
let mut state_forward = MessagesV1 {
messages: messages.clone(),
..Default::default()
};
state_forward
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut reversed = messages.clone();
reversed.reverse();
let mut state_backward = MessagesV1 {
messages: reversed,
..Default::default()
};
state_backward
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
assert_eq!(
state_forward.messages.len(),
max_messages,
"Forward state should have {} messages",
max_messages
);
assert_eq!(
state_backward.messages.len(),
max_messages,
"Backward state should have {} messages",
max_messages
);
let ids_forward: Vec<_> = state_forward.messages.iter().map(|m| m.id()).collect();
let ids_backward: Vec<_> = state_backward.messages.iter().map(|m| m.id()).collect();
assert_eq!(
ids_forward, ids_backward,
"CONVERGENCE FAILURE: Different orders with max_messages={} produced different states",
max_messages
);
}
}
#[test]
fn test_member_exactly_at_capacity() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let mut members: Vec<AuthorizedMember> = Vec::new();
for _ in 0..5 {
let (member, _) = create_test_member(owner_id, owner_id);
let auth_member = create_authorized_member(member, &owner_signing_key);
members.push(auth_member);
}
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 5;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut state_a = MembersV1 {
members: members.clone(),
};
state_a
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut reversed = members.clone();
reversed.reverse();
let mut state_b = MembersV1 { members: reversed };
state_b
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
assert_eq!(state_a.members.len(), 5);
assert_eq!(state_b.members.len(), 5);
let mut ids_a: Vec<_> = state_a.members.iter().map(|m| m.member.id()).collect();
let mut ids_b: Vec<_> = state_b.members.iter().map(|m| m.member.id()).collect();
ids_a.sort();
ids_b.sort();
assert_eq!(ids_a, ids_b, "At capacity, all members should be preserved");
}
#[test]
fn test_member_one_over_capacity() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let mut members: Vec<AuthorizedMember> = Vec::new();
for _ in 0..6 {
let (member, _) = create_test_member(owner_id, owner_id);
let auth_member = create_authorized_member(member, &owner_signing_key);
members.push(auth_member);
}
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 5;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut state_a = MembersV1 {
members: members.clone(),
};
state_a
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut reversed = members.clone();
reversed.reverse();
let mut state_b = MembersV1 { members: reversed };
state_b
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
assert_eq!(state_a.members.len(), 5);
assert_eq!(state_b.members.len(), 5);
let mut ids_a: Vec<_> = state_a.members.iter().map(|m| m.member.id()).collect();
let mut ids_b: Vec<_> = state_b.members.iter().map(|m| m.member.id()).collect();
ids_a.sort();
ids_b.sort();
assert_eq!(
ids_a, ids_b,
"One over capacity: same member should be removed regardless of order"
);
}
#[test]
fn test_messages_all_identical_timestamps() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let same_time = SystemTime::now();
let mut messages: Vec<AuthorizedMessageV1> = Vec::new();
for i in 0..10 {
let msg = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: owner_id,
time: same_time,
content: RoomMessageBody::public(format!("Message {}", i)),
},
&owner_signing_key,
);
messages.push(msg);
}
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_recent_messages = 5;
parent_state.configuration.configuration.max_message_size = 1000;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let orderings: Vec<Vec<AuthorizedMessageV1>> = vec![
messages.clone(),
messages.iter().rev().cloned().collect(),
{
let mut r = messages.clone();
r.rotate_left(3);
r
},
{
let mut r = messages.clone();
r.rotate_right(7);
r
},
];
let mut results: Vec<Vec<MessageId>> = Vec::new();
for ordering in orderings {
let mut state = MessagesV1 {
messages: ordering,
..Default::default()
};
state
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let ids: Vec<_> = state.messages.iter().map(|m| m.id()).collect();
results.push(ids);
}
let first = &results[0];
for (i, result) in results.iter().enumerate().skip(1) {
assert_eq!(
first, result,
"CONVERGENCE FAILURE: Ordering {} produced different result with identical timestamps",
i
);
}
}
#[test]
fn test_deep_invite_chains() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let mut chain: Vec<(AuthorizedMember, SigningKey)> = Vec::new();
let (first_member, first_sk) = create_test_member(owner_id, owner_id);
let first_auth = create_authorized_member(first_member.clone(), &owner_signing_key);
chain.push((first_auth, first_sk));
for _ in 1..12 {
let (prev_auth, prev_sk) = chain.last().unwrap();
let (member, signing_key) = create_test_member(owner_id, prev_auth.member.id());
let auth_member = create_authorized_member(member, prev_sk);
chain.push((auth_member, signing_key));
}
let mut depth_0_members: Vec<AuthorizedMember> = Vec::new();
for _ in 0..3 {
let (member, _) = create_test_member(owner_id, owner_id);
let auth_member = create_authorized_member(member, &owner_signing_key);
depth_0_members.push(auth_member);
}
let mut all_members: Vec<AuthorizedMember> = chain.iter().map(|(m, _)| m.clone()).collect();
all_members.extend(depth_0_members);
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 10;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut state_a = MembersV1 {
members: all_members.clone(),
};
state_a
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut reversed = all_members.clone();
reversed.reverse();
let mut state_b = MembersV1 { members: reversed };
state_b
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
assert_eq!(state_a.members.len(), 10);
assert_eq!(state_b.members.len(), 10);
let mut ids_a: Vec<_> = state_a.members.iter().map(|m| m.member.id()).collect();
let mut ids_b: Vec<_> = state_b.members.iter().map(|m| m.member.id()).collect();
ids_a.sort();
ids_b.sort();
assert_eq!(
ids_a, ids_b,
"Deep invite chains: same members should be kept"
);
let deep_chain_ids: Vec<MemberId> = chain[7..12].iter().map(|(m, _)| m.member.id()).collect();
for deep_id in &deep_chain_ids {
assert!(
!ids_a.contains(deep_id),
"Deep chain member should have been removed"
);
}
}
#[test]
fn test_concurrent_adds_and_bans() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let mut members: Vec<AuthorizedMember> = Vec::new();
for _ in 0..8 {
let (member, _) = create_test_member(owner_id, owner_id);
let auth_member = create_authorized_member(member, &owner_signing_key);
members.push(auth_member);
}
let ban_time = SystemTime::now();
let bans = BansV1(vec![
AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: ban_time,
banned_user: members[0].member.id(),
},
owner_id,
&owner_signing_key,
),
AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: ban_time,
banned_user: members[1].member.id(),
},
owner_id,
&owner_signing_key,
),
]);
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 5;
parent_state.configuration.configuration.max_user_bans = 10;
parent_state.bans = bans;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut state_a = MembersV1 {
members: members.clone(),
};
state_a
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut reversed = members.clone();
reversed.reverse();
let mut state_b = MembersV1 { members: reversed };
state_b
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
assert_eq!(state_a.members.len(), 5);
assert_eq!(state_b.members.len(), 5);
let ids_a: Vec<_> = state_a.members.iter().map(|m| m.member.id()).collect();
assert!(!ids_a.contains(&members[0].member.id()));
assert!(!ids_a.contains(&members[1].member.id()));
let mut ids_a_sorted = ids_a.clone();
let mut ids_b: Vec<_> = state_b.members.iter().map(|m| m.member.id()).collect();
ids_a_sorted.sort();
ids_b.sort();
assert_eq!(ids_a_sorted, ids_b, "States should converge after bans");
}
#[test]
fn test_regression_member_truncation_order_dependent() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let mut members: Vec<AuthorizedMember> = Vec::new();
for _ in 0..5 {
let (member, _) = create_test_member(owner_id, owner_id);
let auth_member = create_authorized_member(member, &owner_signing_key);
members.push(auth_member);
}
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 2;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut state_a = MembersV1::default();
let delta_a = MembersDelta::new(members.clone());
state_a
.apply_delta(&parent_state, ¶meters, &Some(delta_a))
.expect("apply_delta should succeed");
let mut reversed = members.clone();
reversed.reverse();
let mut state_b = MembersV1::default();
let delta_b = MembersDelta::new(reversed);
state_b
.apply_delta(&parent_state, ¶meters, &Some(delta_b))
.expect("apply_delta should succeed");
assert_eq!(state_a.members.len(), 2);
assert_eq!(state_b.members.len(), 2);
let mut ids_a: Vec<_> = state_a.members.iter().map(|m| m.member.id()).collect();
let mut ids_b: Vec<_> = state_b.members.iter().map(|m| m.member.id()).collect();
ids_a.sort();
ids_b.sort();
assert_eq!(
ids_a, ids_b,
"REGRESSION: Member truncation is still order-dependent!\n\
This would have failed before the fix was applied."
);
let mut all_ids: Vec<_> = members.iter().map(|m| m.member.id()).collect();
all_ids.sort();
let expected_kept: Vec<MemberId> = all_ids[0..2].to_vec();
assert_eq!(
ids_a, expected_kept,
"The members with lowest IDs should be kept"
);
}
#[test]
fn test_regression_member_excess_removal_tiebreak() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let mut members: Vec<AuthorizedMember> = Vec::new();
for _ in 0..10 {
let (member, _) = create_test_member(owner_id, owner_id);
let auth_member = create_authorized_member(member, &owner_signing_key);
members.push(auth_member);
}
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 5;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut first_result: Option<Vec<MemberId>> = None;
for rotation in 0..10 {
let mut rotated = members.clone();
rotated.rotate_left(rotation);
let mut state = MembersV1 { members: rotated };
state
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let mut ids: Vec<_> = state.members.iter().map(|m| m.member.id()).collect();
ids.sort();
if let Some(ref first) = first_result {
assert_eq!(
first, &ids,
"REGRESSION: Member excess removal tie-breaking is non-deterministic!\n\
Rotation {} produced different result. This would have failed before the fix.",
rotation
);
} else {
first_result = Some(ids);
}
}
let mut all_ids: Vec<_> = members.iter().map(|m| m.member.id()).collect();
all_ids.sort();
let expected_kept: Vec<MemberId> = all_ids[0..5].to_vec();
assert_eq!(
first_result.unwrap(),
expected_kept,
"The 5 members with lowest IDs should be kept"
);
}
#[test]
fn test_regression_ban_excess_identification() {
use std::collections::HashSet;
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let mut members: Vec<AuthorizedMember> = Vec::new();
for _ in 0..10 {
let (member, _) = create_test_member(owner_id, owner_id);
let auth_member = create_authorized_member(member, &owner_signing_key);
members.push(auth_member);
}
let same_time = SystemTime::now();
let mut bans: Vec<AuthorizedUserBan> = Vec::new();
for member in members.iter().take(8) {
let ban = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: same_time,
banned_user: member.member.id(),
},
owner_id,
&owner_signing_key,
);
bans.push(ban);
}
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_user_bans = 5;
parent_state.members = MembersV1 {
members: members.clone(),
};
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
fn extract_ban_ids(err: &str) -> HashSet<String> {
err.split("BanId(FastHash(")
.skip(1)
.filter_map(|s| s.split("))").next())
.map(|s| s.to_string())
.collect()
}
let mut ban_id_sets: Vec<HashSet<String>> = Vec::new();
for rotation in 0..8 {
let mut rotated = bans.clone();
rotated.rotate_left(rotation);
let bans_state = BansV1(rotated);
let err = bans_state.verify(&parent_state, ¶meters).unwrap_err();
ban_id_sets.push(extract_ban_ids(&err));
}
let first = &ban_id_sets[0];
for (i, ban_ids) in ban_id_sets.iter().enumerate().skip(1) {
assert_eq!(
first, ban_ids,
"REGRESSION: Ban excess identification is non-deterministic!\n\
Rotation {} identified different excess bans. This would have failed before the fix.",
i
);
}
assert_eq!(first.len(), 3, "Should identify exactly 3 excess bans");
}
#[test]
fn test_regression_message_pruning_order() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let base_time = SystemTime::now();
let mut messages: Vec<AuthorizedMessageV1> = Vec::new();
for i in 0..20 {
let time_offset = (i / 4) as u64; let time = base_time + std::time::Duration::from_secs(time_offset);
let msg = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: owner_id,
time,
content: RoomMessageBody::public(format!("Message {}", i)),
},
&owner_signing_key,
);
messages.push(msg);
}
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_recent_messages = 10;
parent_state.configuration.configuration.max_message_size = 1000;
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let mut first_result: Option<Vec<MessageId>> = None;
for rotation in 0..10 {
let mut rotated = messages.clone();
rotated.rotate_left(rotation * 2);
let mut state = MessagesV1 {
messages: rotated,
..Default::default()
};
state
.apply_delta(&parent_state, ¶meters, &None)
.expect("apply_delta should succeed");
let ids: Vec<_> = state.messages.iter().map(|m| m.id()).collect();
if let Some(ref first) = first_result {
assert_eq!(
first,
&ids,
"REGRESSION: Message pruning is non-deterministic!\n\
Rotation {} produced different message order. This would have failed before the fix.",
rotation
);
} else {
first_result = Some(ids);
}
}
}
use river_core::room_state::member_info::{AuthorizedMemberInfo, MemberInfo};
#[test]
fn test_full_state_merge_commutativity() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let (member_a, member_a_sk) = create_test_member(owner_id, owner_id);
let (member_b, member_b_sk) = create_test_member(owner_id, owner_id);
let (member_c, _member_c_sk) = create_test_member(owner_id, owner_id);
let auth_member_a = create_authorized_member(member_a.clone(), &owner_signing_key);
let auth_member_b = create_authorized_member(member_b.clone(), &owner_signing_key);
let auth_member_c = create_authorized_member(member_c.clone(), &owner_signing_key);
let info_a = AuthorizedMemberInfo::new_with_member_key(
MemberInfo::new_public(member_a.id(), 1, "Alice".to_string()),
&member_a_sk,
);
let info_b = AuthorizedMemberInfo::new_with_member_key(
MemberInfo::new_public(member_b.id(), 1, "Bob".to_string()),
&member_b_sk,
);
let owner_info = AuthorizedMemberInfo::new(
MemberInfo::new_public(owner_id, 1, "Owner".to_string()),
&owner_signing_key,
);
let time_1 = SystemTime::now();
let time_2 = time_1 + std::time::Duration::from_secs(1);
let time_3 = time_1 + std::time::Duration::from_secs(2);
let msg_1 = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: owner_id,
time: time_1,
content: RoomMessageBody::public("Hello from owner".to_string()),
},
&owner_signing_key,
);
let msg_2 = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: member_a.id(),
time: time_2,
content: RoomMessageBody::public("Hello from Alice".to_string()),
},
&member_a_sk,
);
let msg_3 = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: member_b.id(),
time: time_3,
content: RoomMessageBody::public("Hello from Bob".to_string()),
},
&member_b_sk,
);
let mut state_a = ChatRoomStateV1::default();
state_a.configuration.configuration.max_members = 10;
state_a.configuration.configuration.max_recent_messages = 100;
state_a.configuration.configuration.max_message_size = 1000;
state_a.members.members.push(auth_member_a.clone());
state_a.members.members.push(auth_member_c.clone());
state_a.recent_messages.messages.push(msg_1.clone());
state_a.recent_messages.messages.push(msg_2.clone());
state_a.member_info.member_info.push(owner_info.clone());
state_a.member_info.member_info.push(info_a.clone());
let mut state_b = ChatRoomStateV1::default();
state_b.configuration.configuration.max_members = 10;
state_b.configuration.configuration.max_recent_messages = 100;
state_b.configuration.configuration.max_message_size = 1000;
state_b.members.members.push(auth_member_b.clone());
state_b.members.members.push(auth_member_c.clone());
state_b.recent_messages.messages.push(msg_1.clone());
state_b.recent_messages.messages.push(msg_3.clone());
state_b.member_info.member_info.push(owner_info.clone());
state_b.member_info.member_info.push(info_b.clone());
let mut merged_ab = state_a.clone();
merged_ab
.merge(&state_a, ¶meters, &state_b)
.expect("merge A+B should succeed");
let mut merged_ba = state_b.clone();
merged_ba
.merge(&state_b, ¶meters, &state_a)
.expect("merge B+A should succeed");
let mut bytes_ab = Vec::new();
ciborium::ser::into_writer(&merged_ab, &mut bytes_ab).expect("serialize merged_ab");
let mut bytes_ba = Vec::new();
ciborium::ser::into_writer(&merged_ba, &mut bytes_ba).expect("serialize merged_ba");
assert_eq!(
bytes_ab,
bytes_ba,
"COMMUTATIVITY FAILURE: merge(A,B) != merge(B,A) at byte level!\n\
merged_ab members: {:?}\n\
merged_ba members: {:?}\n\
merged_ab messages: {:?}\n\
merged_ba messages: {:?}\n\
merged_ab member_info: {:?}\n\
merged_ba member_info: {:?}",
merged_ab
.members
.members
.iter()
.map(|m| m.member.id())
.collect::<Vec<_>>(),
merged_ba
.members
.members
.iter()
.map(|m| m.member.id())
.collect::<Vec<_>>(),
merged_ab
.recent_messages
.messages
.iter()
.map(|m| m.id())
.collect::<Vec<_>>(),
merged_ba
.recent_messages
.messages
.iter()
.map(|m| m.id())
.collect::<Vec<_>>(),
merged_ab
.member_info
.member_info
.iter()
.map(|i| i.member_info.member_id)
.collect::<Vec<_>>(),
merged_ba
.member_info
.member_info
.iter()
.map(|i| i.member_info.member_id)
.collect::<Vec<_>>(),
);
}
#[test]
fn test_regression_combined_scenario() {
let owner_signing_key = SigningKey::generate(&mut OsRng);
let owner_verifying_key = owner_signing_key.verifying_key();
let owner_id: MemberId = owner_verifying_key.into();
let mut members: Vec<(AuthorizedMember, SigningKey)> = Vec::new();
let mut level_0: Vec<(AuthorizedMember, SigningKey)> = Vec::new();
for _ in 0..10 {
let (member, signing_key) = create_test_member(owner_id, owner_id);
let auth_member = create_authorized_member(member, &owner_signing_key);
level_0.push((auth_member.clone(), signing_key.clone()));
members.push((auth_member, signing_key));
}
for i in 0..15 {
let inviter = &level_0[i % level_0.len()];
let (member, signing_key) = create_test_member(owner_id, inviter.0.member.id());
let auth_member = create_authorized_member(member, &inviter.1);
members.push((auth_member, signing_key));
}
for i in 0..5 {
let inviter = &members[10 + (i % 15)]; let (member, signing_key) = create_test_member(owner_id, inviter.0.member.id());
let auth_member = create_authorized_member(member, &inviter.1);
members.push((auth_member, signing_key));
}
assert_eq!(members.len(), 30);
let base_time = SystemTime::now();
let mut messages: Vec<AuthorizedMessageV1> = Vec::new();
for i in 0..50 {
let time_offset = (i / 3) as u64; let time = base_time + std::time::Duration::from_secs(time_offset);
let author_idx = i % members.len();
let msg = AuthorizedMessageV1::new(
MessageV1 {
room_owner: owner_id,
author: members[author_idx].0.member.id(),
time,
content: RoomMessageBody::public(format!("Message {}", i)),
},
&members[author_idx].1,
);
messages.push(msg);
}
let ban_time = SystemTime::now();
let mut bans: Vec<AuthorizedUserBan> = Vec::new();
for i in 0..3 {
let ban = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: ban_time, banned_user: members[25 + i].0.member.id(), },
owner_id,
&owner_signing_key,
);
bans.push(ban);
}
let mut parent_state = ChatRoomStateV1::default();
parent_state.configuration.configuration.max_members = 20;
parent_state.configuration.configuration.max_recent_messages = 30;
parent_state.configuration.configuration.max_message_size = 1000;
parent_state.configuration.configuration.max_user_bans = 10;
parent_state.bans = BansV1(bans);
let parameters = ChatRoomParametersV1 {
owner: owner_verifying_key,
};
let member_list: Vec<AuthorizedMember> = members.iter().map(|(m, _)| m.clone()).collect();
let orderings = vec![
(member_list.clone(), messages.clone()),
(
member_list.iter().rev().cloned().collect(),
messages.iter().rev().cloned().collect(),
),
(
{
let mut m = member_list.clone();
m.rotate_left(11);
m
},
{
let mut msgs = messages.clone();
msgs.rotate_left(17);
msgs
},
),
];
let mut final_states: Vec<(Vec<MemberId>, Vec<MessageId>)> = Vec::new();
for (member_ordering, msg_ordering) in orderings {
let mut local_parent = parent_state.clone();
local_parent.members = MembersV1 {
members: member_ordering.clone(),
};
local_parent
.members
.apply_delta(&parent_state, ¶meters, &None)
.expect("members apply_delta should succeed");
let mut msg_state = MessagesV1 {
messages: msg_ordering,
..Default::default()
};
msg_state
.apply_delta(&local_parent, ¶meters, &None)
.expect("messages apply_delta should succeed");
let mut member_ids: Vec<_> = local_parent
.members
.members
.iter()
.map(|m| m.member.id())
.collect();
member_ids.sort();
let msg_ids: Vec<_> = msg_state.messages.iter().map(|m| m.id()).collect();
final_states.push((member_ids, msg_ids));
}
let (first_members, first_messages) = &final_states[0];
for (i, (members_result, messages_result)) in final_states.iter().enumerate().skip(1) {
assert_eq!(
first_members, members_result,
"REGRESSION: Combined scenario - members don't converge for ordering {}",
i
);
assert_eq!(
first_messages, messages_result,
"REGRESSION: Combined scenario - messages don't converge for ordering {}",
i
);
}
}
use river_core::room_state::configuration::{AuthorizedConfigurationV1, Configuration};
fn create_test_config(owner_sk: &SigningKey) -> AuthorizedConfigurationV1 {
AuthorizedConfigurationV1::new(
Configuration {
max_members: 10,
max_user_bans: 10,
max_recent_messages: 100,
max_message_size: 1000,
..Default::default()
},
owner_sk,
)
}
#[test]
fn test_merge_with_bans_from_member_not_in_other_state() {
let owner_sk = SigningKey::generate(&mut OsRng);
let owner_vk = owner_sk.verifying_key();
let owner_id: MemberId = owner_vk.into();
let parameters = ChatRoomParametersV1 { owner: owner_vk };
let config = create_test_config(&owner_sk);
let (member_x, member_x_sk) = create_test_member(owner_id, owner_id);
let member_x_id = member_x.id();
let auth_member_x = create_authorized_member(member_x.clone(), &owner_sk);
let (member_y, _) = create_test_member(owner_id, member_x_id);
let member_y_id = member_y.id();
let (member_z, member_z_sk) = create_test_member(owner_id, owner_id);
let auth_member_z = create_authorized_member(member_z.clone(), &owner_sk);
let ban_y_by_x = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: SystemTime::now(),
banned_user: member_y_id,
},
member_x_id,
&member_x_sk,
);
let msg_x = create_test_msg(owner_id, member_x_id, &member_x_sk, 0);
let msg_z = create_test_msg(owner_id, member_z.id(), &member_z_sk, 1);
let state_a = ChatRoomStateV1 {
configuration: config.clone(),
members: MembersV1 {
members: vec![auth_member_z.clone()],
},
recent_messages: MessagesV1 {
messages: vec![msg_z.clone()],
..Default::default()
},
..Default::default()
};
let state_b = ChatRoomStateV1 {
configuration: config,
members: MembersV1 {
members: vec![auth_member_x.clone(), auth_member_z.clone()],
},
bans: BansV1(vec![ban_y_by_x.clone()]),
recent_messages: MessagesV1 {
messages: vec![msg_x, msg_z],
..Default::default()
},
..Default::default()
};
let mut merged_ab = state_a.clone();
merged_ab
.merge(&state_a, ¶meters, &state_b)
.expect("merge A+B should succeed — banning member X is added by members delta");
let final_member_ids: Vec<MemberId> = merged_ab
.members
.members
.iter()
.map(|m| m.member.id())
.collect();
assert!(
final_member_ids.contains(&member_x_id),
"Member X should be present after merge"
);
assert!(
!final_member_ids.contains(&member_y_id),
"Member Y should not appear (already removed in state B)"
);
assert!(
final_member_ids.contains(&member_z.id()),
"Member Z should be present after merge"
);
assert_eq!(merged_ab.bans.0.len(), 1, "Ban should be retained");
assert_eq!(merged_ab.bans.0[0].banned_by, member_x_id);
assert!(
merged_ab.verify(&merged_ab, ¶meters).is_ok(),
"Merged state should verify: {:?}",
merged_ab.verify(&merged_ab, ¶meters)
);
}
#[test]
fn test_merge_commutativity_with_owner_bans() {
let owner_sk = SigningKey::generate(&mut OsRng);
let owner_vk = owner_sk.verifying_key();
let owner_id: MemberId = owner_vk.into();
let parameters = ChatRoomParametersV1 { owner: owner_vk };
let config = create_test_config(&owner_sk);
let (member_a, member_a_sk) = create_test_member(owner_id, owner_id);
let member_a_id = member_a.id();
let auth_member_a = create_authorized_member(member_a.clone(), &owner_sk);
let (member_b, member_b_sk) = create_test_member(owner_id, owner_id);
let auth_member_b = create_authorized_member(member_b.clone(), &owner_sk);
let (member_c, member_c_sk) = create_test_member(owner_id, owner_id);
let auth_member_c = create_authorized_member(member_c.clone(), &owner_sk);
let (member_d, member_d_sk) = create_test_member(owner_id, owner_id);
let member_d_id = member_d.id();
let auth_member_d = create_authorized_member(member_d.clone(), &owner_sk);
let ban_a = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: SystemTime::now(),
banned_user: member_a_id,
},
owner_id,
&owner_sk,
);
let ban_d = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: SystemTime::now() + std::time::Duration::from_secs(1),
banned_user: member_d_id,
},
owner_id,
&owner_sk,
);
let msg_a = create_test_msg(owner_id, member_a_id, &member_a_sk, 0);
let msg_b = create_test_msg(owner_id, member_b.id(), &member_b_sk, 1);
let msg_c = create_test_msg(owner_id, member_c.id(), &member_c_sk, 2);
let msg_d = create_test_msg(owner_id, member_d_id, &member_d_sk, 3);
let state_1 = ChatRoomStateV1 {
configuration: config.clone(),
members: MembersV1 {
members: vec![
auth_member_a.clone(),
auth_member_b.clone(),
auth_member_c.clone(),
],
},
bans: BansV1(vec![ban_d.clone()]),
recent_messages: MessagesV1 {
messages: vec![msg_a.clone(), msg_b.clone(), msg_c.clone()],
..Default::default()
},
..Default::default()
};
let state_2 = ChatRoomStateV1 {
configuration: config,
members: MembersV1 {
members: vec![
auth_member_b.clone(),
auth_member_c.clone(),
auth_member_d.clone(),
],
},
bans: BansV1(vec![ban_a.clone()]),
recent_messages: MessagesV1 {
messages: vec![msg_b, msg_c, msg_d],
..Default::default()
},
..Default::default()
};
let mut merged_12 = state_1.clone();
merged_12
.merge(&state_1, ¶meters, &state_2)
.expect("merge 1+2 should succeed");
let mut merged_21 = state_2.clone();
merged_21
.merge(&state_2, ¶meters, &state_1)
.expect("merge 2+1 should succeed");
for (label, merged) in [("merged_12", &merged_12), ("merged_21", &merged_21)] {
let ids: Vec<MemberId> = merged
.members
.members
.iter()
.map(|m| m.member.id())
.collect();
assert!(!ids.contains(&member_a_id), "{}: A should be banned", label);
assert!(ids.contains(&member_b.id()), "{}: B should remain", label);
assert!(ids.contains(&member_c.id()), "{}: C should remain", label);
assert!(!ids.contains(&member_d_id), "{}: D should be banned", label);
assert!(
merged.verify(merged, ¶meters).is_ok(),
"{} should verify: {:?}",
label,
merged.verify(merged, ¶meters)
);
}
let mut bytes_12 = Vec::new();
ciborium::ser::into_writer(&merged_12, &mut bytes_12).expect("serialize merged_12");
let mut bytes_21 = Vec::new();
ciborium::ser::into_writer(&merged_21, &mut bytes_21).expect("serialize merged_21");
assert_eq!(
bytes_12,
bytes_21,
"COMMUTATIVITY FAILURE: merge(1,2) != merge(2,1) with diverged owner bans!\n\
merged_12 members: {:?}\nmerged_21 members: {:?}\n\
merged_12 bans: {:?}\nmerged_21 bans: {:?}",
merged_12
.members
.members
.iter()
.map(|m| m.member.id())
.collect::<Vec<_>>(),
merged_21
.members
.members
.iter()
.map(|m| m.member.id())
.collect::<Vec<_>>(),
merged_12
.bans
.0
.iter()
.map(|b| (b.banned_by, b.ban.banned_user))
.collect::<Vec<_>>(),
merged_21
.bans
.0
.iter()
.map(|b| (b.banned_by, b.ban.banned_user))
.collect::<Vec<_>>(),
);
}
#[test]
fn test_merge_cascade_ban_cleanup() {
let owner_sk = SigningKey::generate(&mut OsRng);
let owner_vk = owner_sk.verifying_key();
let owner_id: MemberId = owner_vk.into();
let parameters = ChatRoomParametersV1 { owner: owner_vk };
let config = create_test_config(&owner_sk);
let (member_a, member_a_sk) = create_test_member(owner_id, owner_id);
let member_a_id = member_a.id();
let auth_member_a = create_authorized_member(member_a.clone(), &owner_sk);
let (member_b, member_b_sk) = create_test_member(owner_id, member_a_id);
let member_b_id = member_b.id();
let auth_member_b = create_authorized_member(member_b.clone(), &member_a_sk);
let (member_c, member_c_sk) = create_test_member(owner_id, owner_id);
let auth_member_c = create_authorized_member(member_c.clone(), &owner_sk);
let ban_b_by_a = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: SystemTime::now(),
banned_user: member_b_id,
},
member_a_id,
&member_a_sk,
);
let ban_a_by_owner = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: SystemTime::now() + std::time::Duration::from_secs(1),
banned_user: member_a_id,
},
owner_id,
&owner_sk,
);
let msg_a = create_test_msg(owner_id, member_a_id, &member_a_sk, 0);
let msg_b = create_test_msg(owner_id, member_b_id, &member_b_sk, 1);
let msg_c = create_test_msg(owner_id, member_c.id(), &member_c_sk, 2);
let state_old = ChatRoomStateV1 {
configuration: config.clone(),
members: MembersV1 {
members: vec![
auth_member_a.clone(),
auth_member_b.clone(),
auth_member_c.clone(),
],
},
bans: BansV1(vec![ban_b_by_a.clone()]),
recent_messages: MessagesV1 {
messages: vec![msg_a.clone(), msg_b.clone(), msg_c.clone()],
..Default::default()
},
..Default::default()
};
let state_new = ChatRoomStateV1 {
configuration: config,
members: MembersV1 {
members: vec![
auth_member_a.clone(),
auth_member_b.clone(),
auth_member_c.clone(),
],
},
bans: BansV1(vec![ban_b_by_a.clone(), ban_a_by_owner.clone()]),
recent_messages: MessagesV1 {
messages: vec![msg_a, msg_b, msg_c],
..Default::default()
},
..Default::default()
};
let mut merged = state_old.clone();
merged
.merge(&state_old, ¶meters, &state_new)
.expect("merge should succeed");
let final_ids: Vec<MemberId> = merged
.members
.members
.iter()
.map(|m| m.member.id())
.collect();
assert!(
!final_ids.contains(&member_a_id),
"A should be removed (banned by owner)"
);
assert!(
!final_ids.contains(&member_b_id),
"B should be cascade-removed (invited by banned A)"
);
assert!(
final_ids.contains(&member_c.id()),
"C should remain (invited by owner)"
);
assert_eq!(
merged.bans.0.len(),
1,
"Only owner's ban should remain after orphan cleanup. Got: {:?}",
merged
.bans
.0
.iter()
.map(|b| (b.banned_by, b.ban.banned_user))
.collect::<Vec<_>>()
);
assert_eq!(merged.bans.0[0].banned_by, owner_id);
assert!(
merged.verify(&merged, ¶meters).is_ok(),
"Merged state should verify: {:?}",
merged.verify(&merged, ¶meters)
);
}
#[test]
fn test_merge_cascade_ban_commutativity() {
let owner_sk = SigningKey::generate(&mut OsRng);
let owner_vk = owner_sk.verifying_key();
let owner_id: MemberId = owner_vk.into();
let parameters = ChatRoomParametersV1 { owner: owner_vk };
let config = create_test_config(&owner_sk);
let (member_a, member_a_sk) = create_test_member(owner_id, owner_id);
let member_a_id = member_a.id();
let auth_member_a = create_authorized_member(member_a.clone(), &owner_sk);
let (member_b, _) = create_test_member(owner_id, member_a_id);
let member_b_id = member_b.id();
let auth_member_b = create_authorized_member(member_b.clone(), &member_a_sk);
let (member_c, _) = create_test_member(owner_id, owner_id);
let auth_member_c = create_authorized_member(member_c.clone(), &owner_sk);
let ban_b_by_a = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: SystemTime::now(),
banned_user: member_b_id,
},
member_a_id,
&member_a_sk,
);
let ban_a_by_owner = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: SystemTime::now() + std::time::Duration::from_secs(1),
banned_user: member_a_id,
},
owner_id,
&owner_sk,
);
let state_s1 = ChatRoomStateV1 {
configuration: config.clone(),
members: MembersV1 {
members: vec![
auth_member_a.clone(),
auth_member_b.clone(),
auth_member_c.clone(),
],
},
bans: BansV1(vec![ban_b_by_a.clone()]),
..Default::default()
};
let state_s2 = ChatRoomStateV1 {
configuration: config,
members: MembersV1 {
members: vec![auth_member_c.clone()],
},
bans: BansV1(vec![ban_a_by_owner.clone()]),
..Default::default()
};
let mut merged_12 = state_s1.clone();
merged_12
.merge(&state_s1, ¶meters, &state_s2)
.expect("merge S1+S2 should succeed");
let mut merged_21 = state_s2.clone();
merged_21
.merge(&state_s2, ¶meters, &state_s1)
.expect("merge S2+S1 should succeed — inviter A is in same delta as B");
assert!(
merged_12.verify(&merged_12, ¶meters).is_ok(),
"merged_12 should verify: {:?}",
merged_12.verify(&merged_12, ¶meters)
);
assert!(
merged_21.verify(&merged_21, ¶meters).is_ok(),
"merged_21 should verify: {:?}",
merged_21.verify(&merged_21, ¶meters)
);
let mut bytes_12 = Vec::new();
ciborium::ser::into_writer(&merged_12, &mut bytes_12).expect("serialize");
let mut bytes_21 = Vec::new();
ciborium::ser::into_writer(&merged_21, &mut bytes_21).expect("serialize");
assert_eq!(
bytes_12,
bytes_21,
"COMMUTATIVITY FAILURE: merge with cascade bans!\n\
merged_12 members: {:?}\nmerged_21 members: {:?}\n\
merged_12 bans: {:?}\nmerged_21 bans: {:?}",
merged_12
.members
.members
.iter()
.map(|m| m.member.id())
.collect::<Vec<_>>(),
merged_21
.members
.members
.iter()
.map(|m| m.member.id())
.collect::<Vec<_>>(),
merged_12
.bans
.0
.iter()
.map(|b| (b.banned_by, b.ban.banned_user))
.collect::<Vec<_>>(),
merged_21
.bans
.0
.iter()
.map(|b| (b.banned_by, b.ban.banned_user))
.collect::<Vec<_>>(),
);
}
#[test]
fn test_merge_owner_ban_across_diverged_states() {
let owner_sk = SigningKey::generate(&mut OsRng);
let owner_vk = owner_sk.verifying_key();
let owner_id: MemberId = owner_vk.into();
let parameters = ChatRoomParametersV1 { owner: owner_vk };
let config = create_test_config(&owner_sk);
let (member_a, member_a_sk) = create_test_member(owner_id, owner_id);
let member_a_id = member_a.id();
let auth_member_a = create_authorized_member(member_a.clone(), &owner_sk);
let (member_b, member_b_sk) = create_test_member(owner_id, owner_id);
let auth_member_b = create_authorized_member(member_b.clone(), &owner_sk);
let ban_a = AuthorizedUserBan::new(
UserBan {
owner_member_id: owner_id,
banned_at: SystemTime::now(),
banned_user: member_a_id,
},
owner_id,
&owner_sk,
);
let msg_a = create_test_msg(owner_id, member_a_id, &member_a_sk, 0);
let msg_b = create_test_msg(owner_id, member_b.id(), &member_b_sk, 1);
let state_1 = ChatRoomStateV1 {
configuration: config.clone(),
members: MembersV1 {
members: vec![auth_member_a.clone(), auth_member_b.clone()],
},
recent_messages: MessagesV1 {
messages: vec![msg_a.clone(), msg_b.clone()],
..Default::default()
},
..Default::default()
};
let state_2 = ChatRoomStateV1 {
configuration: config,
members: MembersV1 {
members: vec![auth_member_b.clone()],
},
bans: BansV1(vec![ban_a.clone()]),
recent_messages: MessagesV1 {
messages: vec![msg_a, msg_b],
..Default::default()
},
..Default::default()
};
let mut merged_12 = state_1.clone();
merged_12
.merge(&state_1, ¶meters, &state_2)
.expect("merge 1+2 should succeed");
let mut merged_21 = state_2.clone();
merged_21
.merge(&state_2, ¶meters, &state_1)
.expect("merge 2+1 should succeed");
for (label, merged) in [("merged_12", &merged_12), ("merged_21", &merged_21)] {
let ids: Vec<MemberId> = merged
.members
.members
.iter()
.map(|m| m.member.id())
.collect();
assert!(!ids.contains(&member_a_id), "{}: A should be banned", label);
assert!(ids.contains(&member_b.id()), "{}: B should remain", label);
assert!(
merged.verify(merged, ¶meters).is_ok(),
"{} should verify: {:?}",
label,
merged.verify(merged, ¶meters)
);
}
let mut bytes_12 = Vec::new();
ciborium::ser::into_writer(&merged_12, &mut bytes_12).unwrap();
let mut bytes_21 = Vec::new();
ciborium::ser::into_writer(&merged_21, &mut bytes_21).unwrap();
assert_eq!(bytes_12, bytes_21, "Owner ban merge must be commutative");
}