Skip to main content

rialo_types/
websocket_op.rs

1// Copyright (c) Subzero Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4// ============================================================================
5// WebSocket Operations
6// ============================================================================
7
8// ============================================================================
9// WebSocket Types
10// ============================================================================
11
12use std::fmt;
13#[cfg(feature = "non-pdk")]
14use std::str::FromStr;
15
16use borsh::{BorshDeserialize, BorshSerialize};
17#[cfg(feature = "non-pdk")]
18use clap::Subcommand;
19use serde::{Deserialize, Serialize};
20
21use crate::rex_info::{RexId, RexUrl, RexValue};
22
23/// Estimated size in bytes for system messages (OversizedWarning).
24/// This accounts for the struct fields and some padding.
25pub const SYSTEM_MESSAGE_SIZE: usize = 128;
26
27/// Read mode for WebSocket buffer.
28///
29/// Determines how messages are retrieved from the buffer.
30#[derive(
31    Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
32)]
33pub enum WebSocketReadMode {
34    /// Return only the most recent message (default, backward compatible).
35    #[default]
36    Latest,
37    /// Return all accumulated messages.
38    All,
39    /// Return messages newer than the given index.
40    ///
41    /// **Note:** Index wraparound at `u64::MAX` is not handled. See DataBuffer
42    /// documentation for limitations. At 1 million messages per second,
43    /// wraparound would take ~584,000 years.
44    FromIndex(u64),
45}
46
47/// Message content type - either user data or system notification.
48#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
49pub enum MessageContent {
50    /// Normal message data from WebSocket.
51    Data(Vec<u8>),
52    /// System-generated message for oversized content that was rejected.
53    OversizedWarning {
54        /// Size of the rejected message in bytes.
55        original_size: usize,
56        /// Buffer size limit in bytes.
57        limit: usize,
58        /// When the oversized message was received (RFC3339 with microseconds).
59        original_timestamp: String,
60    },
61}
62
63impl MessageContent {
64    /// Returns the byte size of this content for buffer accounting.
65    ///
66    /// For data messages, this is the actual data length.
67    /// For system messages, this is a fixed size constant.
68    pub fn byte_size(&self) -> usize {
69        match self {
70            MessageContent::Data(data) => data.len(),
71            MessageContent::OversizedWarning { .. } => SYSTEM_MESSAGE_SIZE,
72        }
73    }
74
75    /// Returns true if this is a system message.
76    pub fn is_system(&self) -> bool {
77        matches!(self, MessageContent::OversizedWarning { .. })
78    }
79
80    /// Returns the data bytes if this is a Data message, None otherwise.
81    pub fn as_data(&self) -> Option<&[u8]> {
82        match self {
83            MessageContent::Data(data) => Some(data),
84            MessageContent::OversizedWarning { .. } => None,
85        }
86    }
87}
88
89/// A single buffered message with metadata.
90#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
91pub struct BufferedMessage {
92    /// Unique, monotonically increasing index (never reused, persists across reconnects).
93    pub index: u64,
94    /// The message content.
95    pub content: MessageContent,
96    /// When this message was received/created (RFC3339 with microseconds).
97    pub received_at: String,
98}
99
100impl BufferedMessage {
101    /// Returns the size of this message in bytes (for buffer accounting).
102    pub fn byte_size(&self) -> usize {
103        self.content.byte_size()
104    }
105
106    /// Returns true if this is a system message.
107    pub fn is_system_message(&self) -> bool {
108        self.content.is_system()
109    }
110
111    /// Returns the data bytes if this contains a Data message, None otherwise.
112    pub fn as_data(&self) -> Option<&[u8]> {
113        self.content.as_data()
114    }
115}
116
117/// Response for WebSocket read operations.
118///
119/// This struct captures the read result with messages and buffer state information.
120///
121/// # Gap Detection
122///
123/// Consumers can detect gaps by comparing message indices. Each `BufferedMessage`
124/// contains an `index` field that increases monotonically. If there's a gap between
125/// indices, some messages were evicted from the buffer.
126///
127/// # Convenience Methods
128///
129/// For common access patterns, use the convenience methods instead of accessing
130/// fields directly:
131///
132/// ```ignore
133/// // Instead of:
134/// let data = response.messages.first().unwrap().content.as_data().unwrap();
135///
136/// // Use:
137/// let data = response.first_data().unwrap();
138/// ```
139#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
140pub struct WebSocketReadResponse {
141    /// All requested messages (based on read mode).
142    pub messages: Vec<BufferedMessage>,
143    /// Current highest index in buffer (for tracking).
144    pub latest_index: Option<u64>,
145    /// Current lowest index in buffer (helpful for gap detection).
146    pub oldest_index: Option<u64>,
147}
148
149impl WebSocketReadResponse {
150    /// Returns true if there are any messages in the response.
151    pub fn has_messages(&self) -> bool {
152        !self.messages.is_empty()
153    }
154
155    /// Returns the number of messages in the response.
156    pub fn message_count(&self) -> usize {
157        self.messages.len()
158    }
159
160    /// Get the first (oldest) message, if any.
161    pub fn first_message(&self) -> Option<&BufferedMessage> {
162        self.messages.first()
163    }
164
165    /// Get the last (most recent) message, if any.
166    pub fn latest_message(&self) -> Option<&BufferedMessage> {
167        self.messages.last()
168    }
169
170    /// Get the data from the first message (convenience for common pattern).
171    ///
172    /// Returns `None` if there are no messages or if the first message is a system message.
173    pub fn first_data(&self) -> Option<&[u8]> {
174        self.messages.first().and_then(|m| m.as_data())
175    }
176
177    /// Get the data from the latest message (convenience for common pattern).
178    ///
179    /// Returns `None` if there are no messages or if the latest message is a system message.
180    pub fn latest_data(&self) -> Option<&[u8]> {
181        self.messages.last().and_then(|m| m.as_data())
182    }
183
184    /// Iterate over only the data messages (filtering out system messages).
185    pub fn data_messages(&self) -> impl Iterator<Item = &BufferedMessage> {
186        self.messages.iter().filter(|m| !m.is_system_message())
187    }
188
189    /// Iterate over only the data bytes (convenience for processing).
190    ///
191    /// This filters out system messages and returns only the raw data bytes.
192    pub fn iter_data(&self) -> impl Iterator<Item = &[u8]> {
193        self.messages.iter().filter_map(|m| m.as_data())
194    }
195
196    /// Returns the number of data messages (excluding system messages).
197    pub fn data_message_count(&self) -> usize {
198        self.messages
199            .iter()
200            .filter(|m| !m.is_system_message())
201            .count()
202    }
203
204    /// Returns true if any message is a system message (like OversizedWarning).
205    pub fn has_system_messages(&self) -> bool {
206        self.messages.iter().any(|m| m.is_system_message())
207    }
208}
209
210/// WebSocket operation types for the WebSocket REX.
211///
212/// This enum defines the different operations that can be performed on WebSocket connections.
213/// All WebSocket operations are handled through a single `TargetRexProgram::WebSocket` variant.
214///
215/// # Variants
216/// * `Connect` - Establish a new WebSocket connection to an external server
217/// * `Read` - Read the latest data from an existing WebSocket connection
218///
219/// # Future Extensions
220/// Additional operations like `Send` and `Close` may be added in the future.
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
222#[cfg_attr(feature = "non-pdk", derive(Subcommand))]
223pub enum WebSocketOperation {
224    /// Establish a new WebSocket connection.
225    ///
226    /// When this operation is executed:
227    /// - A validator is randomly selected to handle the connection
228    /// - The TEE establishes the WebSocket connection to the specified URL
229    /// - On success, the connection is registered in the WebSocket Registry
230    /// - The assigned validator is recorded for future routing
231    Connect {
232        /// WebSocket URL (typically wss://...)
233        #[cfg_attr(feature = "non-pdk", clap(
234            long = "target-url",
235            value_parser = clap::value_parser!(RexUrl)
236        ))]
237        url: RexUrl,
238        /// The RexId that represents this connection request
239        #[cfg_attr(feature = "non-pdk", clap(long, value_parser = parse_rex_id))]
240        rex_id: RexId,
241    },
242    /// Read data from an existing WebSocket connection.
243    ///
244    /// When this operation is executed:
245    /// - The operation is automatically routed to the TEE holding the connection
246    /// - Returns messages from the connection's buffer based on the read mode
247    /// - The referenced connection must exist and be active
248    ///
249    /// # Read Modes
250    ///
251    /// - `Latest` (default): Returns only the most recent message
252    /// - `All`: Returns all accumulated messages in the buffer
253    /// - `FromIndex(n)`: Returns all messages with index > n, with gap detection
254    ///
255    /// # Validation
256    ///
257    /// The handler implementation must verify that:
258    /// - `connection_rex_id` references an existing Connect operation in the WebSocket Registry
259    /// - The connection is still active and hasn't been closed
260    /// - The requesting validator has permission to read from this connection
261    ///
262    /// # Error Handling
263    ///
264    /// If validation fails, the REX should return an appropriate error indicating:
265    /// - `ConnectionNotFound` - if the referenced REX ID doesn't exist
266    /// - `ConnectionClosed` - if the connection has been terminated
267    /// - `InvalidConnectionType` - if the referenced REX is not a WebSocket Connect operation
268    Read {
269        /// References the RexId of the Connect operation that established the connection
270        #[cfg_attr(feature = "non-pdk", clap(long, value_parser = parse_rex_id))]
271        connection_rex_id: RexId,
272        /// The read mode determining which messages to return (defaults to Latest for backward compatibility)
273        #[serde(default)]
274        #[cfg_attr(feature = "non-pdk", clap(skip))]
275        mode: WebSocketReadMode,
276    },
277    /// Send messages to an existing WebSocket connection.
278    ///
279    /// When this operation is executed:
280    /// - The operation is automatically routed to the TEE holding the connection
281    /// - Each `RexValue` in `messages` is sent as a separate WebSocket message
282    /// - The inner value is extracted: `Plain(String)` → sent as-is, `Encrypted(String)` → decrypted then sent
283    /// - Returns the number of messages successfully sent
284    /// - The referenced connection must exist and be active
285    ///
286    /// # Validation
287    ///
288    /// The handler implementation must verify that:
289    /// - `connection_rex_id` references an existing Connect operation in the WebSocket Registry
290    /// - The connection is still active and hasn't been closed
291    /// - The requesting validator has permission to send to this connection
292    ///
293    /// # Error Handling
294    ///
295    /// If validation fails, the REX should return an appropriate error indicating:
296    /// - `ConnectionNotFound` - if the referenced REX ID doesn't exist
297    /// - `ConnectionClosed` - if the connection has been terminated
298    /// - `InvalidConnectionType` - if the referenced REX is not a WebSocket Connect operation
299    /// - `SecretDecryptionFailed` - if an encrypted message cannot be decrypted
300    Send {
301        /// References the RexId of the Connect operation that established the connection
302        #[cfg_attr(feature = "non-pdk", clap(long, value_parser = parse_rex_id))]
303        connection_rex_id: RexId,
304        /// Messages to send to the WebSocket connection
305        /// Each RexValue will be sent as a separate WebSocket message
306        #[cfg_attr(feature = "non-pdk", clap(skip))]
307        messages: Vec<RexValue>,
308    },
309    /// Close an existing WebSocket connection.
310    ///
311    /// When this operation is executed:
312    /// - The operation is automatically routed to the TEE holding the connection
313    /// - The WebSocket connection is gracefully closed
314    /// - The connection status in the WebSocket Registry is updated to `Closed`
315    /// - The referenced connection must exist and be active
316    ///
317    /// # Validation
318    ///
319    /// The handler implementation must verify that:
320    /// - `connection_rex_id` references an existing Connect operation in the WebSocket Registry
321    /// - The requesting validator has permission to close this connection
322    ///
323    /// # Error Handling
324    ///
325    /// If validation fails, the REX should return an appropriate error indicating:
326    /// - `ConnectionNotFound` - if the referenced REX ID doesn't exist
327    /// - `ConnectionAlreadyClosed` - if the connection has already been terminated
328    /// - `InvalidConnectionType` - if the referenced REX is not a WebSocket Connect operation
329    Close {
330        /// References the RexId of the Connect operation to close
331        #[cfg_attr(feature = "non-pdk", clap(long, value_parser = parse_rex_id))]
332        connection_rex_id: RexId,
333    },
334}
335
336/// Parse a RexId from a string (for clap CLI parsing)
337#[cfg(feature = "non-pdk")]
338fn parse_rex_id(s: &str) -> Result<RexId, String> {
339    RexId::from_str(s)
340}
341
342impl WebSocketOperation {
343    /// Create a new Connect operation.
344    pub fn connect(url: impl Into<RexUrl>, rex_id: RexId) -> Self {
345        WebSocketOperation::Connect {
346            url: url.into(),
347            rex_id,
348        }
349    }
350
351    /// Create a new Read operation with default Latest mode.
352    pub fn read(connection_rex_id: RexId) -> Self {
353        WebSocketOperation::Read {
354            connection_rex_id,
355            mode: WebSocketReadMode::default(),
356        }
357    }
358
359    /// Create a new Read operation with a specific mode.
360    pub fn read_with_mode(connection_rex_id: RexId, mode: WebSocketReadMode) -> Self {
361        WebSocketOperation::Read {
362            connection_rex_id,
363            mode,
364        }
365    }
366
367    /// Create a new Send operation.
368    pub fn send(connection_rex_id: RexId, messages: Vec<RexValue>) -> Self {
369        WebSocketOperation::Send {
370            connection_rex_id,
371            messages,
372        }
373    }
374
375    /// Create a new Close operation.
376    pub fn close(connection_rex_id: RexId) -> Self {
377        WebSocketOperation::Close { connection_rex_id }
378    }
379
380    /// Returns true if this is a Connect operation.
381    pub fn is_connect(&self) -> bool {
382        matches!(self, WebSocketOperation::Connect { .. })
383    }
384
385    /// Returns true if this is a Read operation.
386    pub fn is_read(&self) -> bool {
387        matches!(self, WebSocketOperation::Read { .. })
388    }
389
390    /// Returns true if this is a Send operation.
391    pub fn is_send(&self) -> bool {
392        matches!(self, WebSocketOperation::Send { .. })
393    }
394
395    /// Returns true if this is a Close operation.
396    pub fn is_close(&self) -> bool {
397        matches!(self, WebSocketOperation::Close { .. })
398    }
399
400    /// Returns the URL if this is a Connect operation, None otherwise.
401    pub fn url(&self) -> Option<&RexUrl> {
402        match self {
403            WebSocketOperation::Connect { url, .. } => Some(url),
404            _ => None,
405        }
406    }
407
408    /// Returns the rex_id if this is a Connect operation, None otherwise.
409    pub fn rex_id(&self) -> Option<&RexId> {
410        match self {
411            WebSocketOperation::Connect { rex_id, .. } => Some(rex_id),
412            _ => None,
413        }
414    }
415
416    /// Returns the connection_rex_id if this is a Read, Send, or Close operation, None otherwise.
417    pub fn connection_rex_id(&self) -> Option<&RexId> {
418        match self {
419            WebSocketOperation::Read {
420                connection_rex_id, ..
421            }
422            | WebSocketOperation::Send {
423                connection_rex_id, ..
424            }
425            | WebSocketOperation::Close { connection_rex_id } => Some(connection_rex_id),
426            _ => None,
427        }
428    }
429
430    /// Returns the read mode if this is a Read operation, None otherwise.
431    pub fn read_mode(&self) -> Option<&WebSocketReadMode> {
432        match self {
433            WebSocketOperation::Read { mode, .. } => Some(mode),
434            _ => None,
435        }
436    }
437
438    /// Returns the messages if this is a Send operation, None otherwise.
439    pub fn messages(&self) -> Option<&[RexValue]> {
440        match self {
441            WebSocketOperation::Send { messages, .. } => Some(messages),
442            _ => None,
443        }
444    }
445}
446
447impl fmt::Display for WebSocketOperation {
448    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449        match self {
450            WebSocketOperation::Connect { url, rex_id } => {
451                write!(f, "Connect(url={url}, rex_id={rex_id})")
452            }
453            WebSocketOperation::Read {
454                connection_rex_id,
455                mode,
456            } => {
457                write!(
458                    f,
459                    "Read(connection_rex_id={}, mode={:?})",
460                    connection_rex_id, mode
461                )
462            }
463            WebSocketOperation::Send {
464                connection_rex_id,
465                messages,
466            } => {
467                write!(
468                    f,
469                    "Send(connection_rex_id={}, messages_count={})",
470                    connection_rex_id,
471                    messages.len()
472                )
473            }
474            WebSocketOperation::Close { connection_rex_id } => {
475                write!(f, "Close(connection_rex_id={})", connection_rex_id)
476            }
477        }
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use rialo_s_pubkey::Pubkey;
484
485    use super::*;
486    use crate::rex_info::{RexInfo, StartingTimestamp, TargetRexProgram, UpdateFrequency};
487
488    fn base_valid_rex_info() -> RexInfo {
489        RexInfo {
490            description: "test".to_string(),
491            target_rex_programs: vec![TargetRexProgram::Time],
492            // Default is OneShot, which is allowed for both Asap and Timestamp
493            update_frequency: UpdateFrequency::OneShot,
494            // Start at timestamp 0 by default
495            starting_timestamp: StartingTimestamp::Timestamp(0),
496            // Keep other fields as default
497            ..RexInfo::default()
498        }
499    }
500
501    // ========================================================================
502    // WebSocket Operation Tests
503    // ========================================================================
504
505    #[test]
506    fn test_websocket_connect_operation_creation() {
507        let rex_id = RexId::new(Pubkey::default(), 1u64);
508        let op = WebSocketOperation::connect("wss://example.com/stream", rex_id);
509        assert!(op.is_connect());
510        assert!(!op.is_read());
511
512        if let WebSocketOperation::Connect {
513            url,
514            rex_id: op_rex_id,
515        } = op
516        {
517            assert_eq!(url.to_string(), "wss://example.com/stream");
518            assert_eq!(op_rex_id, rex_id);
519        } else {
520            panic!("Expected Connect variant");
521        }
522    }
523
524    #[test]
525    fn test_websocket_read_operation_creation() {
526        let rex_id = RexId::new(Pubkey::default(), 42u64);
527        let op = WebSocketOperation::read(rex_id);
528        assert!(op.is_read());
529        assert!(!op.is_connect());
530
531        if let WebSocketOperation::Read {
532            connection_rex_id,
533            mode,
534        } = op
535        {
536            assert_eq!(connection_rex_id, rex_id);
537            assert_eq!(mode, WebSocketReadMode::Latest);
538        } else {
539            panic!("Expected Read variant");
540        }
541    }
542
543    #[test]
544    fn test_websocket_operation_serde_roundtrip_connect() {
545        let rex_id = RexId::new(Pubkey::default(), 1u64);
546        let op = WebSocketOperation::Connect {
547            url: "wss://example.com/stream".into(),
548            rex_id,
549        };
550
551        // Serialize to JSON
552        let json = serde_json::to_string(&op).expect("Failed to serialize");
553        // Deserialize back
554        let deserialized: WebSocketOperation =
555            serde_json::from_str(&json).expect("Failed to deserialize");
556
557        assert_eq!(op, deserialized);
558    }
559
560    #[test]
561    fn test_websocket_operation_serde_roundtrip_read() {
562        let rex_id = RexId::new(Pubkey::default(), 123u64);
563        let op = WebSocketOperation::Read {
564            connection_rex_id: rex_id,
565            mode: WebSocketReadMode::default(),
566        };
567
568        // Serialize to JSON
569        let json = serde_json::to_string(&op).expect("Failed to serialize");
570        // Deserialize back
571        let deserialized: WebSocketOperation =
572            serde_json::from_str(&json).expect("Failed to deserialize");
573
574        assert_eq!(op, deserialized);
575    }
576
577    #[test]
578    fn test_target_rex_program_websocket_serde_roundtrip() {
579        let rex_id = RexId::new(Pubkey::default(), 1u64);
580        let target = TargetRexProgram::WebSocket(WebSocketOperation::Connect {
581            url: "wss://example.com/stream".into(),
582            rex_id,
583        });
584
585        // Serialize to JSON
586        let json = serde_json::to_string(&target).expect("Failed to serialize");
587        // Deserialize back
588        let deserialized: TargetRexProgram =
589            serde_json::from_str(&json).expect("Failed to deserialize");
590
591        assert_eq!(target, deserialized);
592    }
593
594    #[test]
595    fn test_websocket_operation_display() {
596        let rex_id = RexId::new(Pubkey::default(), 1u64);
597        let connect_op = WebSocketOperation::Connect {
598            url: "wss://example.com".into(),
599            rex_id,
600        };
601        let display = format!("{}", connect_op);
602        assert!(display.contains("Connect"));
603        assert!(display.contains("wss://example.com"));
604
605        let rex_id = RexId::new(Pubkey::default(), 1u64);
606        let read_op = WebSocketOperation::Read {
607            connection_rex_id: rex_id,
608            mode: WebSocketReadMode::default(),
609        };
610        let display = format!("{}", read_op);
611        assert!(display.contains("Read"));
612        assert!(display.contains("connection_rex_id"));
613    }
614
615    #[test]
616    fn test_websocket_connect_with_encrypted_url() {
617        let rex_id = RexId::new(Pubkey::default(), 1u64);
618        let op = WebSocketOperation::Connect {
619            url: "enc://encrypted_websocket_url".into(),
620            rex_id,
621        };
622
623        if let WebSocketOperation::Connect { url, .. } = &op {
624            assert_eq!(url.to_string(), "enc://encrypted_websocket_url");
625        }
626
627        // Verify serde roundtrip preserves encrypted URL
628        let json = serde_json::to_string(&op).expect("Failed to serialize");
629        let deserialized: WebSocketOperation =
630            serde_json::from_str(&json).expect("Failed to deserialize");
631        assert_eq!(op, deserialized);
632    }
633
634    #[test]
635    fn test_rex_info_with_websocket_target() {
636        let rex_id = RexId::new(Pubkey::default(), 1u64);
637        let mut info = base_valid_rex_info();
638        info.target_rex_programs = vec![TargetRexProgram::WebSocket(WebSocketOperation::Connect {
639            url: "wss://example.com/stream".into(),
640            rex_id,
641        })];
642
643        // Should validate successfully
644        assert!(info.validate().is_ok());
645    }
646
647    #[test]
648    fn test_websocket_operation_borsh_roundtrip_connect() {
649        let rex_id = RexId::new(Pubkey::default(), 1u64);
650        let op = WebSocketOperation::Connect {
651            url: "wss://example.com/stream".into(),
652            rex_id,
653        };
654
655        // Serialize to Borsh
656        let bytes = borsh::to_vec(&op).expect("Failed to serialize");
657        // Deserialize back
658        let deserialized: WebSocketOperation =
659            borsh::from_slice(&bytes).expect("Failed to deserialize");
660
661        assert_eq!(op, deserialized);
662    }
663
664    #[test]
665    fn test_websocket_operation_borsh_roundtrip_read() {
666        let rex_id = RexId::new(Pubkey::default(), 123u64);
667        let op = WebSocketOperation::Read {
668            connection_rex_id: rex_id,
669            mode: WebSocketReadMode::default(),
670        };
671
672        // Serialize to Borsh
673        let bytes = borsh::to_vec(&op).expect("Failed to serialize");
674        // Deserialize back
675        let deserialized: WebSocketOperation =
676            borsh::from_slice(&bytes).expect("Failed to deserialize");
677
678        assert_eq!(op, deserialized);
679    }
680
681    #[test]
682    fn test_websocket_operation_borsh_roundtrip_encrypted_url() {
683        let rex_id = RexId::new(Pubkey::default(), 1u64);
684        let op = WebSocketOperation::Connect {
685            url: "enc://encrypted_websocket_url".into(),
686            rex_id,
687        };
688
689        // Serialize to Borsh
690        let bytes = borsh::to_vec(&op).expect("Failed to serialize");
691        // Deserialize back
692        let deserialized: WebSocketOperation =
693            borsh::from_slice(&bytes).expect("Failed to deserialize");
694
695        assert_eq!(op, deserialized);
696    }
697
698    #[test]
699    fn test_websocket_operation_url_getter() {
700        // Connect operation should return Some(url)
701        let rex_id = RexId::new(Pubkey::default(), 1u64);
702        let connect_op = WebSocketOperation::Connect {
703            url: "wss://example.com/stream".into(),
704            rex_id,
705        };
706        assert!(connect_op.url().is_some());
707        assert_eq!(
708            connect_op.url().unwrap().to_string(),
709            "wss://example.com/stream"
710        );
711
712        // Read operation should return None
713        let rex_id = RexId::new(Pubkey::default(), 42u64);
714        let read_op = WebSocketOperation::Read {
715            connection_rex_id: rex_id,
716            mode: WebSocketReadMode::default(),
717        };
718        assert!(read_op.url().is_none());
719    }
720
721    #[test]
722    fn test_websocket_operation_connection_rex_id_getter() {
723        // Read operation should return Some(connection_rex_id)
724        let rex_id = RexId::new(Pubkey::default(), 42u64);
725        let read_op = WebSocketOperation::Read {
726            connection_rex_id: rex_id,
727            mode: WebSocketReadMode::default(),
728        };
729        assert!(read_op.connection_rex_id().is_some());
730        assert_eq!(*read_op.connection_rex_id().unwrap(), rex_id);
731
732        // Connect operation should return None
733        let rex_id = RexId::new(Pubkey::default(), 1u64);
734        let connect_op = WebSocketOperation::Connect {
735            url: "wss://example.com/stream".into(),
736            rex_id,
737        };
738        assert!(connect_op.connection_rex_id().is_none());
739
740        // Send operation should return Some(connection_rex_id)
741        let rex_id = RexId::new(Pubkey::default(), 99u64);
742        let send_op = WebSocketOperation::Send {
743            connection_rex_id: rex_id,
744            messages: vec![],
745        };
746        assert!(send_op.connection_rex_id().is_some());
747        assert_eq!(*send_op.connection_rex_id().unwrap(), rex_id);
748    }
749
750    // ========================================================================
751    // WebSocket Send Operation Tests
752    // ========================================================================
753
754    #[test]
755    fn test_websocket_send_operation_creation() {
756        let rex_id = RexId::new(Pubkey::default(), 5u64);
757        let messages = vec![
758            RexValue::plain_string("message1"),
759            RexValue::plain_string("message2"),
760        ];
761        let op = WebSocketOperation::send(rex_id, messages.clone());
762        assert!(op.is_send());
763        assert!(!op.is_connect());
764        assert!(!op.is_read());
765
766        if let WebSocketOperation::Send {
767            connection_rex_id,
768            messages: op_messages,
769        } = op
770        {
771            assert_eq!(connection_rex_id, rex_id);
772            assert_eq!(op_messages, messages);
773        } else {
774            panic!("Expected Send variant");
775        }
776    }
777
778    #[test]
779    fn test_websocket_send_messages_getter() {
780        let rex_id = RexId::new(Pubkey::default(), 10u64);
781        let messages = vec![RexValue::plain_string("test")];
782        let send_op = WebSocketOperation::Send {
783            connection_rex_id: rex_id,
784            messages: messages.clone(),
785        };
786
787        assert!(send_op.messages().is_some());
788        assert_eq!(send_op.messages().unwrap(), &messages[..]);
789
790        // Connect and Read should return None
791        let connect_op = WebSocketOperation::Connect {
792            url: "wss://example.com".into(),
793            rex_id,
794        };
795        assert!(connect_op.messages().is_none());
796
797        let read_op = WebSocketOperation::Read {
798            connection_rex_id: rex_id,
799            mode: WebSocketReadMode::default(),
800        };
801        assert!(read_op.messages().is_none());
802    }
803
804    #[test]
805    fn test_websocket_operation_serde_roundtrip_send() {
806        let rex_id = RexId::new(Pubkey::default(), 7u64);
807        let messages = vec![
808            RexValue::plain_string("plain_message"),
809            RexValue::encrypted(b"encrypted_data".to_vec()),
810        ];
811        let op = WebSocketOperation::Send {
812            connection_rex_id: rex_id,
813            messages,
814        };
815
816        // Serialize to JSON
817        let json = serde_json::to_string(&op).expect("Failed to serialize");
818        // Deserialize back
819        let deserialized: WebSocketOperation =
820            serde_json::from_str(&json).expect("Failed to deserialize");
821
822        assert_eq!(op, deserialized);
823    }
824
825    #[test]
826    fn test_websocket_operation_borsh_roundtrip_send() {
827        let rex_id = RexId::new(Pubkey::default(), 8u64);
828        let messages = vec![
829            RexValue::plain_string("hello"),
830            RexValue::plain_string("world"),
831        ];
832        let op = WebSocketOperation::Send {
833            connection_rex_id: rex_id,
834            messages,
835        };
836
837        // Serialize to Borsh
838        let bytes = borsh::to_vec(&op).expect("Failed to serialize");
839        // Deserialize back
840        let deserialized: WebSocketOperation =
841            borsh::from_slice(&bytes).expect("Failed to deserialize");
842
843        assert_eq!(op, deserialized);
844    }
845
846    #[test]
847    fn test_websocket_send_display() {
848        let rex_id = RexId::new(Pubkey::default(), 20u64);
849        let messages = vec![
850            RexValue::plain_string("msg1"),
851            RexValue::plain_string("msg2"),
852            RexValue::plain_string("msg3"),
853        ];
854        let send_op = WebSocketOperation::Send {
855            connection_rex_id: rex_id,
856            messages,
857        };
858
859        let display = format!("{}", send_op);
860        assert!(display.contains("Send"));
861        assert!(display.contains("connection_rex_id"));
862        assert!(display.contains("messages_count=3"));
863    }
864
865    #[test]
866    fn test_websocket_send_empty_messages() {
867        let rex_id = RexId::new(Pubkey::default(), 15u64);
868        let op = WebSocketOperation::send(rex_id, vec![]);
869
870        assert!(op.is_send());
871        assert_eq!(op.messages().unwrap().len(), 0);
872
873        let display = format!("{}", op);
874        assert!(display.contains("messages_count=0"));
875    }
876
877    #[test]
878    fn test_websocket_send_with_encrypted_messages() {
879        let rex_id = RexId::new(Pubkey::default(), 25u64);
880        let messages = vec![
881            RexValue::encrypted(b"encrypted1".to_vec()),
882            RexValue::encrypted(b"encrypted2".to_vec()),
883        ];
884        let op = WebSocketOperation::Send {
885            connection_rex_id: rex_id,
886            messages: messages.clone(),
887        };
888
889        // Verify serde roundtrip preserves encrypted messages
890        let json = serde_json::to_string(&op).expect("Failed to serialize");
891        let deserialized: WebSocketOperation =
892            serde_json::from_str(&json).expect("Failed to deserialize");
893        assert_eq!(op, deserialized);
894
895        // Verify borsh roundtrip preserves encrypted messages
896        let bytes = borsh::to_vec(&op).expect("Failed to serialize");
897        let deserialized: WebSocketOperation =
898            borsh::from_slice(&bytes).expect("Failed to deserialize");
899        assert_eq!(op, deserialized);
900    }
901
902    #[test]
903    fn test_target_rex_program_websocket_send_serde_roundtrip() {
904        let rex_id = RexId::new(Pubkey::default(), 30u64);
905        let messages = vec![RexValue::plain_string("test_message")];
906        let target = TargetRexProgram::WebSocket(WebSocketOperation::Send {
907            connection_rex_id: rex_id,
908            messages,
909        });
910
911        // Serialize to JSON
912        let json = serde_json::to_string(&target).expect("Failed to serialize");
913        // Deserialize back
914        let deserialized: TargetRexProgram =
915            serde_json::from_str(&json).expect("Failed to deserialize");
916
917        assert_eq!(target, deserialized);
918    }
919
920    // ========================================================================
921    // WebSocket Close Operation Tests
922    // ========================================================================
923
924    #[test]
925    fn test_websocket_close_operation_creation() {
926        let rex_id = RexId::new(Pubkey::default(), 50u64);
927        let op = WebSocketOperation::close(rex_id);
928        assert!(op.is_close());
929        assert!(!op.is_connect());
930        assert!(!op.is_read());
931        assert!(!op.is_send());
932
933        if let WebSocketOperation::Close { connection_rex_id } = op {
934            assert_eq!(connection_rex_id, rex_id);
935        } else {
936            panic!("Expected Close variant");
937        }
938    }
939
940    #[test]
941    fn test_websocket_close_connection_rex_id_getter() {
942        let rex_id = RexId::new(Pubkey::default(), 55u64);
943        let close_op = WebSocketOperation::Close {
944            connection_rex_id: rex_id,
945        };
946        assert!(close_op.connection_rex_id().is_some());
947        assert_eq!(*close_op.connection_rex_id().unwrap(), rex_id);
948    }
949
950    #[test]
951    fn test_websocket_operation_serde_roundtrip_close() {
952        let rex_id = RexId::new(Pubkey::default(), 60u64);
953        let op = WebSocketOperation::Close {
954            connection_rex_id: rex_id,
955        };
956
957        // Serialize to JSON
958        let json = serde_json::to_string(&op).expect("Failed to serialize");
959        // Deserialize back
960        let deserialized: WebSocketOperation =
961            serde_json::from_str(&json).expect("Failed to deserialize");
962
963        assert_eq!(op, deserialized);
964    }
965
966    #[test]
967    fn test_websocket_operation_borsh_roundtrip_close() {
968        let rex_id = RexId::new(Pubkey::default(), 65u64);
969        let op = WebSocketOperation::Close {
970            connection_rex_id: rex_id,
971        };
972
973        // Serialize to Borsh
974        let bytes = borsh::to_vec(&op).expect("Failed to serialize");
975        // Deserialize back
976        let deserialized: WebSocketOperation =
977            borsh::from_slice(&bytes).expect("Failed to deserialize");
978
979        assert_eq!(op, deserialized);
980    }
981
982    #[test]
983    fn test_websocket_close_display() {
984        let rex_id = RexId::new(Pubkey::default(), 70u64);
985        let close_op = WebSocketOperation::Close {
986            connection_rex_id: rex_id,
987        };
988
989        let display = format!("{}", close_op);
990        assert!(display.contains("Close"));
991        assert!(display.contains("connection_rex_id"));
992        assert!(display.contains(&rex_id.to_string()));
993    }
994
995    #[test]
996    fn test_target_rex_program_websocket_close_serde_roundtrip() {
997        let rex_id = RexId::new(Pubkey::default(), 75u64);
998        let target = TargetRexProgram::WebSocket(WebSocketOperation::Close {
999            connection_rex_id: rex_id,
1000        });
1001
1002        // Serialize to JSON
1003        let json = serde_json::to_string(&target).expect("Failed to serialize");
1004        // Deserialize back
1005        let deserialized: TargetRexProgram =
1006            serde_json::from_str(&json).expect("Failed to deserialize");
1007
1008        assert_eq!(target, deserialized);
1009    }
1010}