Skip to main content

steam_client/internal/
messaging.rs

1//! Message sending abstraction for testable Steam communication.
2//!
3//! This module provides a `MessageSender` trait that abstracts message sending,
4//! enabling mock implementations for unit testing without actual network
5//! connections.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use steam_client::messaging::{MessageSender, MockMessageSender, SessionInfo};
11//!
12//! // Create a mock sender for testing
13//! let mut sender = MockMessageSender::new_logged_in(12345, 76561198012345678);
14//!
15//! // Use it in tests - messages are recorded for inspection
16//! sender.send_message(EMsg::ClientChangeStatus, &body).await?;
17//!
18//! // Verify what was sent
19//! assert_eq!(sender.sent_messages.len(), 1);
20//! assert_eq!(sender.sent_messages[0].msg_type, EMsg::ClientChangeStatus);
21//! ```
22
23use async_trait::async_trait;
24use prost::Message;
25use steam_enums::EMsg;
26
27use crate::error::SteamError;
28
29/// Session information needed for building message headers.
30#[derive(Debug, Clone, Copy, Default)]
31pub struct SessionInfo {
32    /// The client session ID.
33    pub session_id: i32,
34    /// The Steam ID (as u64).
35    pub steam_id: u64,
36}
37
38impl SessionInfo {
39    /// Create a new session info.
40    pub fn new(session_id: i32, steam_id: u64) -> Self {
41        Self { session_id, steam_id }
42    }
43}
44
45/// Record of a sent message for test inspection.
46#[derive(Debug, Clone)]
47pub struct SentMessage {
48    /// The message type.
49    pub msg_type: EMsg,
50    /// The encoded message body.
51    pub body: Vec<u8>,
52    /// Job ID if this was a job-tracked message.
53    pub job_id: Option<u64>,
54    /// Service method name if this was a service call.
55    pub service_method: Option<String>,
56}
57
58/// Trait for sending Steam protocol messages.
59///
60/// This trait abstracts message sending, enabling mock implementations
61/// for unit testing without actual network connections.
62///
63/// # Example
64///
65/// ```rust,ignore
66/// async fn set_persona<S: MessageSender>(
67///     sender: &mut S,
68///     state: EPersonaState,
69/// ) -> Result<(), SteamError> {
70///     if !sender.is_logged_in() {
71///         return Err(SteamError::NotLoggedOn);
72///     }
73///     
74///     let msg = CMsgClientChangeStatus {
75///         persona_state: Some(state as u32),
76///         ..Default::default()
77///     };
78///     
79///     sender.send_message(EMsg::ClientChangeStatus, &msg).await
80/// }
81/// ```
82#[async_trait]
83pub trait MessageSender: Send {
84    /// Check if currently logged in.
85    fn is_logged_in(&self) -> bool;
86
87    /// Get session info for building headers.
88    fn session_info(&self) -> SessionInfo;
89
90    /// Send a protobuf message.
91    ///
92    /// # Arguments
93    /// * `msg_type` - The Steam message type (EMsg)
94    /// * `body` - The protobuf message body
95    async fn send_message<T: Message + Send + Sync>(&mut self, msg_type: EMsg, body: &T) -> Result<(), SteamError>;
96
97    /// Send a service method call (unified messages).
98    ///
99    /// # Arguments
100    /// * `method` - The service method name (e.g., "Player.IgnoreFriend#1")
101    /// * `body` - The protobuf request body
102    async fn send_service_method<T: Message + Send + Sync>(&mut self, method: &str, body: &T) -> Result<(), SteamError>;
103}
104
105/// Mock message sender for testing.
106///
107/// Records all sent messages for inspection and verification in tests.
108/// Can be configured to simulate logged-in or logged-out state.
109///
110/// # Example
111///
112/// ```rust,ignore
113/// let mut mock = MockMessageSender::new_logged_in(12345, 76561198012345678);
114///
115/// // Simulate sending a message
116/// mock.send_message(EMsg::ClientChangeStatus, &body).await?;
117///
118/// // Verify the message was sent
119/// assert_eq!(mock.sent_messages.len(), 1);
120/// assert_eq!(mock.sent_messages[0].msg_type, EMsg::ClientChangeStatus);
121///
122/// // Decode the body for detailed verification
123/// let sent_body: CMsgClientChangeStatus = mock.decode_last_message()?;
124/// assert_eq!(sent_body.persona_state, Some(1));
125/// ```
126#[derive(Debug, Default)]
127pub struct MockMessageSender {
128    /// Whether the mock is in "logged in" state.
129    pub logged_in: bool,
130    /// Session information.
131    pub session_info: SessionInfo,
132    /// All messages sent through this mock.
133    pub sent_messages: Vec<SentMessage>,
134    /// If set, next send will return this error.
135    pub next_error: Option<SteamError>,
136    /// Current job ID source for simulation.
137    pub current_job_id: u64,
138}
139
140impl MockMessageSender {
141    /// Create a new mock sender in logged-out state.
142    pub fn new() -> Self {
143        Self::default()
144    }
145
146    /// Create a mock sender in logged-in state.
147    pub fn new_logged_in(session_id: i32, steam_id: u64) -> Self {
148        Self {
149            logged_in: true,
150            session_info: SessionInfo::new(session_id, steam_id),
151            sent_messages: Vec::new(),
152            next_error: None,
153            current_job_id: 0,
154        }
155    }
156
157    /// Set whether this mock is "logged in".
158    pub fn set_logged_in(&mut self, logged_in: bool) {
159        self.logged_in = logged_in;
160    }
161
162    /// Make the next send call return an error.
163    pub fn set_next_error(&mut self, error: SteamError) {
164        self.next_error = Some(error);
165    }
166
167    /// Clear all recorded messages.
168    pub fn clear(&mut self) {
169        self.sent_messages.clear();
170    }
171
172    /// Get the last sent message, if any.
173    pub fn last_sent(&self) -> Option<&SentMessage> {
174        self.sent_messages.last()
175    }
176
177    /// Decode the body of the last sent message.
178    ///
179    /// Useful for verifying the exact content of sent messages in tests.
180    pub fn decode_last_message<T: Message + Default>(&self) -> Result<T, SteamError> {
181        let msg = self.last_sent().ok_or_else(|| SteamError::Other("No messages sent".into()))?;
182        T::decode(&msg.body[..]).map_err(|e| SteamError::ProtocolError(format!("Failed to decode: {}", e)))
183    }
184
185    /// Get messages of a specific type.
186    pub fn messages_of_type(&self, msg_type: EMsg) -> Vec<&SentMessage> {
187        self.sent_messages.iter().filter(|m| m.msg_type == msg_type).collect()
188    }
189
190    /// Get service method calls.
191    pub fn service_calls(&self) -> Vec<&SentMessage> {
192        self.sent_messages.iter().filter(|m| m.service_method.is_some()).collect()
193    }
194}
195
196#[async_trait]
197impl MessageSender for MockMessageSender {
198    fn is_logged_in(&self) -> bool {
199        self.logged_in
200    }
201
202    fn session_info(&self) -> SessionInfo {
203        self.session_info
204    }
205
206    async fn send_message<T: Message + Send + Sync>(&mut self, msg_type: EMsg, body: &T) -> Result<(), SteamError> {
207        // Check for injected error
208        if let Some(error) = self.next_error.take() {
209            return Err(error);
210        }
211
212        // Increment job ID to simulate real client behavior
213        self.current_job_id += 1;
214
215        // Record the message
216        self.sent_messages.push(SentMessage { msg_type, body: body.encode_to_vec(), job_id: Some(self.current_job_id), service_method: None });
217
218        Ok(())
219    }
220
221    async fn send_service_method<T: Message + Send + Sync>(&mut self, method: &str, body: &T) -> Result<(), SteamError> {
222        // Check for injected error
223        if let Some(error) = self.next_error.take() {
224            return Err(error);
225        }
226
227        // Increment job ID to simulate real client behavior
228        self.current_job_id += 1;
229
230        // Record the service call
231        self.sent_messages.push(SentMessage {
232            msg_type: EMsg::ServiceMethodCallFromClient,
233            body: body.encode_to_vec(),
234            job_id: Some(self.current_job_id),
235            service_method: Some(method.to_string()),
236        });
237
238        Ok(())
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use steam_protos::CMsgClientChangeStatus;
245
246    use super::*;
247
248    #[tokio::test]
249    async fn test_mock_sender_records_messages() {
250        let mut mock = MockMessageSender::new_logged_in(12345, 76561198012345678);
251
252        let body = CMsgClientChangeStatus { persona_state: Some(1), ..Default::default() };
253
254        mock.send_message(EMsg::ClientChangeStatus, &body).await.expect("test should not fail");
255
256        assert_eq!(mock.sent_messages.len(), 1);
257        assert_eq!(mock.sent_messages[0].msg_type, EMsg::ClientChangeStatus);
258        assert!(mock.sent_messages[0].service_method.is_none());
259    }
260
261    #[tokio::test]
262    async fn test_mock_sender_decode_last_message() {
263        let mut mock = MockMessageSender::new_logged_in(12345, 76561198012345678);
264
265        let body = CMsgClientChangeStatus { persona_state: Some(3), player_name: Some("TestPlayer".to_string()), ..Default::default() };
266
267        mock.send_message(EMsg::ClientChangeStatus, &body).await.expect("test should not fail");
268
269        let decoded: CMsgClientChangeStatus = mock.decode_last_message().expect("test should not fail");
270        assert_eq!(decoded.persona_state, Some(3));
271        assert_eq!(decoded.player_name, Some("TestPlayer".to_string()));
272    }
273
274    #[tokio::test]
275    async fn test_mock_sender_service_method() {
276        let mut mock = MockMessageSender::new_logged_in(12345, 76561198012345678);
277
278        let body = steam_protos::CPlayerGetNicknameListRequest {};
279        mock.send_service_method("Player.GetNicknameList#1", &body).await.expect("test should not fail");
280
281        assert_eq!(mock.sent_messages.len(), 1);
282        assert_eq!(mock.sent_messages[0].service_method, Some("Player.GetNicknameList#1".to_string()));
283        assert_eq!(mock.sent_messages[0].msg_type, EMsg::ServiceMethodCallFromClient);
284    }
285
286    #[tokio::test]
287    async fn test_mock_sender_error_injection() {
288        let mut mock = MockMessageSender::new_logged_in(12345, 76561198012345678);
289        mock.set_next_error(SteamError::NotConnected);
290
291        let body = CMsgClientChangeStatus::default();
292        let result = mock.send_message(EMsg::ClientChangeStatus, &body).await;
293
294        assert!(result.is_err());
295        assert!(mock.sent_messages.is_empty()); // Message not recorded on error
296    }
297
298    #[tokio::test]
299    async fn test_mock_sender_is_logged_in() {
300        let mock_out = MockMessageSender::new();
301        assert!(!mock_out.is_logged_in());
302
303        let mock_in = MockMessageSender::new_logged_in(123, 456);
304        assert!(mock_in.is_logged_in());
305    }
306
307    #[tokio::test]
308    async fn test_mock_sender_session_info() {
309        let mock = MockMessageSender::new_logged_in(12345, 76561198012345678);
310        let info = mock.session_info();
311
312        assert_eq!(info.session_id, 12345);
313        assert_eq!(info.steam_id, 76561198012345678);
314    }
315
316    #[tokio::test]
317    async fn test_mock_sender_messages_of_type() {
318        let mut mock = MockMessageSender::new_logged_in(12345, 76561198012345678);
319
320        mock.send_message(EMsg::ClientChangeStatus, &CMsgClientChangeStatus::default()).await.expect("test should not fail");
321        mock.send_message(EMsg::ClientHeartBeat, &steam_protos::CMsgClientHeartBeat::default()).await.expect("test should not fail");
322        mock.send_message(EMsg::ClientChangeStatus, &CMsgClientChangeStatus::default()).await.expect("test should not fail");
323
324        let status_msgs = mock.messages_of_type(EMsg::ClientChangeStatus);
325        assert_eq!(status_msgs.len(), 2);
326
327        let heartbeat_msgs = mock.messages_of_type(EMsg::ClientHeartBeat);
328        assert_eq!(heartbeat_msgs.len(), 1);
329    }
330
331    #[tokio::test]
332    async fn test_mock_sender_clear() {
333        let mut mock = MockMessageSender::new_logged_in(12345, 76561198012345678);
334
335        mock.send_message(EMsg::ClientChangeStatus, &CMsgClientChangeStatus::default()).await.expect("test should not fail");
336        assert_eq!(mock.sent_messages.len(), 1);
337
338        mock.clear();
339        assert!(mock.sent_messages.is_empty());
340    }
341}