use tokio::sync::broadcast;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionInvalidationReason {
UserDropped,
UserDeactivated,
RoleGranted,
RoleRevoked,
RoleAltered,
}
impl SessionInvalidationReason {
pub fn as_str(&self) -> &'static str {
match self {
SessionInvalidationReason::UserDropped => "UserDropped",
SessionInvalidationReason::UserDeactivated => "UserDeactivated",
SessionInvalidationReason::RoleGranted => "RoleGranted",
SessionInvalidationReason::RoleRevoked => "RoleRevoked",
SessionInvalidationReason::RoleAltered => "RoleAltered",
}
}
pub fn is_hard_revoke(&self) -> bool {
match self {
SessionInvalidationReason::UserDropped => true,
SessionInvalidationReason::UserDeactivated => true,
SessionInvalidationReason::RoleGranted => false,
SessionInvalidationReason::RoleRevoked => false,
SessionInvalidationReason::RoleAltered => false,
}
}
}
#[derive(Debug, Clone)]
pub struct SessionInvalidated {
pub user_id: u64,
pub reason: SessionInvalidationReason,
}
pub struct SessionInvalidationBus {
tx: broadcast::Sender<SessionInvalidated>,
}
pub const SESSION_INVALIDATION_CHANNEL_CAPACITY: usize = 256;
impl SessionInvalidationBus {
pub fn new() -> (Self, broadcast::Receiver<SessionInvalidated>) {
let (tx, rx) = broadcast::channel(SESSION_INVALIDATION_CHANNEL_CAPACITY);
(Self { tx }, rx)
}
pub fn from_existing(tx: broadcast::Sender<SessionInvalidated>) -> Self {
Self { tx }
}
pub fn publish(&self, event: SessionInvalidated) -> usize {
self.tx.send(event).unwrap_or(0)
}
pub fn subscribe(&self) -> broadcast::Receiver<SessionInvalidated> {
self.tx.subscribe()
}
pub fn sender(&self) -> broadcast::Sender<SessionInvalidated> {
self.tx.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn session_invalidation_bus_produce_consume() {
let (bus, mut rx) = SessionInvalidationBus::new();
bus.publish(SessionInvalidated {
user_id: 42,
reason: SessionInvalidationReason::UserDropped,
});
let event = rx.recv().await.expect("should receive event");
assert_eq!(event.user_id, 42);
assert!(matches!(
event.reason,
SessionInvalidationReason::UserDropped
));
}
#[test]
fn reason_as_str_covers_all_variants() {
let variants = [
SessionInvalidationReason::UserDropped,
SessionInvalidationReason::UserDeactivated,
SessionInvalidationReason::RoleGranted,
SessionInvalidationReason::RoleRevoked,
SessionInvalidationReason::RoleAltered,
];
for v in &variants {
assert!(!v.as_str().is_empty());
}
}
#[test]
fn hard_revoke_correct_for_all_variants() {
assert!(SessionInvalidationReason::UserDropped.is_hard_revoke());
assert!(SessionInvalidationReason::UserDeactivated.is_hard_revoke());
assert!(!SessionInvalidationReason::RoleGranted.is_hard_revoke());
assert!(!SessionInvalidationReason::RoleRevoked.is_hard_revoke());
assert!(!SessionInvalidationReason::RoleAltered.is_hard_revoke());
}
}