Run cargo +nightly fmt, add towncrier news fragment, remove plan documents that served their purpose during development. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
219 lines
6.7 KiB
Rust
219 lines
6.7 KiB
Rust
//! Cache consistency tests using a MockCache that mirrors the Service's
|
|
//! cache structures. These tests validate the algorithm/logic but do NOT
|
|
//! exercise the actual Service methods (which require async service
|
|
//! dependencies). See `tests.rs` for tests that call the extracted pure
|
|
//! logic functions directly.
|
|
|
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
|
|
use conduwuit_core::matrix::space_roles::RoleDefinition;
|
|
use ruma::{OwnedRoomId, OwnedUserId, room_id, user_id};
|
|
|
|
use super::tests::{make_requirements, make_roles, make_user_roles};
|
|
|
|
/// Simulates the full cache state for a space.
|
|
struct MockCache {
|
|
roles: HashMap<OwnedRoomId, BTreeMap<String, RoleDefinition>>,
|
|
user_roles: HashMap<OwnedRoomId, HashMap<OwnedUserId, HashSet<String>>>,
|
|
room_requirements: HashMap<OwnedRoomId, HashMap<OwnedRoomId, HashSet<String>>>,
|
|
room_to_space: HashMap<OwnedRoomId, HashSet<OwnedRoomId>>,
|
|
}
|
|
|
|
impl MockCache {
|
|
fn new() -> Self {
|
|
Self {
|
|
roles: HashMap::new(),
|
|
user_roles: HashMap::new(),
|
|
room_requirements: HashMap::new(),
|
|
room_to_space: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
fn add_space(&mut self, space: OwnedRoomId, roles: BTreeMap<String, RoleDefinition>) {
|
|
self.roles.insert(space, roles);
|
|
}
|
|
|
|
fn add_child(&mut self, space: &OwnedRoomId, child: OwnedRoomId) {
|
|
self.room_to_space
|
|
.entry(child)
|
|
.or_default()
|
|
.insert(space.clone());
|
|
}
|
|
|
|
fn assign_role(&mut self, space: &OwnedRoomId, user: OwnedUserId, role: String) {
|
|
self.user_roles
|
|
.entry(space.clone())
|
|
.or_default()
|
|
.entry(user)
|
|
.or_default()
|
|
.insert(role);
|
|
}
|
|
|
|
fn revoke_role(&mut self, space: &OwnedRoomId, user: &OwnedUserId, role: &str) {
|
|
if let Some(space_users) = self.user_roles.get_mut(space) {
|
|
if let Some(user_roles) = space_users.get_mut(user) {
|
|
user_roles.remove(role);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_room_requirements(
|
|
&mut self,
|
|
space: &OwnedRoomId,
|
|
room: OwnedRoomId,
|
|
reqs: HashSet<String>,
|
|
) {
|
|
self.room_requirements
|
|
.entry(space.clone())
|
|
.or_default()
|
|
.insert(room, reqs);
|
|
}
|
|
|
|
fn user_qualifies(
|
|
&self,
|
|
space: &OwnedRoomId,
|
|
room: &OwnedRoomId,
|
|
user: &OwnedUserId,
|
|
) -> bool {
|
|
let reqs = self.room_requirements.get(space).and_then(|r| r.get(room));
|
|
|
|
match reqs {
|
|
| None => true,
|
|
| Some(required) if required.is_empty() => true,
|
|
| Some(required) => {
|
|
let assigned = self.user_roles.get(space).and_then(|u| u.get(user));
|
|
match assigned {
|
|
| None => false,
|
|
| Some(roles) => required.iter().all(|r| roles.contains(r)),
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
fn get_power_level(&self, space: &OwnedRoomId, user: &OwnedUserId) -> Option<i64> {
|
|
let role_defs = self.roles.get(space)?;
|
|
let assigned = self.user_roles.get(space)?.get(user)?;
|
|
assigned
|
|
.iter()
|
|
.filter_map(|r| role_defs.get(r)?.power_level)
|
|
.max()
|
|
}
|
|
|
|
fn clear(&mut self) {
|
|
self.roles.clear();
|
|
self.user_roles.clear();
|
|
self.room_requirements.clear();
|
|
self.room_to_space.clear();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cache_populate_and_lookup() {
|
|
let mut cache = MockCache::new();
|
|
let space = room_id!("!space:example.com").to_owned();
|
|
let child = room_id!("!child:example.com").to_owned();
|
|
let alice = user_id!("@alice:example.com").to_owned();
|
|
|
|
cache.add_space(space.clone(), make_roles(&[("admin", Some(100)), ("nsfw", None)]));
|
|
cache.add_child(&space, child.clone());
|
|
cache.assign_role(&space, alice.clone(), "nsfw".to_owned());
|
|
cache.set_room_requirements(&space, child.clone(), make_requirements(&["nsfw"]));
|
|
|
|
assert!(cache.user_qualifies(&space, &child, &alice));
|
|
assert_eq!(cache.get_power_level(&space, &alice), None); // nsfw has no PL
|
|
}
|
|
|
|
#[test]
|
|
fn cache_invalidation_on_role_revoke() {
|
|
let mut cache = MockCache::new();
|
|
let space = room_id!("!space:example.com").to_owned();
|
|
let child = room_id!("!nsfw:example.com").to_owned();
|
|
let alice = user_id!("@alice:example.com").to_owned();
|
|
|
|
cache.add_space(space.clone(), make_roles(&[("nsfw", None)]));
|
|
cache.assign_role(&space, alice.clone(), "nsfw".to_owned());
|
|
cache.set_room_requirements(&space, child.clone(), make_requirements(&["nsfw"]));
|
|
|
|
assert!(cache.user_qualifies(&space, &child, &alice));
|
|
|
|
// Revoke
|
|
cache.revoke_role(&space, &alice, "nsfw");
|
|
assert!(!cache.user_qualifies(&space, &child, &alice));
|
|
}
|
|
|
|
#[test]
|
|
fn cache_invalidation_on_requirement_change() {
|
|
let mut cache = MockCache::new();
|
|
let space = room_id!("!space:example.com").to_owned();
|
|
let child = room_id!("!room:example.com").to_owned();
|
|
let alice = user_id!("@alice:example.com").to_owned();
|
|
|
|
cache.add_space(space.clone(), make_roles(&[("nsfw", None), ("vip", None)]));
|
|
cache.assign_role(&space, alice.clone(), "vip".to_owned());
|
|
cache.set_room_requirements(&space, child.clone(), make_requirements(&["vip"]));
|
|
|
|
assert!(cache.user_qualifies(&space, &child, &alice));
|
|
|
|
// Add nsfw requirement
|
|
cache.set_room_requirements(&space, child.clone(), make_requirements(&["vip", "nsfw"]));
|
|
assert!(!cache.user_qualifies(&space, &child, &alice));
|
|
}
|
|
|
|
#[test]
|
|
fn cache_clear_empties_all() {
|
|
let mut cache = MockCache::new();
|
|
let space = room_id!("!space:example.com").to_owned();
|
|
cache.add_space(space.clone(), make_roles(&[("admin", Some(100))]));
|
|
cache.assign_role(&space, user_id!("@alice:example.com").to_owned(), "admin".to_owned());
|
|
|
|
cache.clear();
|
|
|
|
assert!(cache.roles.is_empty());
|
|
assert!(cache.user_roles.is_empty());
|
|
assert!(cache.room_requirements.is_empty());
|
|
assert!(cache.room_to_space.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn cache_reverse_lookup_consistency() {
|
|
let mut cache = MockCache::new();
|
|
let space = room_id!("!space:example.com").to_owned();
|
|
let child1 = room_id!("!child1:example.com").to_owned();
|
|
let child2 = room_id!("!child2:example.com").to_owned();
|
|
|
|
cache.add_child(&space, child1.clone());
|
|
cache.add_child(&space, child2.clone());
|
|
|
|
assert!(cache.room_to_space.get(&child1).unwrap().contains(&space));
|
|
assert!(cache.room_to_space.get(&child2).unwrap().contains(&space));
|
|
assert!(
|
|
cache
|
|
.room_to_space
|
|
.get(room_id!("!unknown:example.com"))
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cache_power_level_updates_on_role_change() {
|
|
let mut cache = MockCache::new();
|
|
let space = room_id!("!space:example.com").to_owned();
|
|
let alice = user_id!("@alice:example.com").to_owned();
|
|
|
|
cache.add_space(space.clone(), make_roles(&[("admin", Some(100)), ("mod", Some(50))]));
|
|
|
|
// No roles -> no PL
|
|
assert_eq!(cache.get_power_level(&space, &alice), None);
|
|
|
|
// Assign mod -> PL 50
|
|
cache.assign_role(&space, alice.clone(), "mod".to_owned());
|
|
assert_eq!(cache.get_power_level(&space, &alice), Some(50));
|
|
|
|
// Also assign admin -> PL 100 (highest wins)
|
|
cache.assign_role(&space, alice.clone(), "admin".to_owned());
|
|
assert_eq!(cache.get_power_level(&space, &alice), Some(100));
|
|
|
|
// Revoke admin -> back to PL 50
|
|
cache.revoke_role(&space, &alice, "admin");
|
|
assert_eq!(cache.get_power_level(&space, &alice), Some(50));
|
|
}
|