Skip to main content

slim_bindings/
app.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4//! # SLIM Bindings Adapter (UniFFI-Compatible)
5//!
6//! This module provides a language-agnostic FFI interface to SLIM using a hybrid approach
7//! that exposes both synchronous (blocking) and asynchronous versions of operations.
8//!
9//! ## Architecture
10//! - **Flexible Authentication**: Uses `AuthProvider`/`AuthVerifier` enums supporting multiple auth types
11//!   (SharedSecret, JWT, SPIRE, StaticToken) instead of generics (UniFFI requirement)
12//! - **Hybrid API**: Both sync (FFI-exposed) and async (internal) methods
13//! - **Runtime management**: Manages Tokio runtime for blocking operations
14
15use std::sync::Arc;
16
17use tokio::sync::{RwLock, mpsc};
18
19use crate::errors::SlimError;
20use crate::name::Name;
21use crate::{get_global_service, get_runtime};
22
23use crate::session::SessionConfig;
24
25use slim_auth::auth_provider::{AuthProvider, AuthVerifier};
26
27use crate::identity_config::{IdentityProviderConfig, IdentityVerifierConfig};
28
29use futures_timer::Delay;
30use slim_datapath::messages::Name as SlimName;
31use slim_service::Service as SlimService;
32use slim_service::app::App as SlimApp;
33use slim_session::Direction as CoreDirection;
34
35use slim_session::SessionConfig as SlimSessionConfig;
36use slim_session::session_controller::SessionController;
37use slim_session::{Notification, SessionError as SlimSessionError};
38
39/// Direction enum
40/// Indicates whether the App can send, receive, both, or neither.
41#[derive(Clone, Copy, Debug, uniffi::Enum)]
42pub enum Direction {
43    Send,          // Can only send data messages (shutdown_send: false, shutdown_receive: true)
44    Recv,          // Can only receive data messages (shutdown_send: true, shutdown_receive: false)
45    Bidirectional, // Can send and receive data messages (shutdown_send: false, shutdown_receive: false)
46    None, // Neither send nor receive data messages (shutdown_send: true, shutdown_receive: true)
47}
48
49impl From<Direction> for CoreDirection {
50    fn from(direction: Direction) -> Self {
51        match direction {
52            Direction::Send => CoreDirection::Send,
53            Direction::Recv => CoreDirection::Recv,
54            Direction::Bidirectional => CoreDirection::Bidirectional,
55            Direction::None => CoreDirection::None,
56        }
57    }
58}
59
60impl From<CoreDirection> for Direction {
61    fn from(direction: CoreDirection) -> Self {
62        match direction {
63            CoreDirection::Send => Direction::Send,
64            CoreDirection::Recv => Direction::Recv,
65            CoreDirection::Bidirectional => Direction::Bidirectional,
66            CoreDirection::None => Direction::None,
67        }
68    }
69}
70
71// ============================================================================
72// Return Types
73// ============================================================================
74
75/// Result of creating a session, containing the session context and a completion handle
76///
77/// The completion handle should be awaited to ensure the session is fully established.
78#[derive(uniffi::Record)]
79pub struct SessionWithCompletion {
80    /// The session context for performing operations
81    pub session: Arc<crate::Session>,
82    /// Completion handle to wait for session establishment
83    pub completion: Arc<crate::CompletionHandle>,
84}
85
86/// Adapter that bridges the App API with language-bindings interface
87///
88/// This adapter uses enum-based auth types (`AuthProvider`/`AuthVerifier`) instead of generics
89/// to be compatible with UniFFI, supporting multiple authentication mechanisms (SharedSecret,
90/// JWT, SPIRE, StaticToken). It provides both synchronous (blocking) and asynchronous methods
91/// for flexibility.
92#[derive(uniffi::Object)]
93pub struct App {
94    /// The underlying App instance with enum-based auth types (supports SharedSecret, JWT, SPIRE)
95    app: Arc<SlimApp<AuthProvider, AuthVerifier>>,
96
97    /// Channel receiver for notifications from the app
98    notification_rx: Arc<RwLock<mpsc::Receiver<Result<Notification, SlimSessionError>>>>,
99
100    /// Service instance for lifecycle management (Arc to inner SlimService)
101    service: Arc<SlimService>,
102}
103
104impl App {
105    /// Internal constructor from parts
106    ///
107    /// Used by Service::create_adapter_async to construct a App from its components.
108    pub(crate) fn from_parts(
109        app: Arc<SlimApp<AuthProvider, AuthVerifier>>,
110        notification_rx: Arc<RwLock<mpsc::Receiver<Result<Notification, SlimSessionError>>>>,
111        service: Arc<SlimService>,
112    ) -> Self {
113        Self {
114            app,
115            notification_rx,
116            service,
117        }
118    }
119
120    /// Get a reference to the core SlimApp instance
121    ///
122    /// This allows direct access to the core SLIM API methods without going through
123    /// the bindings layer. Useful for advanced use cases that need core functionality.
124    ///
125    /// # Note
126    /// This is a public internal method that exposes the core app. Use with caution as it
127    /// bypasses the bindings layer abstractions.
128    pub fn core_app(&self) -> &Arc<SlimApp<AuthProvider, AuthVerifier>> {
129        &self.app
130    }
131
132    /// Get a clone of the inner core SlimApp instance
133    ///
134    /// This is used internally by other bindings modules (like slimrpc) that need
135    /// to interact with the core SLIM app.
136    pub fn inner(&self) -> Arc<SlimApp<AuthProvider, AuthVerifier>> {
137        self.app.clone()
138    }
139
140    /// Async constructor - Create a new App with complete creation logic
141    ///
142    /// This is the recommended entry point for language bindings to avoid nested block_on issues.
143    /// Uses the global service instance.
144    pub async fn new_async(
145        base_name: SlimName,
146        identity_provider_config: IdentityProviderConfig,
147        identity_verifier_config: IdentityVerifierConfig,
148    ) -> Result<Self, SlimError> {
149        // Use the global service
150        let service_arc = get_global_service().inner.clone();
151
152        // Delegate to the core creation logic
153        crate::service::create_app_async_internal(
154            base_name,
155            identity_provider_config,
156            identity_verifier_config,
157            service_arc,
158            Direction::Bidirectional,
159        )
160        .await
161    }
162
163    /// Get a reference to the service instance
164    ///
165    /// This provides access to the underlying SLIM service that manages this app.
166    /// Useful for accessing service-level functionality and state.
167    ///
168    /// # Returns
169    /// A reference to the Arc-wrapped SlimService instance
170    pub fn service(&self) -> &Arc<SlimService> {
171        &self.service
172    }
173
174    /// Create a new App with traffic direction (async version)
175    ///
176    /// This is a convenience function for creating a SLIM application with configurable
177    /// traffic direction (send-only, receive-only, bidirectional, or none).
178    /// This is the async version for use in async contexts.
179    ///
180    /// # Arguments
181    /// * `name` - The base name for the app (without ID)
182    /// * `identity_provider_config` - Configuration for proving identity to others
183    /// * `identity_verifier_config` - Configuration for verifying identity of others
184    /// * `direction` - Traffic direction for sessions (Send, Recv, Bidirectional, or None)
185    ///
186    /// # Returns
187    /// * `Ok(Arc<App>)` - Successfully created app
188    /// * `Err(SlimError)` - If app creation fails
189    pub async fn new_with_direction_async(
190        name: Arc<Name>,
191        identity_provider_config: IdentityProviderConfig,
192        identity_verifier_config: IdentityVerifierConfig,
193        direction: Direction,
194    ) -> Result<Arc<App>, SlimError> {
195        // Delegate to the global service's async create_app_with_direction method
196        get_global_service()
197            .create_app_with_direction_async(
198                name,
199                identity_provider_config,
200                identity_verifier_config,
201                direction,
202            )
203            .await
204    }
205
206    /// Create a new App with SharedSecret authentication (async version)
207    ///
208    /// This is a convenience function for creating a SLIM application using SharedSecret authentication.
209    /// This is the async version for use in async contexts.
210    ///
211    /// # Arguments
212    /// * `name` - The base name for the app (without ID)
213    /// * `secret` - The shared secret string for authentication
214    ///
215    /// # Returns
216    /// * `Ok(Arc<App>)` - Successfully created adapter
217    /// * `Err(SlimError)` - If adapter creation fails
218    pub async fn new_with_secret_async(
219        name: Arc<Name>,
220        secret: String,
221    ) -> Result<Arc<App>, SlimError> {
222        // Delegate to the global service's async create_app_with_secret method
223        get_global_service()
224            .create_app_with_secret_async(name, secret)
225            .await
226    }
227}
228
229#[uniffi::export]
230impl App {
231    /// Create a new App with identity provider and verifier configurations
232    ///
233    /// This is the main entry point for creating a SLIM application from language bindings.
234    ///
235    /// # Arguments
236    /// * `base_name` - The base name for the app (without ID)
237    /// * `identity_provider_config` - Configuration for proving identity to others
238    /// * `identity_verifier_config` - Configuration for verifying identity of others
239    ///
240    /// # Returns
241    /// * `Ok(Arc<App>)` - Successfully created adapter
242    /// * `Err(SlimError)` - If adapter creation fails
243    ///
244    /// # Supported Identity Types
245    /// - SharedSecret: Symmetric key authentication
246    /// - JWT: Dynamic JWT generation/verification with signing/decoding keys
247    /// - StaticJWT: Static JWT loaded from file with auto-reload
248    #[uniffi::constructor]
249    pub fn new(
250        base_name: Arc<Name>,
251        identity_provider_config: IdentityProviderConfig,
252        identity_verifier_config: IdentityVerifierConfig,
253    ) -> Result<Arc<Self>, SlimError> {
254        crate::config::get_runtime().block_on(async {
255            Self::new_async(
256                base_name.as_ref().into(),
257                identity_provider_config,
258                identity_verifier_config,
259            )
260            .await
261            .map(Arc::new)
262        })
263    }
264
265    /// Create a new App with traffic direction (blocking version)
266    ///
267    /// This is a convenience function for creating a SLIM application with configurable
268    /// traffic direction (send-only, receive-only, bidirectional, or none).
269    ///
270    /// # Arguments
271    /// * `name` - The base name for the app (without ID)
272    /// * `identity_provider_config` - Configuration for proving identity to others
273    /// * `identity_verifier_config` - Configuration for verifying identity of others
274    /// * `direction` - Traffic direction for sessions (Send, Recv, Bidirectional, or None)
275    ///
276    /// # Returns
277    /// * `Ok(Arc<App>)` - Successfully created app
278    /// * `Err(SlimError)` - If app creation fails
279    #[uniffi::constructor]
280    pub fn new_with_direction(
281        name: Arc<Name>,
282        identity_provider_config: IdentityProviderConfig,
283        identity_verifier_config: IdentityVerifierConfig,
284        direction: Direction,
285    ) -> Result<Arc<App>, SlimError> {
286        // Delegate to the global service's blocking method
287        get_global_service().create_app_with_direction(
288            name,
289            identity_provider_config,
290            identity_verifier_config,
291            direction,
292        )
293    }
294
295    /// Create a new App with SharedSecret authentication (blocking version)
296    ///
297    /// This is a convenience function for creating a SLIM application using SharedSecret authentication.
298    ///
299    /// # Arguments
300    /// * `name` - The base name for the app (without ID)
301    /// * `secret` - The shared secret string for authentication
302    ///
303    /// # Returns
304    /// * `Ok(Arc<App>)` - Successfully created adapter
305    /// * `Err(SlimError)` - If adapter creation fails
306    #[uniffi::constructor]
307    pub fn new_with_secret(name: Arc<Name>, secret: String) -> Result<Arc<App>, SlimError> {
308        // Delegate to the global service's blocking method
309        get_global_service().create_app_with_secret(name, secret)
310    }
311
312    /// Get the app ID (derived from name)
313    pub fn id(&self) -> u64 {
314        self.app.app_name().id()
315    }
316
317    /// Get the app name
318    pub fn name(&self) -> Arc<Name> {
319        Arc::new(self.app.app_name().into())
320    }
321
322    /// Create a new session (blocking version for FFI)
323    ///
324    /// Returns a SessionWithCompletion containing the session context and a completion handle.
325    /// Call `.wait()` on the completion handle to wait for session establishment.
326    pub fn create_session(
327        &self,
328        config: SessionConfig,
329        destination: Arc<Name>,
330    ) -> Result<SessionWithCompletion, SlimError> {
331        crate::config::get_runtime()
332            .block_on(async { self.create_session_async(config, destination).await })
333    }
334
335    /// Create a new session (async version)
336    ///
337    /// Returns a SessionWithCompletion containing the session context and a completion handle.
338    /// Await the completion handle to wait for session establishment.
339    /// For point-to-point sessions, this ensures the remote peer has acknowledged the session.
340    /// For multicast sessions, this ensures the initial setup is complete.
341    pub async fn create_session_async(
342        &self,
343        config: SessionConfig,
344        destination: Arc<Name>,
345    ) -> Result<SessionWithCompletion, SlimError> {
346        let slim_config: SlimSessionConfig = config.into();
347        let slim_dest: SlimName = destination.as_ref().into();
348        let app = self.app.clone();
349        let runtime = get_runtime();
350
351        // Spawn on the runtime's handle to ensure tokio context is available
352        let handle = runtime
353            .handle()
354            .spawn(async move { app.create_session(slim_config, slim_dest, None).await });
355
356        let (session_ctx, completion) = handle.await.map_err(|e| SlimError::SessionError {
357            message: format!("Failed to create session: {}", e),
358        })??;
359
360        // Create Session and CompletionHandle
361        let bindings_ctx = Arc::new(crate::Session::new(session_ctx));
362        let completion_handle = Arc::new(crate::CompletionHandle::from(completion));
363
364        Ok(SessionWithCompletion {
365            session: bindings_ctx,
366            completion: completion_handle,
367        })
368    }
369
370    /// Create a new session and wait for completion (blocking version)
371    ///
372    /// This method creates a session and blocks until the session establishment completes.
373    /// Returns only the session context, as the completion has already been awaited.
374    pub fn create_session_and_wait(
375        &self,
376        config: SessionConfig,
377        destination: Arc<Name>,
378    ) -> Result<Arc<crate::Session>, SlimError> {
379        crate::config::get_runtime().block_on(async {
380            self.create_session_and_wait_async(config, destination)
381                .await
382        })
383    }
384
385    /// Create a new session and wait for completion (async version)
386    ///
387    /// This method creates a session and waits until the session establishment completes.
388    /// Returns only the session context, as the completion has already been awaited.
389    pub async fn create_session_and_wait_async(
390        &self,
391        config: SessionConfig,
392        destination: Arc<Name>,
393    ) -> Result<Arc<crate::Session>, SlimError> {
394        let session_with_completion = self.create_session_async(config, destination).await?;
395        session_with_completion.completion.wait_async().await?;
396        Ok(session_with_completion.session)
397    }
398
399    /// Delete a session (blocking version for FFI)
400    ///
401    /// Returns a completion handle that can be awaited to ensure the deletion completes.
402    pub fn delete_session(
403        &self,
404        session: Arc<crate::Session>,
405    ) -> Result<Arc<crate::CompletionHandle>, SlimError> {
406        crate::config::get_runtime().block_on(async { self.delete_session_async(session).await })
407    }
408
409    /// Delete a session (async version)
410    ///
411    /// Returns a completion handle that can be awaited to ensure the deletion completes.
412    pub async fn delete_session_async(
413        &self,
414        session: Arc<crate::Session>,
415    ) -> Result<Arc<crate::CompletionHandle>, SlimError> {
416        let session_ref = session
417            .session
418            .upgrade()
419            .ok_or_else(|| SlimError::SessionError {
420                message: "Session already closed or dropped".to_string(),
421            })?;
422
423        let completion = self.app.delete_session(&session_ref)?;
424
425        // Return completion handle for caller to wait on
426        Ok(Arc::new(crate::CompletionHandle::from(completion)))
427    }
428
429    /// Delete a session and wait for completion (blocking version)
430    ///
431    /// This method deletes a session and blocks until the deletion completes.
432    pub fn delete_session_and_wait(&self, session: Arc<crate::Session>) -> Result<(), SlimError> {
433        crate::config::get_runtime()
434            .block_on(async { self.delete_session_and_wait_async(session).await })
435    }
436
437    /// Delete a session and wait for completion (async version)
438    ///
439    /// This method deletes a session and waits until the deletion completes.
440    pub async fn delete_session_and_wait_async(
441        &self,
442        session: Arc<crate::Session>,
443    ) -> Result<(), SlimError> {
444        let completion_handle = self.delete_session_async(session).await?;
445        completion_handle.wait_async().await
446    }
447
448    /// Subscribe to a session name (blocking version for FFI)
449    pub fn subscribe(&self, name: Arc<Name>, connection_id: Option<u64>) -> Result<(), SlimError> {
450        crate::config::get_runtime()
451            .block_on(async { self.subscribe_async(name, connection_id).await })
452    }
453
454    /// Subscribe to a name (async version)
455    pub async fn subscribe_async(
456        &self,
457        name: Arc<Name>,
458        connection_id: Option<u64>,
459    ) -> Result<(), SlimError> {
460        let slim_name: SlimName = name.as_ref().into();
461        self.app.subscribe(&slim_name, connection_id).await?;
462        Ok(())
463    }
464
465    /// Unsubscribe from a name (blocking version for FFI)
466    pub fn unsubscribe(
467        &self,
468        name: Arc<Name>,
469        connection_id: Option<u64>,
470    ) -> Result<(), SlimError> {
471        crate::config::get_runtime()
472            .block_on(async { self.unsubscribe_async(name, connection_id).await })
473    }
474
475    /// Unsubscribe from a name (async version)
476    pub async fn unsubscribe_async(
477        &self,
478        name: Arc<Name>,
479        connection_id: Option<u64>,
480    ) -> Result<(), SlimError> {
481        let slim_name: SlimName = name.as_ref().into();
482        self.app.unsubscribe(&slim_name, connection_id).await?;
483        Ok(())
484    }
485
486    /// Set a route to a name for a specific connection (blocking version for FFI)
487    pub fn set_route(&self, name: Arc<Name>, connection_id: u64) -> Result<(), SlimError> {
488        crate::config::get_runtime()
489            .block_on(async { self.set_route_async(name, connection_id).await })
490    }
491
492    /// Set a route to a name for a specific connection (async version)
493    pub async fn set_route_async(
494        &self,
495        name: Arc<Name>,
496        connection_id: u64,
497    ) -> Result<(), SlimError> {
498        let slim_name: SlimName = name.as_ref().into();
499        self.app.set_route(&slim_name, connection_id).await?;
500        Ok(())
501    }
502
503    /// Remove a route (blocking version for FFI)
504    pub fn remove_route(&self, name: Arc<Name>, connection_id: u64) -> Result<(), SlimError> {
505        crate::config::get_runtime()
506            .block_on(async { self.remove_route_async(name, connection_id).await })
507    }
508
509    /// Remove a route (async version)
510    pub async fn remove_route_async(
511        &self,
512        name: Arc<Name>,
513        connection_id: u64,
514    ) -> Result<(), SlimError> {
515        let slim_name: SlimName = name.as_ref().into();
516        self.app.remove_route(&slim_name, connection_id).await?;
517        Ok(())
518    }
519
520    /// Listen for incoming sessions (blocking version for FFI)
521    pub fn listen_for_session(
522        &self,
523        timeout: Option<std::time::Duration>,
524    ) -> Result<Arc<crate::Session>, SlimError> {
525        crate::get_runtime().block_on(async { self.listen_for_session_async(timeout).await })
526    }
527
528    /// Listen for incoming sessions (async version)
529    pub async fn listen_for_session_async(
530        &self,
531        timeout: Option<std::time::Duration>,
532    ) -> Result<Arc<crate::Session>, SlimError> {
533        let mut rx = self.notification_rx.write().await;
534
535        let recv_fut = rx.recv();
536        let notification_opt = if let Some(dur) = timeout {
537            // Runtime-agnostic timeout using futures-timer
538            futures::pin_mut!(recv_fut);
539            let delay = Delay::new(dur);
540            futures::pin_mut!(delay);
541
542            match futures::future::select(recv_fut, delay).await {
543                futures::future::Either::Left((result, _)) => result,
544                futures::future::Either::Right(_) => {
545                    return Err(SlimError::ReceiveError {
546                        message: "listen_for_session timed out".to_string(),
547                    });
548                }
549            }
550        } else {
551            recv_fut.await
552        };
553
554        if notification_opt.is_none() {
555            return Err(SlimError::ReceiveError {
556                message: "application channel closed".to_string(),
557            });
558        }
559
560        match notification_opt.unwrap() {
561            Ok(Notification::NewSession(ctx)) => Ok(Arc::new(crate::Session::new(ctx))),
562            Ok(Notification::NewMessage(_)) => Err(SlimError::ReceiveError {
563                message: "received unexpected message notification while listening for session"
564                    .to_string(),
565            }),
566            Err(e) => Err(SlimError::ReceiveError {
567                message: format!("failed to receive session notification: {}", e),
568            }),
569        }
570    }
571}
572
573// Non-UniFFI methods for internal use (slimrpc)
574impl App {
575    /// Get reference to internal app for advanced use cases (slimrpc)
576    pub fn inner_app(&self) -> &Arc<SlimApp<AuthProvider, AuthVerifier>> {
577        &self.app
578    }
579
580    /// Get notification receiver for server use (slimrpc)
581    pub fn notification_receiver(
582        &self,
583    ) -> Arc<RwLock<mpsc::Receiver<Result<Notification, SlimSessionError>>>> {
584        self.notification_rx.clone()
585    }
586}
587
588// ============================================================================
589// Internal methods for PyO3 bindings (not exported through UniFFI)
590// ============================================================================
591
592impl App {
593    /// Create a new session returning internal Session (for PyO3 bindings)
594    ///
595    /// This method is for Python bindings to bypass the FFI layer and get
596    /// a proper Session with its CompletionHandle.
597    pub async fn create_session_internal(
598        &self,
599        config: SlimSessionConfig,
600        destination: SlimName,
601    ) -> Result<
602        (
603            slim_session::context::SessionContext,
604            slim_session::CompletionHandle,
605        ),
606        SlimError,
607    > {
608        let (session_ctx, completion) = self.app.create_session(config, destination, None).await?;
609
610        Ok((session_ctx, completion))
611    }
612
613    /// Listen for incoming sessions returning internal Session (for PyO3 bindings)
614    pub async fn listen_for_session_internal(
615        &self,
616        timeout: Option<std::time::Duration>,
617    ) -> Result<slim_session::context::SessionContext, SlimError> {
618        let mut rx = self.notification_rx.write().await;
619
620        let recv_fut = rx.recv();
621        let notification_opt = if let Some(dur) = timeout {
622            // Runtime-agnostic timeout using futures-timer
623            futures::pin_mut!(recv_fut);
624            let delay = Delay::new(dur);
625            futures::pin_mut!(delay);
626
627            match futures::future::select(recv_fut, delay).await {
628                futures::future::Either::Left((result, _)) => result,
629                futures::future::Either::Right(_) => {
630                    return Err(SlimError::ReceiveError {
631                        message: "listen_for_session timed out".to_string(),
632                    });
633                }
634            }
635        } else {
636            recv_fut.await
637        };
638
639        if notification_opt.is_none() {
640            return Err(SlimError::ReceiveError {
641                message: "application channel closed".to_string(),
642            });
643        }
644
645        match notification_opt.unwrap() {
646            Ok(Notification::NewSession(ctx)) => Ok(ctx),
647            Ok(Notification::NewMessage(_)) => Err(SlimError::ReceiveError {
648                message: "received unexpected message notification while listening for session"
649                    .to_string(),
650            }),
651            Err(e) => Err(SlimError::ReceiveError {
652                message: format!("failed to receive session notification: {}", e),
653            }),
654        }
655    }
656
657    /// Delete a session returning CompletionHandle (for PyO3 bindings)
658    ///
659    /// This method is for Python bindings to get the CompletionHandle when deleting a session.
660    pub fn delete_session_internal(
661        &self,
662        session: &SessionController,
663    ) -> Result<slim_session::CompletionHandle, SlimError> {
664        let ret = self.app.delete_session(session)?;
665
666        Ok(ret)
667    }
668}
669
670// ============================================================================
671// Tests
672// ============================================================================
673
674#[cfg(test)]
675mod tests {
676    use crate::SessionType;
677
678    use super::*;
679
680    use slim_config::component::ComponentBuilder;
681    use slim_datapath::messages::Name as SlimName;
682    use slim_testing::utils::TEST_VALID_SECRET;
683
684    // Helper to create test identity configs
685    fn create_test_configs(secret: &str) -> (IdentityProviderConfig, IdentityVerifierConfig) {
686        (
687            IdentityProviderConfig::SharedSecret {
688                id: "test-service".to_string(),
689                data: secret.to_string(),
690            },
691            IdentityVerifierConfig::SharedSecret {
692                id: "test-service".to_string(),
693                data: secret.to_string(),
694            },
695        )
696    }
697
698    /// Test basic adapter creation
699    #[tokio::test]
700    async fn test_adapter_creation() {
701        let base_name = SlimName::from_strings(["org", "namespace", "test-app"]);
702        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
703
704        let result = App::new_async(base_name, provider_config, verifier_config).await;
705        assert!(result.is_ok());
706
707        let adapter = result.unwrap();
708        assert!(adapter.id() > 0);
709    }
710
711    /// Test that adapter ID is consistently derived from its internal provider's token ID
712    #[tokio::test]
713    async fn test_deterministic_id_generation() {
714        let base_name = SlimName::from_strings(["org", "namespace", "test-app"]);
715        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
716
717        // Create the adapter
718        let adapter = App::new_async(base_name, provider_config, verifier_config)
719            .await
720            .expect("Failed to create adapter");
721
722        // The adapter's ID should be non-zero (derived from token ID hash)
723        let adapter_id = adapter.id();
724        assert!(adapter_id > 0, "Adapter ID should be non-zero");
725
726        // Verify the adapter's name includes the ID
727        let adapter_name = adapter.name();
728        assert_eq!(
729            adapter_name.id(),
730            adapter_id,
731            "Name ID should match adapter ID"
732        );
733    }
734
735    /// Test that session creation auto-waits for establishment
736    #[tokio::test]
737    async fn test_session_creation_auto_wait() {
738        // This test verifies that create_session_async properly awaits the completion handle
739        // In a real scenario, this would ensure the session is fully established
740
741        let base_name = SlimName::from_strings(["org", "namespace", "create-test"]);
742        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
743
744        let adapter = App::new_async(base_name, provider_config, verifier_config)
745            .await
746            .expect("Failed to create adapter");
747
748        let session_config = SessionConfig {
749            session_type: SessionType::PointToPoint,
750            enable_mls: false,
751            max_retries: Some(3),
752            interval: Some(std::time::Duration::from_millis(100)),
753            metadata: std::collections::HashMap::new(),
754        };
755
756        let destination = Arc::new(Name::new(
757            "org".to_string(),
758            "test".to_string(),
759            "dest".to_string(),
760        ));
761
762        // This should auto-wait for session establishment
763        // If it returns without error, the session is fully established
764        let result = adapter
765            .create_session_async(session_config, destination)
766            .await;
767
768        // In a real scenario with network, this would verify the session is ready
769        // For this test, we just verify it completes without panicking
770        match result {
771            Ok(_session) => {
772                // Session created and auto-waited successfully
773            }
774            Err(e) => {
775                // Expected to fail in test environment without network
776                // but shouldn't panic
777                println!("Expected error in test environment: {:?}", e);
778            }
779        }
780    }
781
782    /// Test publish_with_completion returns a valid completion handle
783    #[tokio::test]
784    async fn test_publish_with_completion_returns_handle() {
785        // Note: This is a structural test - in a real environment with connections,
786        // the completion handle would actually track message delivery
787
788        let base_name = SlimName::from_strings(["org", "namespace", "publish-test"]);
789        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
790
791        let adapter = App::new_async(base_name, provider_config, verifier_config)
792            .await
793            .expect("Failed to create adapter");
794
795        let session_config = SessionConfig {
796            session_type: SessionType::PointToPoint,
797            enable_mls: false,
798            max_retries: Some(3),
799            interval: Some(std::time::Duration::from_millis(100)),
800            metadata: std::collections::HashMap::new(),
801        };
802
803        let destination = Arc::new(Name::new(
804            "org".to_string(),
805            "test".to_string(),
806            "dest".to_string(),
807        ));
808
809        // Try to create a session (may fail without network)
810        if let Ok(session) = adapter
811            .create_session_async(session_config, destination)
812            .await
813        {
814            let data = b"test message".to_vec();
815
816            // Attempt to publish (always returns completion handle)
817            // This verifies the API exists and returns the right type
818            let result = session.session.publish_async(data, None, None).await;
819
820            match result {
821                Ok(completion_handle) => {
822                    // Verify we got a completion handle
823                    // In a real scenario, we could wait on it
824                    assert!(Arc::strong_count(&completion_handle) > 0);
825                }
826                Err(e) => {
827                    // Expected to fail without actual connections
828                    println!("Expected error without network: {:?}", e);
829                }
830            }
831        }
832    }
833
834    // ========================================================================
835    // TlsConfig Tests
836    // ========================================================================
837
838    /// Test TlsConfig with all fields
839    #[test]
840    fn test_tls_config_full() {
841        use crate::common_config::TlsClientConfig;
842
843        let config = TlsClientConfig {
844            insecure: false,
845            insecure_skip_verify: false,
846            source: crate::common_config::TlsSource::File {
847                cert: "test-cert.pem".to_string(),
848                key: "test-key.pem".to_string(),
849            },
850            ca_source: crate::common_config::CaSource::File {
851                path: "test-ca.pem".to_string(),
852            },
853            include_system_ca_certs_pool: true,
854            tls_version: "tls1.3".to_string(),
855        };
856
857        assert!(!config.insecure);
858        assert!(!config.insecure_skip_verify);
859        assert!(matches!(
860            config.source,
861            crate::common_config::TlsSource::File { .. }
862        ));
863        assert!(matches!(
864            config.ca_source,
865            crate::common_config::CaSource::File { .. }
866        ));
867        assert_eq!(config.tls_version, "tls1.3");
868        assert!(config.include_system_ca_certs_pool);
869    }
870
871    /// Test TlsConfig with insecure mode
872    #[test]
873    fn test_tls_config_insecure() {
874        use crate::common_config::TlsClientConfig;
875
876        let config = TlsClientConfig {
877            insecure: true,
878            insecure_skip_verify: false,
879            source: crate::common_config::TlsSource::None,
880            ca_source: crate::common_config::CaSource::None,
881            include_system_ca_certs_pool: true,
882            tls_version: "tls1.3".to_string(),
883        };
884
885        assert!(config.insecure);
886        assert!(matches!(
887            config.source,
888            crate::common_config::TlsSource::None
889        ));
890    }
891
892    // ========================================================================
893    // Adapter Methods Tests
894    // ========================================================================
895
896    /// Test adapter id() and name() methods
897    #[tokio::test]
898    async fn test_adapter_id_and_name() {
899        let base_name = SlimName::from_strings(["org", "namespace", "id-test"]);
900        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
901
902        let adapter = App::new_async(base_name.clone(), provider_config, verifier_config)
903            .await
904            .expect("Failed to create adapter");
905
906        // ID should be non-zero
907        let id = adapter.id();
908        assert!(id > 0, "Adapter ID should be positive");
909
910        // Name should have the right components
911        let name = adapter.name();
912        assert_eq!(name.components()[0], "org");
913        assert_eq!(name.components()[1], "namespace");
914        assert_eq!(name.components()[2], "id-test");
915        assert!(name.id() > 0);
916    }
917
918    /// Test adapter with local service
919    /// Test adapter with global service
920    #[tokio::test]
921    async fn test_adapter_with_global_service() {
922        let base_name = SlimName::from_strings(["org", "namespace", "global-test"]);
923        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
924
925        let result = App::new_async(base_name, provider_config, verifier_config).await;
926        assert!(result.is_ok(), "Should create adapter with global service");
927    }
928
929    /// Test adapter creation with different namespace values
930    #[tokio::test]
931    async fn test_adapter_different_namespaces() {
932        // Test with different valid namespace configurations
933        let namespaces = [
934            ["org1", "namespace1", "app1"],
935            ["company", "team", "service"],
936            ["prod", "api", "gateway"],
937        ];
938
939        for ns in namespaces {
940            let base_name = SlimName::from_strings(ns);
941            let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
942
943            let result = App::new_async(base_name, provider_config, verifier_config).await;
944            assert!(
945                result.is_ok(),
946                "Should create adapter for namespace {:?}",
947                ns
948            );
949        }
950    }
951
952    // ========================================================================
953    // initialize_crypto_provider Tests
954    // ========================================================================
955    // App::new Tests
956    // ========================================================================
957
958    /// Test App::new_async entry point
959    #[tokio::test]
960    async fn test_bindings_adapter_new() {
961        let base_name = SlimName::from_strings(["org", "namespace", "ffi-app"]);
962
963        let provider_config = IdentityProviderConfig::SharedSecret {
964            id: "test-sync-service".to_string(),
965            data: TEST_VALID_SECRET.to_string(),
966        };
967        let verifier_config = IdentityVerifierConfig::SharedSecret {
968            id: "test-sync-service".to_string(),
969            data: TEST_VALID_SECRET.to_string(),
970        };
971
972        let result = App::new_async(base_name, provider_config, verifier_config).await;
973        assert!(result.is_ok(), "App::new_async should succeed");
974
975        let adapter = result.unwrap();
976        assert!(adapter.id() > 0);
977
978        let name = adapter.name();
979        assert_eq!(name.components()[0], "org");
980        assert_eq!(name.components()[1], "namespace");
981        assert_eq!(name.components()[2], "ffi-app");
982    }
983
984    /// Test App::new_async with minimal name (3 components)
985    #[tokio::test]
986    async fn test_bindings_adapter_new_minimal_name() {
987        let base_name = SlimName::from_strings(["org", "ns", "test-app"]);
988
989        let provider_config = IdentityProviderConfig::SharedSecret {
990            id: "test-minimal-service".to_string(),
991            data: TEST_VALID_SECRET.to_string(),
992        };
993        let verifier_config = IdentityVerifierConfig::SharedSecret {
994            id: "test-minimal-service".to_string(),
995            data: TEST_VALID_SECRET.to_string(),
996        };
997
998        let result = App::new_async(base_name, provider_config, verifier_config).await;
999        // Should handle minimal name
1000        assert!(result.is_ok(), "Should create adapter with minimal name");
1001    }
1002
1003    // ========================================================================
1004    // Listen for Session Timeout Tests
1005    // ========================================================================
1006
1007    /// Test listen_for_session with timeout
1008    #[tokio::test]
1009    async fn test_listen_for_session_timeout() {
1010        let base_name = SlimName::from_strings(["org", "namespace", "listen-test"]);
1011        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1012
1013        let adapter = App::new_async(base_name, provider_config, verifier_config)
1014            .await
1015            .expect("Failed to create adapter");
1016
1017        // Listen with a very short timeout - should timeout
1018        let result = adapter
1019            .listen_for_session_async(Some(std::time::Duration::from_millis(10)))
1020            .await;
1021
1022        match result {
1023            Err(SlimError::ReceiveError { message }) => {
1024                assert!(
1025                    message.contains("timed out"),
1026                    "Should contain timeout message"
1027                );
1028            }
1029            _ => {
1030                // May get a different error in some cases, which is fine
1031            }
1032        }
1033    }
1034
1035    // ========================================================================
1036    // Subscribe/Unsubscribe Tests
1037    // ========================================================================
1038
1039    /// Test subscribe and unsubscribe (requires running service)
1040    #[tokio::test]
1041    async fn test_subscribe_unsubscribe() {
1042        let base_name = SlimName::from_strings(["org", "namespace", "sub-test"]);
1043        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1044
1045        let adapter = App::new_async(base_name, provider_config, verifier_config)
1046            .await
1047            .expect("Failed to create adapter");
1048
1049        let target_name = Arc::new(Name::new(
1050            "org".to_string(),
1051            "ns".to_string(),
1052            "target".to_string(),
1053        ));
1054
1055        // Subscribe (may fail without connection, but shouldn't panic)
1056        let sub_result = adapter.subscribe_async(target_name.clone(), None).await;
1057        // We don't assert success because there's no active connection
1058
1059        // Unsubscribe
1060        let unsub_result = adapter.unsubscribe_async(target_name, None).await;
1061        // Same - may fail but shouldn't panic
1062
1063        let _ = (sub_result, unsub_result);
1064    }
1065
1066    // ========================================================================
1067    // Set/Remove Route Tests
1068    // ========================================================================
1069
1070    /// Test set_route and remove_route
1071    #[tokio::test]
1072    async fn test_set_remove_route() {
1073        let base_name = SlimName::from_strings(["org", "namespace", "route-test"]);
1074        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1075
1076        let adapter = App::new_async(base_name, provider_config, verifier_config)
1077            .await
1078            .expect("Failed to create adapter");
1079
1080        let target_name = Arc::new(Name::new(
1081            "org".to_string(),
1082            "ns".to_string(),
1083            "route-target".to_string(),
1084        ));
1085
1086        // Set route (may fail without valid connection_id)
1087        let set_result = adapter.set_route_async(target_name.clone(), 12345).await;
1088        // Remove route
1089        let remove_result = adapter.remove_route_async(target_name, 12345).await;
1090
1091        // These will likely fail without actual connections, but shouldn't panic
1092        let _ = (set_result, remove_result);
1093    }
1094
1095    // ========================================================================
1096    // Stop Server Tests
1097    // ========================================================================
1098
1099    /// Test stop_server on non-existent server
1100    #[tokio::test]
1101    async fn test_stop_server_nonexistent() {
1102        let base_name = SlimName::from_strings(["org", "namespace", "stop-test"]);
1103        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1104
1105        let _adapter = App::new_async(base_name, provider_config, verifier_config)
1106            .await
1107            .expect("Failed to create adapter");
1108
1109        // Try to stop a server that doesn't exist using the global service
1110        let service = get_global_service();
1111        let result = service.stop_server("127.0.0.1:99999".to_string());
1112        // Should fail with appropriate error
1113        assert!(result.is_err());
1114    }
1115
1116    // ========================================================================
1117    // Disconnect Tests
1118    // ========================================================================
1119
1120    /// Test disconnect with invalid connection_id
1121    #[tokio::test]
1122    async fn test_disconnect_invalid_id() {
1123        let base_name = SlimName::from_strings(["org", "namespace", "disconnect-test"]);
1124        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1125
1126        let _adapter = App::new_async(base_name, provider_config, verifier_config)
1127            .await
1128            .expect("Failed to create adapter");
1129
1130        // Try to disconnect with an invalid connection ID using the global service
1131        let service = get_global_service();
1132        let result = service.disconnect(999999);
1133        // Should fail but not panic
1134        assert!(result.is_err());
1135    }
1136
1137    // ========================================================================
1138    // Delete Session Tests
1139    // ========================================================================
1140
1141    /// Test delete_session with a session (structural test)
1142    #[tokio::test]
1143    async fn test_delete_session_flow() {
1144        let base_name = SlimName::from_strings(["org", "namespace", "delete-test"]);
1145        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1146
1147        let adapter = App::new_async(base_name, provider_config, verifier_config)
1148            .await
1149            .expect("Failed to create adapter");
1150
1151        let session_config = SessionConfig {
1152            session_type: SessionType::PointToPoint,
1153            enable_mls: false,
1154            max_retries: Some(1),
1155            interval: Some(std::time::Duration::from_millis(50)),
1156            metadata: std::collections::HashMap::new(),
1157        };
1158
1159        let destination = Arc::new(Name::new(
1160            "org".to_string(),
1161            "test".to_string(),
1162            "delete-dest".to_string(),
1163        ));
1164
1165        // Create session (may fail without network)
1166        if let Ok(session_with_completion) = adapter
1167            .create_session_async(session_config, destination)
1168            .await
1169        {
1170            // Delete session
1171            let delete_result = adapter
1172                .delete_session_async(session_with_completion.session)
1173                .await;
1174            // May succeed or fail depending on session state
1175            if let Ok(completion) = delete_result {
1176                let _ = completion;
1177            }
1178        }
1179    }
1180
1181    // ========================================================================
1182    // Blocking Method Tests
1183    // ========================================================================
1184
1185    /// Test blocking version of listen_for_session
1186    #[tokio::test]
1187    async fn test_listen_for_session_blocking_timeout() {
1188        let base_name = SlimName::from_strings(["org", "namespace", "blocking-listen"]);
1189        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1190
1191        let adapter = App::new_async(base_name, provider_config, verifier_config)
1192            .await
1193            .expect("Failed to create adapter");
1194
1195        // Call async version with short timeout (blocking version can't be called from async context)
1196        let result = adapter
1197            .listen_for_session_async(Some(std::time::Duration::from_millis(10)))
1198            .await;
1199
1200        // Should timeout
1201        match result {
1202            Err(SlimError::ReceiveError { message }) => {
1203                assert!(message.contains("timed out") || message.contains("closed"));
1204            }
1205            _ => {
1206                // Other errors are acceptable too
1207            }
1208        }
1209    }
1210
1211    /// Test blocking version of subscribe
1212    #[tokio::test]
1213    async fn test_subscribe_blocking() {
1214        let base_name = SlimName::from_strings(["org", "namespace", "blocking-sub"]);
1215        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1216
1217        let adapter = App::new_async(base_name, provider_config, verifier_config)
1218            .await
1219            .expect("Failed to create adapter");
1220
1221        let target_name = Arc::new(Name::new(
1222            "org".to_string(),
1223            "ns".to_string(),
1224            "block-target".to_string(),
1225        ));
1226
1227        // Call async version (blocking version can't be called from async context)
1228        let _ = adapter.subscribe_async(target_name, None).await;
1229    }
1230
1231    /// Test from_parts internal constructor
1232    #[tokio::test]
1233    async fn test_from_parts_constructor() {
1234        let base_name = SlimName::from_strings(["org", "namespace", "from-parts-test"]);
1235        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1236
1237        // First create an adapter normally to get its parts
1238        let _adapter1 = App::new_async(
1239            base_name.clone(),
1240            provider_config.clone(),
1241            verifier_config.clone(),
1242        )
1243        .await
1244        .expect("Failed to create adapter");
1245
1246        // Now create another adapter using the Service's create_adapter_async method
1247        // which uses from_parts internally
1248        let service = get_global_service();
1249        let name = Arc::new(Name::new(
1250            "org".to_string(),
1251            "namespace".to_string(),
1252            "from-parts-test-2".to_string(),
1253        ));
1254
1255        let adapter2 = service
1256            .create_app_async(name, provider_config, verifier_config)
1257            .await
1258            .expect("Failed to create adapter via service");
1259
1260        // Verify the adapter created via from_parts works correctly
1261        assert!(adapter2.id() > 0);
1262        assert!(!adapter2.name().to_string().is_empty());
1263    }
1264
1265    /// Test creating app with a custom service using Service::create_app_async
1266    #[tokio::test]
1267    async fn test_new_async_with_custom_service() {
1268        use slim_service::Service as SlimService;
1269
1270        let base_name = SlimName::from_strings(["org", "namespace", "custom-service-test"]);
1271        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1272
1273        // Create a custom service instance
1274        let custom_service = SlimService::builder()
1275            .build("test-custom-service".to_string())
1276            .expect("Failed to create custom service");
1277        let service_wrapper = crate::Service {
1278            inner: Arc::new(custom_service),
1279        };
1280
1281        // Create adapter with custom service using Service API
1282        let base_name_arc = Arc::new(Name::from(base_name));
1283        let result = service_wrapper
1284            .create_app_async(base_name_arc, provider_config, verifier_config)
1285            .await;
1286
1287        assert!(result.is_ok(), "Should create adapter with custom service");
1288        let adapter = result.unwrap();
1289        assert!(adapter.id() > 0);
1290        assert!(!adapter.name().to_string().is_empty());
1291    }
1292
1293    /// Test that new_async uses global service by default
1294    #[tokio::test]
1295    async fn test_new_async_uses_global_service() {
1296        let base_name = SlimName::from_strings(["org", "namespace", "new-async-global"]);
1297        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1298
1299        // Create two adapters using new_async
1300        let adapter1 = App::new_async(
1301            base_name.clone(),
1302            provider_config.clone(),
1303            verifier_config.clone(),
1304        )
1305        .await
1306        .expect("Failed to create first adapter");
1307
1308        let base_name2 = SlimName::from_strings(["org", "namespace", "new-async-global-2"]);
1309        let adapter2 = App::new_async(base_name2, provider_config, verifier_config)
1310            .await
1311            .expect("Failed to create second adapter");
1312
1313        // Both should be created successfully (using the same global service)
1314        assert!(adapter1.id() > 0);
1315        assert!(adapter2.id() > 0);
1316        assert_ne!(
1317            adapter1.id(),
1318            adapter2.id(),
1319            "Different adapters should have different IDs"
1320        );
1321    }
1322
1323    /// Test multiple adapters with different custom services
1324    /// Test that different service instances create isolated adapters
1325    #[tokio::test]
1326    async fn test_different_services_create_isolated_adapters() {
1327        use slim_service::Service as SlimService;
1328
1329        let base_name1 = SlimName::from_strings(["org", "namespace", "isolated-1"]);
1330        let base_name2 = SlimName::from_strings(["org", "namespace", "isolated-2"]);
1331        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1332
1333        // Create two different custom services
1334        let service1 = SlimService::builder()
1335            .build("test-service-1".to_string())
1336            .expect("Failed to create service 1");
1337        let service1_wrapper = crate::Service {
1338            inner: Arc::new(service1),
1339        };
1340
1341        let service2 = SlimService::builder()
1342            .build("test-service-2".to_string())
1343            .expect("Failed to create service 2");
1344        let service2_wrapper = crate::Service {
1345            inner: Arc::new(service2),
1346        };
1347
1348        // Create adapters with different services using Service API
1349        let base_name1_arc = Arc::new(Name::from(base_name1));
1350        let adapter1 = service1_wrapper
1351            .create_app_async(
1352                base_name1_arc,
1353                provider_config.clone(),
1354                verifier_config.clone(),
1355            )
1356            .await
1357            .expect("Failed to create adapter 1");
1358
1359        let base_name2_arc = Arc::new(Name::from(base_name2));
1360        let adapter2 = service2_wrapper
1361            .create_app_async(base_name2_arc, provider_config, verifier_config)
1362            .await
1363            .expect("Failed to create adapter 2");
1364
1365        // Adapters should have different IDs since they're on different services
1366        assert_ne!(adapter1.id(), adapter2.id());
1367    }
1368
1369    /// Test new_with_secret_async creates app successfully
1370    #[tokio::test]
1371    async fn test_new_with_secret_async() {
1372        let name = Arc::new(Name::new(
1373            "org".to_string(),
1374            "namespace".to_string(),
1375            "secret-test".to_string(),
1376        ));
1377        let secret = TEST_VALID_SECRET.to_string();
1378
1379        let result = App::new_with_secret_async(name, secret).await;
1380
1381        assert!(result.is_ok(), "Should create app with secret");
1382        let app = result.unwrap();
1383        assert!(app.id() > 0, "App should have non-zero ID");
1384        assert!(
1385            !app.name().to_string().is_empty(),
1386            "App should have valid name"
1387        );
1388    }
1389
1390    /// Test new_with_secret blocking version
1391    #[test]
1392    fn test_new_with_secret_blocking() {
1393        let name = Arc::new(Name::new(
1394            "org".to_string(),
1395            "namespace".to_string(),
1396            "secret-blocking-test".to_string(),
1397        ));
1398        let secret = TEST_VALID_SECRET.to_string();
1399
1400        // Call blocking version from sync context
1401        let result = App::new_with_secret(name, secret);
1402
1403        assert!(
1404            result.is_ok(),
1405            "Should create app with secret in blocking mode"
1406        );
1407        let app = result.unwrap();
1408        assert!(app.id() > 0, "App should have non-zero ID");
1409    }
1410
1411    /// Test new_with_secret creates unique IDs for different apps
1412    #[tokio::test]
1413    async fn test_new_with_secret_unique_ids() {
1414        let secret = TEST_VALID_SECRET.to_string();
1415
1416        let name1 = Arc::new(Name::new(
1417            "org".to_string(),
1418            "namespace".to_string(),
1419            "unique-1".to_string(),
1420        ));
1421        let name2 = Arc::new(Name::new(
1422            "org".to_string(),
1423            "namespace".to_string(),
1424            "unique-2".to_string(),
1425        ));
1426
1427        let app1 = App::new_with_secret_async(name1, secret.clone())
1428            .await
1429            .expect("Failed to create app1");
1430
1431        let app2 = App::new_with_secret_async(name2, secret)
1432            .await
1433            .expect("Failed to create app2");
1434
1435        assert_ne!(app1.id(), app2.id(), "Apps should have different IDs");
1436    }
1437
1438    /// Test create_session_async runtime spawning
1439    #[tokio::test]
1440    async fn test_create_session_async_runtime_spawning() {
1441        let base_name = SlimName::from_strings(["org", "namespace", "runtime-spawn-test"]);
1442        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1443
1444        let app = App::new_async(base_name, provider_config, verifier_config)
1445            .await
1446            .expect("Failed to create app");
1447
1448        let session_config = SessionConfig {
1449            session_type: SessionType::PointToPoint,
1450            enable_mls: false,
1451            max_retries: Some(3),
1452            interval: Some(std::time::Duration::from_millis(100)),
1453            metadata: std::collections::HashMap::new(),
1454        };
1455
1456        let destination = Arc::new(Name::new(
1457            "org".to_string(),
1458            "test".to_string(),
1459            "spawn-dest".to_string(),
1460        ));
1461
1462        // This should not panic even though it spawns on runtime
1463        let result = app.create_session_async(session_config, destination).await;
1464
1465        // May fail without network, but shouldn't panic from join error
1466        let _ = result;
1467    }
1468
1469    /// Test listen_for_session with timeout using futures-timer
1470    #[tokio::test]
1471    async fn test_listen_for_session_timeout_futures_timer() {
1472        let base_name = SlimName::from_strings(["org", "namespace", "listen-timeout-test"]);
1473        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1474
1475        let app = App::new_async(base_name, provider_config, verifier_config)
1476            .await
1477            .expect("Failed to create app");
1478
1479        // Listen with a short timeout - should timeout since no session is incoming
1480        let result = app
1481            .listen_for_session_async(Some(std::time::Duration::from_millis(50)))
1482            .await;
1483
1484        assert!(result.is_err(), "Should timeout when no session arrives");
1485        if let Err(e) = result {
1486            let error_msg = format!("{:?}", e);
1487            assert!(
1488                error_msg.contains("timed out") || error_msg.contains("timeout"),
1489                "Error should mention timeout"
1490            );
1491        }
1492    }
1493
1494    /// Test listen_for_session with timeout using futures-timer (second test)
1495    #[tokio::test]
1496    async fn test_listen_for_session_no_incoming() {
1497        let base_name = SlimName::from_strings(["org", "namespace", "no-incoming-test"]);
1498        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1499
1500        let app = App::new_async(base_name, provider_config, verifier_config)
1501            .await
1502            .expect("Failed to create app");
1503
1504        // Listen with a short timeout when no session is incoming - should timeout
1505        let result = app
1506            .listen_for_session_async(Some(std::time::Duration::from_millis(30)))
1507            .await;
1508
1509        assert!(result.is_err(), "Should timeout when no session arrives");
1510        if let Err(e) = result {
1511            let error_msg = format!("{:?}", e);
1512            assert!(
1513                error_msg.contains("timed out") || error_msg.contains("timeout"),
1514                "Error should mention timeout"
1515            );
1516        }
1517    }
1518
1519    /// Test that new_async delegates to service's create_app_async_internal
1520    #[tokio::test]
1521    async fn test_new_async_uses_internal_creation() {
1522        let base_name = SlimName::from_strings(["org", "namespace", "internal-test"]);
1523        let (provider_config, verifier_config) = create_test_configs(TEST_VALID_SECRET);
1524
1525        // This tests that new_async properly delegates to the internal creation function
1526        let result = App::new_async(base_name, provider_config, verifier_config).await;
1527
1528        assert!(result.is_ok(), "Should create app via internal function");
1529        let app = result.unwrap();
1530        assert!(app.id() > 0);
1531        assert!(!app.name().to_string().is_empty());
1532    }
1533}