- Fix doc comment referencing room_to_space instead of space_to_rooms - Add space_to_rooms forward index to design doc index table - Use Err! consistently for validation errors in admin commands - Rename test to follow deserialize_ prefix convention Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
202 lines
6.2 KiB
Rust
202 lines
6.2 KiB
Rust
//! Custom state event content types for space permission cascading.
|
|
//!
|
|
//! These events live in Space rooms and define roles, user-role assignments,
|
|
//! and room-role requirements.
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Custom event type for space role definitions.
|
|
pub const SPACE_ROLES_EVENT_TYPE: &str = "com.continuwuity.space.roles";
|
|
|
|
/// Custom event type for per-user role assignments within a space.
|
|
pub const SPACE_ROLE_MEMBER_EVENT_TYPE: &str = "com.continuwuity.space.role.member";
|
|
|
|
/// Custom event type for per-room role requirements within a space.
|
|
pub const SPACE_ROLE_ROOM_EVENT_TYPE: &str = "com.continuwuity.space.role.room";
|
|
|
|
/// Content for `com.continuwuity.space.roles` (state key: "")
|
|
///
|
|
/// Defines available roles for a Space.
|
|
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
|
pub struct SpaceRolesEventContent {
|
|
pub roles: BTreeMap<String, RoleDefinition>,
|
|
}
|
|
|
|
/// A single role definition within a Space.
|
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
|
pub struct RoleDefinition {
|
|
pub description: String,
|
|
|
|
/// If present, users with this role receive this power level in child
|
|
/// rooms.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub power_level: Option<i64>,
|
|
}
|
|
|
|
/// Content for `com.continuwuity.space.role.member` (state key: user ID)
|
|
///
|
|
/// Assigns roles to a user within a Space.
|
|
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
|
pub struct SpaceRoleMemberEventContent {
|
|
pub roles: Vec<String>,
|
|
}
|
|
|
|
/// Content for `com.continuwuity.space.role.room` (state key: room ID)
|
|
///
|
|
/// Declares which roles a child room requires for access.
|
|
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
|
pub struct SpaceRoleRoomEventContent {
|
|
pub required_roles: Vec<String>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn serialize_space_roles() {
|
|
let mut roles = BTreeMap::new();
|
|
roles.insert(
|
|
"admin".to_owned(),
|
|
RoleDefinition {
|
|
description: "Space administrator".to_owned(),
|
|
power_level: Some(100),
|
|
},
|
|
);
|
|
roles.insert(
|
|
"nsfw".to_owned(),
|
|
RoleDefinition {
|
|
description: "NSFW access".to_owned(),
|
|
power_level: None,
|
|
},
|
|
);
|
|
let content = SpaceRolesEventContent { roles };
|
|
let json = serde_json::to_string(&content).unwrap();
|
|
let deserialized: SpaceRolesEventContent = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(deserialized.roles.len(), 2);
|
|
assert_eq!(deserialized.roles["admin"].power_level, Some(100));
|
|
assert!(deserialized.roles["nsfw"].power_level.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_role_member() {
|
|
let content = SpaceRoleMemberEventContent {
|
|
roles: vec!["nsfw".to_owned(), "vip".to_owned()],
|
|
};
|
|
let json = serde_json::to_string(&content).unwrap();
|
|
let deserialized: SpaceRoleMemberEventContent = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(deserialized.roles, vec!["nsfw", "vip"]);
|
|
}
|
|
|
|
#[test]
|
|
fn serialize_role_room() {
|
|
let content = SpaceRoleRoomEventContent {
|
|
required_roles: vec!["nsfw".to_owned()],
|
|
};
|
|
let json = serde_json::to_string(&content).unwrap();
|
|
let deserialized: SpaceRoleRoomEventContent = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(deserialized.required_roles, vec!["nsfw"]);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_roles_deserialize() {
|
|
let json = r#"{"roles":{}}"#;
|
|
let content: SpaceRolesEventContent = serde_json::from_str(json).unwrap();
|
|
assert!(content.roles.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_role_with_power_level() {
|
|
let json = r#"{"description":"Admin","power_level":100}"#;
|
|
let role: RoleDefinition = serde_json::from_str(json).unwrap();
|
|
assert_eq!(role.description, "Admin");
|
|
assert_eq!(role.power_level, Some(100));
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_role_without_power_level() {
|
|
let json = r#"{"description":"NSFW access"}"#;
|
|
let role: RoleDefinition = serde_json::from_str(json).unwrap();
|
|
assert!(role.power_level.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn power_level_omitted_in_serialization_when_none() {
|
|
let role = RoleDefinition {
|
|
description: "Test".to_owned(),
|
|
power_level: None,
|
|
};
|
|
let json = serde_json::to_string(&role).unwrap();
|
|
assert!(!json.contains("power_level"));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_member_roles() {
|
|
let content = SpaceRoleMemberEventContent { roles: vec![] };
|
|
let json = serde_json::to_string(&content).unwrap();
|
|
let deserialized: SpaceRoleMemberEventContent = serde_json::from_str(&json).unwrap();
|
|
assert!(deserialized.roles.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn empty_room_requirements() {
|
|
let content = SpaceRoleRoomEventContent {
|
|
required_roles: vec![],
|
|
};
|
|
let json = serde_json::to_string(&content).unwrap();
|
|
let deserialized: SpaceRoleRoomEventContent = serde_json::from_str(&json).unwrap();
|
|
assert!(deserialized.required_roles.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn roles_ordering_preserved() {
|
|
let mut roles = BTreeMap::new();
|
|
roles.insert("zebra".to_owned(), RoleDefinition {
|
|
description: "Z".to_owned(),
|
|
power_level: None,
|
|
});
|
|
roles.insert("alpha".to_owned(), RoleDefinition {
|
|
description: "A".to_owned(),
|
|
power_level: None,
|
|
});
|
|
let content = SpaceRolesEventContent { roles };
|
|
let json = serde_json::to_string(&content).unwrap();
|
|
let deserialized: SpaceRolesEventContent = serde_json::from_str(&json).unwrap();
|
|
let keys: Vec<_> = deserialized.roles.keys().collect();
|
|
assert_eq!(keys, vec!["alpha", "zebra"]);
|
|
}
|
|
|
|
#[test]
|
|
fn negative_power_level() {
|
|
let json = r#"{"description":"Restricted","power_level":-10}"#;
|
|
let role: RoleDefinition = serde_json::from_str(json).unwrap();
|
|
assert_eq!(role.power_level, Some(-10));
|
|
}
|
|
|
|
#[test]
|
|
fn deserialize_ignores_unknown_fields() {
|
|
let json = r#"{"roles":["nsfw"],"extra_field":"ignored"}"#;
|
|
let content: SpaceRoleMemberEventContent = serde_json::from_str(json).unwrap();
|
|
assert_eq!(content.roles, vec!["nsfw"]);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_description_fails() {
|
|
let json = r#"{"power_level":100}"#;
|
|
assert!(serde_json::from_str::<RoleDefinition>(json).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_type_for_roles_fails() {
|
|
let json = r#"{"roles":"not_an_array"}"#;
|
|
assert!(serde_json::from_str::<SpaceRoleMemberEventContent>(json).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_type_for_required_roles_fails() {
|
|
let json = r#"{"required_roles":42}"#;
|
|
assert!(serde_json::from_str::<SpaceRoleRoomEventContent>(json).is_err());
|
|
}
|
|
}
|