//! This module defines events affecting the room module and their handling. use anyhow::Context as AnyhowContext; use log::error; use solstice_proto::server; use solstice_proto::ServerRequest; use crate::context::Context; use crate::control; use crate::handlers::{ RoomJoinResponseHandler, RoomListRequestHandler, RoomListResponseHandler, RoomMessageRequestHandler, RoomMessageResponseHandler, }; use crate::message_handler::MessageHandler; /// An event affecting the chat room module. #[derive(Debug, PartialEq, Eq)] pub enum RoomEvent { JoinRequest(String), JoinResponse(server::RoomJoinResponse), ListRequest, ListResponse(server::RoomListResponse), MessageRequest(control::RoomMessageRequest), MessageResponse(server::RoomMessageResponse), } /// An interface for room event handlers. /// /// Allows mocking of handlers in tests. pub trait HandleRoomEvent { /// Handles the given `event` against the given global `context`. fn handle( &mut self, context: &mut Context, event: RoomEvent, ) -> anyhow::Result<()>; } /// The real, default implementation of `HandleRoomEvent`. #[derive(Default)] pub struct RoomEventHandler; fn start_joining(context: &mut Context, room_name: &str) -> anyhow::Result<()> { let room = context.state.rooms.get_mut_strict(room_name)?; room.start_joining().map_err(|err| { let response = control::Response::RoomJoinResponse(control::RoomJoinResponse { room_name: room_name.to_string(), room: room.clone_state(), }); if let Err(err) = context.control_response_tx.blocking_send(response) { error!( "Failed to send RoomJoinResponse for room {}: {}", room_name, err ); } err.into() }) } fn handle_join_request( context: &mut Context, room_name: String, ) -> anyhow::Result<()> { start_joining(context, &room_name).context("joining room")?; context .server_request_tx .blocking_send(ServerRequest::RoomJoinRequest(server::RoomJoinRequest { room_name, })) .context("sending server request")?; Ok(()) } impl HandleRoomEvent for RoomEventHandler { fn handle( &mut self, context: &mut Context, event: RoomEvent, ) -> anyhow::Result<()> { // TODO: Remove individual handlers, move code into RoomEventHandler. match event { RoomEvent::JoinRequest(room_name) => { handle_join_request(context, room_name) } RoomEvent::JoinResponse(response) => { RoomJoinResponseHandler::default().run(context, &response) } RoomEvent::ListRequest => { RoomListRequestHandler::default().run(context, &()) } RoomEvent::ListResponse(response) => { RoomListResponseHandler::default().run(context, &response) } RoomEvent::MessageRequest(request) => { RoomMessageRequestHandler::default().run(context, &request) } RoomEvent::MessageResponse(response) => { RoomMessageResponseHandler::default().run(context, &response) } } } } #[cfg(test)] pub mod testing { use crate::context::Context; use super::{HandleRoomEvent, RoomEvent}; #[derive(Default)] pub struct FakeRoomEventHandler { pub events: Vec, } impl HandleRoomEvent for FakeRoomEventHandler { fn handle( &mut self, _context: &mut Context, event: RoomEvent, ) -> anyhow::Result<()> { self.events.push(event); Ok(()) } } } #[cfg(test)] mod tests { use solstice_proto::server; use crate::context::{ContextBundle, ContextOptions}; use crate::room::{RoomMembership, RoomState, RoomVisibility}; use super::*; #[test] fn handle_room_join_request_failure() { let mut bundle = ContextBundle::default(); RoomEventHandler .handle( &mut bundle.context, RoomEvent::JoinRequest("bleep".to_string()), ) .unwrap_err(); // Room state has not changed. assert_eq!(bundle.context.state.rooms.get_room_list(), vec![]); // Close the channel, so we can observe it was empty without hanging. drop(bundle.context.server_request_tx); assert_eq!(bundle.server_request_rx.blocking_recv(), None); } #[test] fn handle_room_join_request_already_joined() { let mut room = RoomState::new(RoomVisibility::Public, 3); room.membership = RoomMembership::Member; let mut options = ContextOptions::default(); options .initial_state .rooms .insert("bleep".to_string(), room.clone()); let mut bundle = ContextBundle::new(options); RoomEventHandler .handle( &mut bundle.context, RoomEvent::JoinRequest("bleep".to_string()), ) .unwrap_err(); // Room state has not changed. assert_eq!( bundle.context.state.rooms.get_room_list(), vec![("bleep".to_string(), room.clone())] ); assert_eq!( bundle.control_response_rx.blocking_recv(), Some(control::Response::RoomJoinResponse( control::RoomJoinResponse { room_name: "bleep".to_string(), room, } )) ); } #[test] fn handle_room_join_request_success() { let mut options = ContextOptions::default(); options.initial_state.rooms.insert( "bleep".to_string(), RoomState::new(RoomVisibility::Public, 3), ); let mut bundle = ContextBundle::new(options); RoomEventHandler .handle( &mut bundle.context, RoomEvent::JoinRequest("bleep".to_string()), ) .expect("handling request"); let request = bundle.server_request_rx.blocking_recv().unwrap(); // Room state has been altered to reflect the request. assert_eq!( bundle .context .state .rooms .get_strict("bleep") .expect("getting room") .clone_state() .membership, RoomMembership::Joining ); // The request is forwarded onwards. assert_eq!( request, ServerRequest::RoomJoinRequest(server::RoomJoinRequest { room_name: "bleep".to_string(), }) ); } }