Skip to main content

bitwarden_ipc/
ipc_client.rs

1use std::sync::{Arc, Mutex};
2
3use bitwarden_threading::cancellation_token::CancellationToken;
4use serde::de::DeserializeOwned;
5use thiserror::Error;
6use tokio::select;
7
8use crate::{
9    constants::CHANNEL_BUFFER_CAPACITY,
10    error::{
11        AlreadyRunningError, ErrorKind, IpcErrorKind, ReceiveError, SendError, SubscribeError,
12        TypedReceiveError,
13    },
14    message::{
15        IncomingMessage, OutgoingMessage, PayloadTypeName, TypedIncomingMessage,
16        TypedOutgoingMessage,
17    },
18    rpc::{
19        exec::{handler::ErasedRpcHandler, handler_registry::RpcHandlerRegistry},
20        request_message::{RPC_REQUEST_PAYLOAD_TYPE_NAME, RpcRequestPayload},
21        response_message::OutgoingRpcResponseMessage,
22    },
23    serde_utils,
24    traits::{CommunicationBackend, CryptoProvider, SessionRepository},
25};
26
27/// A subscription to receive messages over IPC.
28/// The subcription will start buffering messages after its creation and return them
29/// when receive() is called. Messages received before the subscription was created will not be
30/// returned.
31pub struct IpcClientSubscription {
32    pub(crate) receiver: tokio::sync::broadcast::Receiver<IncomingMessage>,
33    pub(crate) topic: Option<String>,
34}
35
36/// A subscription to receive messages over IPC.
37/// The subcription will start buffering messages after its creation and return them
38/// when receive() is called. Messages received before the subscription was created will not be
39/// returned.
40pub struct IpcClientTypedSubscription<Payload: DeserializeOwned + PayloadTypeName>(
41    IpcClientSubscription,
42    std::marker::PhantomData<Payload>,
43);
44
45/// Internal shared state for the IPC client.
46struct IpcClientInner<Crypto, Com, Ses>
47where
48    Crypto: CryptoProvider<Com, Ses>,
49    Com: CommunicationBackend,
50    Ses: SessionRepository<Crypto::Session>,
51{
52    crypto: Crypto,
53    communication: Com,
54    sessions: Ses,
55
56    handlers: RpcHandlerRegistry,
57    incoming: Mutex<Option<tokio::sync::broadcast::Receiver<IncomingMessage>>>,
58    cancellation_token: Mutex<Option<CancellationToken>>,
59}
60
61/// An IPC client that handles communication between different components and clients.
62/// It uses a crypto provider to encrypt and decrypt messages, a communication backend to send and
63/// receive messages, and a session repository to persist sessions.
64///
65/// This is the concrete implementation of the [`IpcClient`](crate::IpcClient) trait.
66pub struct IpcClientImpl<Crypto, Com, Ses>
67where
68    Crypto: CryptoProvider<Com, Ses>,
69    Com: CommunicationBackend,
70    Ses: SessionRepository<Crypto::Session>,
71{
72    inner: Arc<IpcClientInner<Crypto, Com, Ses>>,
73}
74
75impl<Crypto, Com, Ses> Clone for IpcClientImpl<Crypto, Com, Ses>
76where
77    Crypto: CryptoProvider<Com, Ses>,
78    Com: CommunicationBackend,
79    Ses: SessionRepository<Crypto::Session>,
80{
81    fn clone(&self) -> Self {
82        Self {
83            inner: self.inner.clone(),
84        }
85    }
86}
87
88impl<Crypto, Com, Ses> IpcClientImpl<Crypto, Com, Ses>
89where
90    Crypto: CryptoProvider<Com, Ses>,
91    Com: CommunicationBackend,
92    Ses: SessionRepository<Crypto::Session>,
93{
94    /// Create a new IPC client with the provided crypto provider, communication backend, and
95    /// session repository.
96    pub fn new(crypto: Crypto, communication: Com, sessions: Ses) -> Self {
97        Self {
98            inner: Arc::new(IpcClientInner {
99                crypto,
100                communication,
101                sessions,
102
103                handlers: RpcHandlerRegistry::new(),
104                incoming: Mutex::new(None),
105                cancellation_token: Mutex::new(None),
106            }),
107        }
108    }
109}
110
111#[async_trait::async_trait]
112impl<Crypto, Com, Ses> crate::ipc_client_trait::IpcClient for IpcClientImpl<Crypto, Com, Ses>
113where
114    Crypto: CryptoProvider<Com, Ses>,
115    Com: CommunicationBackend,
116    Ses: SessionRepository<Crypto::Session>,
117{
118    async fn start(
119        &self,
120        cancellation_token: Option<CancellationToken>,
121    ) -> Result<(), AlreadyRunningError> {
122        if self.is_running() {
123            return Err(AlreadyRunningError);
124        }
125
126        let cancellation_token = cancellation_token.unwrap_or_default();
127        self.inner
128            .cancellation_token
129            .lock()
130            .expect("Failed to lock cancellation token mutex")
131            .replace(cancellation_token.clone());
132
133        let com_receiver = self.inner.communication.subscribe().await;
134        let (client_tx, client_rx) = tokio::sync::broadcast::channel(CHANNEL_BUFFER_CAPACITY);
135
136        self.inner
137            .incoming
138            .lock()
139            .expect("Failed to lock incoming mutex")
140            .replace(client_rx);
141
142        let inner = self.inner.clone();
143        let future = async move {
144            loop {
145                let rpc_topic = RPC_REQUEST_PAYLOAD_TYPE_NAME.to_owned();
146                select! {
147                    _ = cancellation_token.cancelled() => {
148                        tracing::debug!("Cancellation signal received, stopping IPC client");
149                        break;
150                    }
151                    received = inner.crypto.receive(&com_receiver, &inner.communication, &inner.sessions) => {
152                        match received {
153                            Ok(message) if message.topic == Some(rpc_topic) => {
154                                handle_rpc_request(&inner, message)
155                            }
156                            Ok(message) => {
157                                if client_tx.send(message).is_err() {
158                                    tracing::error!("Failed to save incoming message");
159                                    break;
160                                };
161                            }
162                            Err(error) if matches!(error.kind(), ErrorKind::Fatal) => {
163                                tracing::error!(?error, "Fatal error receiving message, stopping IPC client");
164                                break;
165                            }
166                            Err(error) => {
167                                tracing::warn!(?error, "Recoverable error receiving message, continuing");
168                            }
169                        }
170                    }
171                }
172            }
173            tracing::debug!("IPC client shutting down");
174            stop_inner(&inner);
175        };
176
177        #[cfg(not(target_arch = "wasm32"))]
178        tokio::spawn(future);
179
180        #[cfg(target_arch = "wasm32")]
181        wasm_bindgen_futures::spawn_local(future);
182
183        Ok(())
184    }
185
186    fn is_running(&self) -> bool {
187        let has_incoming = self
188            .inner
189            .incoming
190            .lock()
191            .expect("Failed to lock incoming mutex")
192            .as_ref()
193            .map(|receiver| !receiver.is_closed())
194            .unwrap_or(false);
195        let has_cancellation_token = self
196            .inner
197            .cancellation_token
198            .lock()
199            .expect("Failed to lock cancellation token mutex")
200            .is_some();
201        has_incoming && has_cancellation_token
202    }
203
204    async fn send(&self, message: OutgoingMessage) -> Result<(), SendError> {
205        let result = self
206            .inner
207            .crypto
208            .send(&self.inner.communication, &self.inner.sessions, message)
209            .await;
210
211        if let Err(ref error) = result {
212            match error.kind() {
213                ErrorKind::Fatal => {
214                    tracing::error!(?error, "Fatal error sending message, stopping IPC client");
215                    stop_inner(&self.inner);
216                }
217                // An unreachable destination is an expected condition and not logged
218                ErrorKind::Unreachable => {}
219                // Every other recoverable send failure is still surfaced.
220                ErrorKind::Other => {
221                    tracing::warn!(
222                        ?error,
223                        "Recoverable error sending message, IPC client will continue running"
224                    );
225                }
226            }
227        }
228
229        result.map_err(|e| match e.kind() {
230            ErrorKind::Unreachable => SendError::Unreachable,
231            _ => SendError::Other(format!("{e:?}")),
232        })
233    }
234
235    async fn subscribe(
236        &self,
237        topic: Option<String>,
238    ) -> Result<IpcClientSubscription, SubscribeError> {
239        Ok(IpcClientSubscription {
240            receiver: self
241                .inner
242                .incoming
243                .lock()
244                .expect("Failed to lock incoming mutex")
245                .as_ref()
246                .ok_or(SubscribeError::NotStarted)?
247                .resubscribe(),
248            topic,
249        })
250    }
251
252    async fn register_rpc_handler_erased(&self, name: &str, handler: Box<dyn ErasedRpcHandler>) {
253        self.inner
254            .handlers
255            .register_erased(name.to_owned(), handler)
256            .await;
257    }
258}
259
260fn stop_inner<Crypto, Com, Ses>(inner: &IpcClientInner<Crypto, Com, Ses>)
261where
262    Crypto: CryptoProvider<Com, Ses>,
263    Com: CommunicationBackend,
264    Ses: SessionRepository<Crypto::Session>,
265{
266    let mut cancellation_token = inner
267        .cancellation_token
268        .lock()
269        .expect("Failed to lock cancellation token mutex");
270
271    if let Some(cancellation_token) = cancellation_token.take() {
272        cancellation_token.cancel();
273    }
274}
275
276fn handle_rpc_request<Crypto, Com, Ses>(
277    inner: &Arc<IpcClientInner<Crypto, Com, Ses>>,
278    incoming_message: IncomingMessage,
279) where
280    Crypto: CryptoProvider<Com, Ses>,
281    Com: CommunicationBackend,
282    Ses: SessionRepository<Crypto::Session>,
283{
284    let inner = inner.clone();
285    let future = async move {
286        #[derive(Debug, Error)]
287        enum HandleError {
288            #[error("Failed to deserialize request message: {0}")]
289            Deserialize(String),
290
291            #[error("Failed to serialize response message: {0}")]
292            Serialize(String),
293        }
294
295        async fn handle(
296            incoming_message: IncomingMessage,
297            handlers: &RpcHandlerRegistry,
298        ) -> Result<OutgoingMessage, HandleError> {
299            let request = RpcRequestPayload::from_slice(incoming_message.payload.clone()).map_err(
300                |e: serde_utils::DeserializeError| HandleError::Deserialize(e.to_string()),
301            )?;
302
303            let response = handlers.handle(&request).await;
304
305            let response_message = OutgoingRpcResponseMessage {
306                request_id: request.request_id(),
307                request_type: request.request_type(),
308                result: response,
309            };
310
311            let outgoing = TypedOutgoingMessage {
312                payload: response_message,
313                destination: incoming_message.source.into(),
314            }
315            .try_into()
316            .map_err(|e: serde_utils::SerializeError| HandleError::Serialize(e.to_string()))?;
317
318            Ok(outgoing)
319        }
320
321        match handle(incoming_message, &inner.handlers).await {
322            Ok(outgoing_message) => {
323                // Send response directly through the crypto provider (not through the trait)
324                // since we're inside the background task and don't have a trait object.
325                let result = inner
326                    .crypto
327                    .send(&inner.communication, &inner.sessions, outgoing_message)
328                    .await;
329                if result.is_err() {
330                    tracing::error!("Failed to send response message");
331                }
332            }
333            Err(error) => {
334                tracing::error!(%error, "Error handling RPC request");
335            }
336        }
337    };
338
339    #[cfg(not(target_arch = "wasm32"))]
340    tokio::spawn(future);
341
342    #[cfg(target_arch = "wasm32")]
343    wasm_bindgen_futures::spawn_local(future);
344}
345
346impl IpcClientSubscription {
347    /// Receive a message, optionally filtering by topic.
348    /// Setting the cancellation_token to `None` will wait indefinitely.
349    pub async fn receive(
350        &mut self,
351        cancellation_token: Option<CancellationToken>,
352    ) -> Result<IncomingMessage, ReceiveError> {
353        let cancellation_token = cancellation_token.unwrap_or_default();
354
355        loop {
356            select! {
357                _ = cancellation_token.cancelled() => {
358                    return Err(ReceiveError::Cancelled)
359                }
360                result = self.receiver.recv() => {
361                    let received = result?;
362                    if self.topic.is_none() || received.topic == self.topic {
363                        return Ok::<IncomingMessage, ReceiveError>(received);
364                    }
365                }
366            }
367        }
368    }
369}
370
371impl<Payload> IpcClientTypedSubscription<Payload>
372where
373    Payload: DeserializeOwned + PayloadTypeName,
374{
375    pub(crate) fn new(subscription: IpcClientSubscription) -> Self {
376        Self(subscription, std::marker::PhantomData)
377    }
378
379    /// Receive a message.
380    /// Setting the cancellation_token to `None` will wait indefinitely.
381    pub async fn receive(
382        &mut self,
383        cancellation_token: Option<CancellationToken>,
384    ) -> Result<TypedIncomingMessage<Payload>, TypedReceiveError> {
385        let received = self.0.receive(cancellation_token).await?;
386        received
387            .try_into()
388            .map_err(|e: serde_utils::DeserializeError| TypedReceiveError::Typing(e.to_string()))
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use std::{collections::HashMap, time::Duration};
395
396    use bitwarden_threading::time::sleep;
397    use serde::{Deserialize, Serialize};
398
399    use super::*;
400    use crate::{
401        IpcClientExt,
402        endpoint::{Endpoint, HostId, Source},
403        ipc_client_trait::IpcClient,
404        message::PayloadTypeName,
405        rpc::{
406            request::RpcRequest,
407            request_message::{RPC_REQUEST_PAYLOAD_TYPE_NAME, RpcRequestMessage},
408            response_message::IncomingRpcResponseMessage,
409        },
410        traits::{InMemorySessionRepository, NoEncryptionCryptoProvider, TestCommunicationBackend},
411    };
412
413    /// Error type for [`TestCryptoProvider`] that carries an explicit fatal/recoverable
414    /// classification so tests can exercise both control-flow paths.
415    #[derive(Debug, Clone)]
416    struct TestCryptoError {
417        // Read only through the derived `Debug` impl (the outer `SendError` wraps it via
418        // `{e:?}`), which dead-code analysis does not count as a use.
419        #[allow(dead_code)]
420        message: String,
421        fatal: bool,
422    }
423
424    impl IpcErrorKind for TestCryptoError {
425        fn kind(&self) -> ErrorKind {
426            if self.fatal {
427                ErrorKind::Fatal
428            } else {
429                ErrorKind::Other
430            }
431        }
432    }
433
434    struct TestCryptoProvider {
435        /// Simulate a send result. Set to `None` wait indefinitely
436        send_result: Option<Result<(), TestCryptoError>>,
437        /// Simulate a receive result. Set to `None` wait indefinitely
438        receive_result: Option<Result<IncomingMessage, TestCryptoError>>,
439    }
440
441    type TestSessionRepository = InMemorySessionRepository<String>;
442    impl CryptoProvider<TestCommunicationBackend, TestSessionRepository> for TestCryptoProvider {
443        type Session = String;
444        type SendError = TestCryptoError;
445        type ReceiveError = TestCryptoError;
446
447        async fn receive(
448            &self,
449            _receiver: &<TestCommunicationBackend as CommunicationBackend>::Receiver,
450            _communication: &TestCommunicationBackend,
451            _sessions: &TestSessionRepository,
452        ) -> Result<IncomingMessage, Self::ReceiveError> {
453            match &self.receive_result {
454                Some(result) => {
455                    // Yield (and throttle) so a recoverable error that makes the processing loop
456                    // `continue` doesn't busy-spin and starve the single-threaded test runtime.
457                    // Real backends await their underlying transport here, which has the same
458                    // effect.
459                    sleep(Duration::from_millis(5)).await;
460                    result.clone()
461                }
462                None => {
463                    // Simulate waiting for a message but never returning
464                    sleep(Duration::from_secs(600)).await;
465                    Err(TestCryptoError {
466                        message: "Simulated timeout".to_string(),
467                        fatal: true,
468                    })
469                }
470            }
471        }
472
473        async fn send(
474            &self,
475            _communication: &TestCommunicationBackend,
476            _sessions: &TestSessionRepository,
477            _message: OutgoingMessage,
478        ) -> Result<(), Self::SendError> {
479            match &self.send_result {
480                Some(result) => result.clone(),
481                None => {
482                    // Simulate waiting for a message to be send but never returning
483                    sleep(Duration::from_secs(600)).await;
484                    Err(TestCryptoError {
485                        message: "Simulated timeout".to_string(),
486                        fatal: true,
487                    })
488                }
489            }
490        }
491    }
492
493    #[tokio::test]
494    async fn returns_send_error_when_crypto_provider_returns_error() {
495        let message = OutgoingMessage {
496            payload: vec![],
497            destination: Endpoint::BrowserBackground { id: HostId::Own },
498            topic: None,
499        };
500        let crypto_provider = TestCryptoProvider {
501            send_result: Some(Err(TestCryptoError {
502                message: "Crypto error".to_string(),
503                fatal: false,
504            })),
505            receive_result: Some(Err(TestCryptoError {
506                message: "Should not have be called".to_string(),
507                fatal: false,
508            })),
509        };
510        let communication_provider = TestCommunicationBackend::new();
511        let session_map = TestSessionRepository::new(HashMap::new());
512        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
513        let _ = client.start(None).await;
514
515        let error = client.send(message).await.unwrap_err();
516
517        assert!(error.to_string().contains("Crypto error"));
518    }
519
520    #[tokio::test]
521    async fn communication_provider_has_outgoing_message_when_sending_through_ipc_client() {
522        let message = OutgoingMessage {
523            payload: vec![],
524            destination: Endpoint::BrowserBackground { id: HostId::Own },
525            topic: None,
526        };
527        let crypto_provider = NoEncryptionCryptoProvider;
528        let communication_provider = TestCommunicationBackend::new();
529        let session_map = InMemorySessionRepository::new(HashMap::new());
530        let client =
531            IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
532        let _ = client.start(None).await;
533
534        client.send(message.clone()).await.unwrap();
535
536        let outgoing_messages = communication_provider.outgoing().await;
537        assert_eq!(outgoing_messages, vec![message]);
538    }
539
540    #[tokio::test]
541    async fn returns_received_message_when_received_from_backend() {
542        let message = IncomingMessage {
543            payload: vec![],
544            source: Source::Web {
545                tab_id: 9001,
546                document_id: "doc-1".to_string(),
547                origin: "https://example.com".to_string(),
548            },
549            destination: Endpoint::BrowserBackground { id: HostId::Own },
550            topic: None,
551        };
552        let crypto_provider = NoEncryptionCryptoProvider;
553        let communication_provider = TestCommunicationBackend::new();
554        let session_map = InMemorySessionRepository::new(HashMap::new());
555        let client =
556            IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
557        let _ = client.start(None).await;
558
559        let mut subscription = client
560            .subscribe(None)
561            .await
562            .expect("Subscribing should not fail");
563        communication_provider.push_incoming(message.clone());
564        let received_message = subscription.receive(None).await.unwrap();
565
566        assert_eq!(received_message, message);
567    }
568
569    #[tokio::test]
570    async fn skips_non_matching_topics_and_returns_first_matching_message() {
571        let non_matching_message = IncomingMessage {
572            payload: vec![],
573            source: Source::Web {
574                tab_id: 9001,
575                document_id: "doc-1".to_string(),
576                origin: "https://example.com".to_string(),
577            },
578            destination: Endpoint::BrowserBackground { id: HostId::Own },
579            topic: Some("non_matching_topic".to_owned()),
580        };
581        let matching_message = IncomingMessage {
582            payload: vec![109],
583            source: Source::Web {
584                tab_id: 9001,
585                document_id: "doc-1".to_string(),
586                origin: "https://example.com".to_string(),
587            },
588            destination: Endpoint::BrowserBackground { id: HostId::Own },
589            topic: Some("matching_topic".to_owned()),
590        };
591
592        let crypto_provider = NoEncryptionCryptoProvider;
593        let communication_provider = TestCommunicationBackend::new();
594        let session_map = InMemorySessionRepository::new(HashMap::new());
595        let client =
596            IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
597        let _ = client.start(None).await;
598        let mut subscription = client
599            .subscribe(Some("matching_topic".to_owned()))
600            .await
601            .expect("Subscribing should not fail");
602        communication_provider.push_incoming(non_matching_message.clone());
603        communication_provider.push_incoming(non_matching_message.clone());
604        communication_provider.push_incoming(matching_message.clone());
605
606        let received_message: IncomingMessage = subscription.receive(None).await.unwrap();
607
608        assert_eq!(received_message, matching_message);
609    }
610
611    #[tokio::test]
612    async fn skips_unrelated_messages_and_returns_typed_message() {
613        #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
614        struct TestPayload {
615            some_data: String,
616        }
617
618        impl PayloadTypeName for TestPayload {
619            const PAYLOAD_TYPE_NAME: &str = "TestPayload";
620        }
621
622        let unrelated = IncomingMessage {
623            payload: vec![],
624            source: Source::Web {
625                tab_id: 9001,
626                document_id: "doc-1".to_string(),
627                origin: "https://example.com".to_string(),
628            },
629            destination: Endpoint::BrowserBackground { id: HostId::Own },
630            topic: None,
631        };
632        let typed_message = crate::message::TypedIncomingMessage {
633            payload: TestPayload {
634                some_data: "Hello, world!".to_string(),
635            },
636            source: Source::Web {
637                tab_id: 9001,
638                document_id: "doc-1".to_string(),
639                origin: "https://example.com".to_string(),
640            },
641            destination: Endpoint::BrowserBackground { id: HostId::Own },
642        };
643
644        let crypto_provider = NoEncryptionCryptoProvider;
645        let communication_provider = TestCommunicationBackend::new();
646        let session_map = InMemorySessionRepository::new(HashMap::new());
647        let client =
648            IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
649        let _ = client.start(None).await;
650        let mut subscription = client
651            .subscribe_typed::<TestPayload>()
652            .await
653            .expect("Subscribing should not fail");
654        communication_provider.push_incoming(unrelated.clone());
655        communication_provider.push_incoming(unrelated.clone());
656        communication_provider.push_incoming(
657            typed_message
658                .clone()
659                .try_into()
660                .expect("Serialization should not fail"),
661        );
662
663        let received_message = subscription.receive(None).await.unwrap();
664
665        assert_eq!(received_message, typed_message);
666    }
667
668    #[tokio::test]
669    async fn returns_error_if_related_message_was_not_deserializable() {
670        #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
671        struct TestPayload {
672            some_data: String,
673        }
674
675        impl PayloadTypeName for TestPayload {
676            const PAYLOAD_TYPE_NAME: &str = "TestPayload";
677        }
678
679        let non_deserializable_message = IncomingMessage {
680            payload: vec![],
681            source: Source::Web {
682                tab_id: 9001,
683                document_id: "doc-1".to_string(),
684                origin: "https://example.com".to_string(),
685            },
686            destination: Endpoint::BrowserBackground { id: HostId::Own },
687            topic: Some("TestPayload".to_owned()),
688        };
689
690        let crypto_provider = NoEncryptionCryptoProvider;
691        let communication_provider = TestCommunicationBackend::new();
692        let session_map = InMemorySessionRepository::new(HashMap::new());
693        let client =
694            IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
695        let _ = client.start(None).await;
696        let mut subscription = client
697            .subscribe_typed::<TestPayload>()
698            .await
699            .expect("Subscribing should not fail");
700        communication_provider.push_incoming(non_deserializable_message.clone());
701
702        let result = subscription.receive(None).await;
703        assert!(matches!(result, Err(TypedReceiveError::Typing(_))));
704    }
705
706    #[tokio::test]
707    async fn ipc_client_stops_if_crypto_returns_fatal_send_error() {
708        let message = OutgoingMessage {
709            payload: vec![],
710            destination: Endpoint::BrowserBackground { id: HostId::Own },
711            topic: None,
712        };
713        let crypto_provider = TestCryptoProvider {
714            send_result: Some(Err(TestCryptoError {
715                message: "Crypto error".to_string(),
716                fatal: true,
717            })),
718            receive_result: None,
719        };
720        let communication_provider = TestCommunicationBackend::new();
721        let session_map = TestSessionRepository::new(HashMap::new());
722        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
723        let _ = client.start(None).await;
724
725        let error = client.send(message).await.unwrap_err();
726        let is_running = client.is_running();
727
728        assert!(error.to_string().contains("Crypto error"));
729        assert!(!is_running);
730    }
731
732    #[tokio::test]
733    async fn ipc_client_keeps_running_if_crypto_returns_recoverable_send_error() {
734        let message = OutgoingMessage {
735            payload: vec![],
736            destination: Endpoint::BrowserBackground { id: HostId::Own },
737            topic: None,
738        };
739        let crypto_provider = TestCryptoProvider {
740            // A recoverable send error (e.g. a handshake timeout because the peer is down) must
741            // not tear down the shared client.
742            send_result: Some(Err(TestCryptoError {
743                message: "Crypto error".to_string(),
744                fatal: false,
745            })),
746            // Block forever on receive so the loop stays alive while we inspect it.
747            receive_result: None,
748        };
749        let communication_provider = TestCommunicationBackend::new();
750        let session_map = TestSessionRepository::new(HashMap::new());
751        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
752        let _ = client.start(None).await;
753
754        let error = client.send(message).await.unwrap_err();
755        let is_running = client.is_running();
756
757        // The error is still surfaced to the caller...
758        assert!(error.to_string().contains("Crypto error"));
759        // ...but the client keeps running so future sends/requests can succeed.
760        assert!(is_running);
761    }
762
763    #[tokio::test]
764    async fn ipc_client_stops_if_crypto_returns_fatal_receive_error() {
765        let crypto_provider = TestCryptoProvider {
766            send_result: None,
767            receive_result: Some(Err(TestCryptoError {
768                message: "Crypto error".to_string(),
769                fatal: true,
770            })),
771        };
772        let communication_provider = TestCommunicationBackend::new();
773        let session_map = TestSessionRepository::new(HashMap::new());
774        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
775        let cancellation_token = CancellationToken::new();
776        let _ = client.start(Some(cancellation_token.clone())).await;
777
778        // Give the client some time to process the error
779        tokio::time::sleep(Duration::from_millis(100)).await;
780        let is_running = client.is_running();
781
782        assert!(!is_running);
783        assert!(cancellation_token.is_cancelled());
784    }
785
786    #[tokio::test]
787    async fn ipc_client_keeps_running_if_crypto_returns_recoverable_receive_error() {
788        let crypto_provider = TestCryptoProvider {
789            send_result: None,
790            // A recoverable receive error must not stop the processing loop.
791            receive_result: Some(Err(TestCryptoError {
792                message: "Crypto error".to_string(),
793                fatal: false,
794            })),
795        };
796        let communication_provider = TestCommunicationBackend::new();
797        let session_map = TestSessionRepository::new(HashMap::new());
798        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
799        let cancellation_token = CancellationToken::new();
800        let _ = client.start(Some(cancellation_token.clone())).await;
801
802        // Give the client time to hit the recoverable receive error (repeatedly).
803        tokio::time::sleep(Duration::from_millis(100)).await;
804        let is_running = client.is_running();
805
806        assert!(is_running);
807        assert!(!cancellation_token.is_cancelled());
808    }
809
810    #[tokio::test]
811    async fn ipc_client_is_not_running_if_cancellation_token_is_cancelled() {
812        let crypto_provider = TestCryptoProvider {
813            send_result: None,
814            receive_result: None,
815        };
816        let communication_provider = TestCommunicationBackend::new();
817        let session_map = TestSessionRepository::new(HashMap::new());
818        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
819        let cancellation_token = CancellationToken::new();
820        let _ = client.start(Some(cancellation_token.clone())).await;
821
822        // Give the client some time to process
823        tokio::time::sleep(Duration::from_millis(100)).await;
824
825        // Cancel the token and give the client some time to process the cancellation
826        cancellation_token.cancel();
827        tokio::time::sleep(Duration::from_millis(100)).await;
828        let is_running = client.is_running();
829
830        assert!(!is_running);
831    }
832
833    #[tokio::test]
834    async fn ipc_client_is_running_if_no_errors_are_encountered() {
835        let crypto_provider = TestCryptoProvider {
836            send_result: None,
837            receive_result: None,
838        };
839        let communication_provider = TestCommunicationBackend::new();
840        let session_map = TestSessionRepository::new(HashMap::new());
841        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
842        let cancellation_token = CancellationToken::new();
843        let _ = client.start(Some(cancellation_token.clone())).await;
844
845        // Give the client some time to process
846        tokio::time::sleep(Duration::from_millis(100)).await;
847        let is_running = client.is_running();
848
849        assert!(is_running);
850        assert!(!cancellation_token.is_cancelled());
851    }
852
853    #[tokio::test]
854    async fn ipc_client_is_not_running_if_not_started() {
855        let crypto_provider = TestCryptoProvider {
856            send_result: None,
857            receive_result: None,
858        };
859        let communication_provider = TestCommunicationBackend::new();
860        let session_map = TestSessionRepository::new(HashMap::new());
861        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
862
863        // Give the client some time to process
864        tokio::time::sleep(Duration::from_millis(100)).await;
865        let is_running = client.is_running();
866
867        assert!(!is_running);
868    }
869
870    #[tokio::test]
871    async fn ipc_client_start_returns_error_if_already_running() {
872        let crypto_provider = TestCryptoProvider {
873            send_result: None,
874            receive_result: None,
875        };
876        let communication_provider = TestCommunicationBackend::new();
877        let session_map = TestSessionRepository::new(HashMap::new());
878        let client = IpcClientImpl::new(crypto_provider, communication_provider, session_map);
879        let cancellation_token = CancellationToken::new();
880        let first_result = client.start(Some(cancellation_token.clone())).await;
881        assert_eq!(first_result, Ok(()));
882
883        // Give the client some time to process
884        tokio::time::sleep(Duration::from_millis(100)).await;
885        assert!(client.is_running());
886
887        let second_result = client.start(Some(cancellation_token.clone())).await;
888        assert_eq!(second_result, Err(AlreadyRunningError));
889    }
890
891    mod request {
892        use super::*;
893        use crate::RpcHandler;
894
895        #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
896        struct TestRequest {
897            a: i32,
898            b: i32,
899        }
900
901        #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
902        struct TestResponse {
903            result: i32,
904        }
905
906        impl RpcRequest for TestRequest {
907            type Response = TestResponse;
908
909            const NAME: &str = "TestRequest";
910        }
911
912        struct TestHandler;
913
914        impl RpcHandler for TestHandler {
915            type Request = TestRequest;
916
917            async fn handle(&self, request: Self::Request) -> TestResponse {
918                TestResponse {
919                    result: request.a + request.b,
920                }
921            }
922        }
923
924        #[tokio::test]
925        async fn request_sends_message_and_returns_response() {
926            let crypto_provider = NoEncryptionCryptoProvider;
927            let communication_provider = TestCommunicationBackend::new();
928            let session_map = InMemorySessionRepository::default();
929            let client =
930                IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
931            let _ = client.start(None).await;
932            let request = TestRequest { a: 1, b: 2 };
933            let response = TestResponse { result: 3 };
934
935            // Send the request
936            let request_clone = request.clone();
937            let client_clone = client.clone();
938            let result_handle = tokio::spawn(async move {
939                client_clone
940                    .request::<TestRequest>(
941                        request_clone,
942                        Endpoint::BrowserBackground { id: HostId::Own },
943                        None,
944                    )
945                    .await
946            });
947            tokio::time::sleep(Duration::from_millis(100)).await;
948
949            // Read and verify the outgoing message
950            let outgoing_messages = communication_provider.outgoing().await;
951            let outgoing_request: RpcRequestMessage<TestRequest> =
952                serde_utils::from_slice(&outgoing_messages[0].payload)
953                    .expect("Deserialization should not fail");
954            assert_eq!(outgoing_request.request_type, "TestRequest");
955            assert_eq!(outgoing_request.request, request);
956
957            // Simulate receiving a response
958            let simulated_response = IncomingRpcResponseMessage {
959                result: Ok(response),
960                request_id: outgoing_request.request_id.clone(),
961                request_type: outgoing_request.request_type.clone(),
962            };
963            let simulated_response = IncomingMessage {
964                payload: serde_utils::to_vec(&simulated_response)
965                    .expect("Serialization should not fail"),
966                source: Source::BrowserBackground { id: HostId::Own },
967                destination: Endpoint::Web {
968                    tab_id: 9001,
969                    document_id: "doc-1".to_string(),
970                },
971                topic: Some(
972                    IncomingRpcResponseMessage::<TestRequest>::PAYLOAD_TYPE_NAME.to_owned(),
973                ),
974            };
975            communication_provider.push_incoming(simulated_response);
976
977            // Wait for the response
978            let result = result_handle.await.unwrap();
979            assert_eq!(result.unwrap().result, 3);
980        }
981
982        #[tokio::test]
983        async fn incoming_rpc_message_handles_request_and_returns_response() {
984            let crypto_provider = NoEncryptionCryptoProvider;
985            let communication_provider = TestCommunicationBackend::new();
986            let session_map = InMemorySessionRepository::default();
987            let client =
988                IpcClientImpl::new(crypto_provider, communication_provider.clone(), session_map);
989            let _ = client.start(None).await;
990            let request_id = uuid::Uuid::new_v4().to_string();
991            let request = TestRequest { a: 1, b: 2 };
992            let response = TestResponse { result: 3 };
993
994            // Register the handler
995            client.register_rpc_handler(TestHandler).await;
996
997            // Simulate receiving a request
998            let simulated_request = RpcRequestMessage {
999                request,
1000                request_id: request_id.clone(),
1001                request_type: "TestRequest".to_string(),
1002            };
1003            let simulated_request_message = IncomingMessage {
1004                payload: serde_utils::to_vec(&simulated_request)
1005                    .expect("Serialization should not fail"),
1006                source: Source::Web {
1007                    tab_id: 9001,
1008                    document_id: "doc-1".to_string(),
1009                    origin: "https://example.com".to_string(),
1010                },
1011                destination: Endpoint::BrowserBackground { id: HostId::Own },
1012                topic: Some(RPC_REQUEST_PAYLOAD_TYPE_NAME.to_owned()),
1013            };
1014            communication_provider.push_incoming(simulated_request_message);
1015
1016            // Give the client some time to process the request
1017            tokio::time::sleep(Duration::from_millis(100)).await;
1018
1019            // Read and verify the outgoing message
1020            let outgoing_messages = communication_provider.outgoing().await;
1021            let outgoing_response: IncomingRpcResponseMessage<TestResponse> =
1022                serde_utils::from_slice(&outgoing_messages[0].payload)
1023                    .expect("Deserialization should not fail");
1024
1025            assert_eq!(
1026                outgoing_messages[0].topic,
1027                Some(IncomingRpcResponseMessage::<TestResponse>::PAYLOAD_TYPE_NAME.to_owned())
1028            );
1029            assert_eq!(outgoing_response.request_type, "TestRequest");
1030            assert_eq!(outgoing_response.result, Ok(response));
1031        }
1032    }
1033}