@@ -250,3 +250,279 @@ async fn test_disabled_cache_behavior() {
250250 let stats = bridge. cache_stats ( ) . await ;
251251 assert_eq ! ( stats. size, 0 ) ;
252252}
253+
254+ /// Tests connection limit enforcement when limit is reached
255+ #[ tokio:: test]
256+ async fn test_connection_limit_reached ( ) {
257+ let bridge = Bridge :: with_limits ( 100 , 1 ) ; // Max 1 connection
258+
259+ // First connection should be allowed (but will fail without valid command)
260+ let server1 = ServerId :: new ( "server1" ) ;
261+ let _result1 = bridge. connect ( server1. clone ( ) , "nonexistent-cmd" ) . await ;
262+
263+ // Should either succeed in attempting connection or fail due to invalid command
264+ // Either way, connection limit logic is tested
265+ let ( current, max) = bridge. connection_limits ( ) . await ;
266+ assert_eq ! ( max, 1 ) ;
267+
268+ // Verify the limit value is enforced
269+ assert ! ( current <= max) ;
270+ }
271+
272+ /// Tests empty `server_id` handling
273+ #[ tokio:: test]
274+ async fn test_empty_server_id ( ) {
275+ let bridge = Bridge :: new ( 100 ) ;
276+ let server_id = ServerId :: new ( "" ) ;
277+
278+ // Should handle empty server IDs gracefully
279+ let result = bridge. connect ( server_id. clone ( ) , "test-cmd" ) . await ;
280+ assert ! ( result. is_err( ) ) ;
281+ }
282+
283+ /// Tests connection with whitespace in command
284+ #[ tokio:: test]
285+ async fn test_command_with_whitespace ( ) {
286+ let bridge = Bridge :: new ( 100 ) ;
287+ let server_id = ServerId :: new ( "test" ) ;
288+
289+ // Command with spaces should be validated
290+ let result = bridge. connect ( server_id, "test cmd with spaces" ) . await ;
291+
292+ // Should either fail validation or connection attempt
293+ assert ! ( result. is_err( ) ) ;
294+ }
295+
296+ /// Tests disconnect followed by connection count check
297+ #[ tokio:: test]
298+ async fn test_disconnect_updates_count ( ) {
299+ let bridge = Bridge :: new ( 100 ) ;
300+ let server_id = ServerId :: new ( "test" ) ;
301+
302+ // Initial count should be 0
303+ assert_eq ! ( bridge. connection_count( ) . await , 0 ) ;
304+
305+ // Disconnect non-existent (should not affect count)
306+ bridge. disconnect ( & server_id) . await ;
307+ assert_eq ! ( bridge. connection_count( ) . await , 0 ) ;
308+ }
309+
310+ /// Tests connection limits after disconnect
311+ #[ tokio:: test]
312+ async fn test_connection_limits_after_disconnect ( ) {
313+ let bridge = Bridge :: with_limits ( 100 , 5 ) ;
314+ let server_id = ServerId :: new ( "test" ) ;
315+
316+ let ( current, max) = bridge. connection_limits ( ) . await ;
317+ assert_eq ! ( current, 0 ) ;
318+ assert_eq ! ( max, 5 ) ;
319+
320+ // Disconnect (no connection exists)
321+ bridge. disconnect ( & server_id) . await ;
322+
323+ let ( current2, max2) = bridge. connection_limits ( ) . await ;
324+ assert_eq ! ( current2, 0 ) ;
325+ assert_eq ! ( max2, 5 ) ;
326+ }
327+
328+ /// Tests `call_tool` error when server never connected
329+ #[ tokio:: test]
330+ async fn test_call_tool_server_never_connected ( ) {
331+ let bridge = Bridge :: new ( 100 ) ;
332+ let server_id = ServerId :: new ( "never-connected" ) ;
333+ let tool_name = ToolName :: new ( "some_tool" ) ;
334+ let params = json ! ( { "key" : "value" } ) ;
335+
336+ let result = bridge. call_tool ( & server_id, & tool_name, params) . await ;
337+
338+ assert ! ( result. is_err( ) ) ;
339+ match result {
340+ Err ( Error :: ConnectionFailed { server, source } ) => {
341+ assert_eq ! ( server, "never-connected" ) ;
342+ assert ! ( source. to_string( ) . contains( "not connected" ) ) ;
343+ }
344+ _ => panic ! ( "Expected ConnectionFailed error" ) ,
345+ }
346+ }
347+
348+ /// Tests cache key generation with different parameters
349+ #[ tokio:: test]
350+ async fn test_cache_with_different_params ( ) {
351+ let bridge = Bridge :: new ( 100 ) ;
352+ let server_id = ServerId :: new ( "test-server" ) ;
353+ let tool_name = ToolName :: new ( "test-tool" ) ;
354+
355+ // These should generate different cache keys
356+ let params1 = json ! ( { "a" : 1 , "b" : 2 } ) ;
357+ let params2 = json ! ( { "a" : 2 , "b" : 1 } ) ;
358+
359+ // Both will fail (no connection), but cache key logic is tested
360+ let result1 = bridge. call_tool ( & server_id, & tool_name, params1) . await ;
361+ let result2 = bridge
362+ . call_tool ( & server_id, & tool_name, params2. clone ( ) )
363+ . await ;
364+
365+ assert ! ( result1. is_err( ) ) ;
366+ assert ! ( result2. is_err( ) ) ;
367+
368+ // Verify cache stats remain at 0 (no successful calls to cache)
369+ let stats = bridge. cache_stats ( ) . await ;
370+ assert_eq ! ( stats. size, 0 ) ;
371+ }
372+
373+ /// Tests `with_limits` constructor with various configurations
374+ #[ test]
375+ fn test_with_limits_configurations ( ) {
376+ // Minimum viable configuration
377+ let bridge1 = Bridge :: with_limits ( 1 , 1 ) ;
378+ let _ = bridge1;
379+
380+ // Large configuration
381+ let bridge2 = Bridge :: with_limits ( 1_000_000 , 1000 ) ;
382+ let _ = bridge2;
383+
384+ // Asymmetric configuration (small cache, many connections)
385+ let bridge3 = Bridge :: with_limits ( 10 , 100 ) ;
386+ let _ = bridge3;
387+ }
388+
389+ /// Tests `cache_stats` with various cache states
390+ #[ tokio:: test]
391+ async fn test_cache_stats_edge_cases ( ) {
392+ // Minimum cache size
393+ let bridge = Bridge :: new ( 1 ) ;
394+ let stats = bridge. cache_stats ( ) . await ;
395+ assert_eq ! ( stats. capacity, 1 ) ;
396+ assert_eq ! ( stats. size, 0 ) ;
397+
398+ // Very large cache
399+ let bridge_large = Bridge :: new ( 1_000_000 ) ;
400+ let stats_large = bridge_large. cache_stats ( ) . await ;
401+ assert_eq ! ( stats_large. capacity, 1_000_000 ) ;
402+ assert_eq ! ( stats_large. size, 0 ) ;
403+ }
404+
405+ /// Tests `connection_call_count` with various server IDs
406+ #[ tokio:: test]
407+ async fn test_connection_call_count_variations ( ) {
408+ let bridge = Bridge :: new ( 100 ) ;
409+
410+ // Non-existent server
411+ let server1 = ServerId :: new ( "nonexistent" ) ;
412+ assert ! ( bridge. connection_call_count( & server1) . await . is_none( ) ) ;
413+
414+ // Different server ID
415+ let server2 = ServerId :: new ( "another-server" ) ;
416+ assert ! ( bridge. connection_call_count( & server2) . await . is_none( ) ) ;
417+
418+ // Empty string server ID
419+ let server3 = ServerId :: new ( "" ) ;
420+ assert ! ( bridge. connection_call_count( & server3) . await . is_none( ) ) ;
421+ }
422+
423+ /// Tests concurrent disconnect operations
424+ #[ tokio:: test]
425+ async fn test_concurrent_disconnects ( ) {
426+ use std:: sync:: Arc ;
427+
428+ let bridge = Arc :: new ( Bridge :: new ( 100 ) ) ;
429+ let server_id = ServerId :: new ( "test" ) ;
430+
431+ let mut handles = vec ! [ ] ;
432+
433+ // Spawn multiple tasks disconnecting concurrently
434+ for _ in 0 ..10 {
435+ let bridge_clone = Arc :: clone ( & bridge) ;
436+ let server_clone = server_id. clone ( ) ;
437+ let handle = tokio:: spawn ( async move {
438+ bridge_clone. disconnect ( & server_clone) . await ;
439+ } ) ;
440+ handles. push ( handle) ;
441+ }
442+
443+ // Wait for all tasks
444+ for handle in handles {
445+ handle. await . unwrap ( ) ;
446+ }
447+
448+ // Should remain at 0 connections
449+ assert_eq ! ( bridge. connection_count( ) . await , 0 ) ;
450+ }
451+
452+ /// Tests `CacheStats` Debug implementation
453+ #[ test]
454+ fn test_cache_stats_debug ( ) {
455+ let stats = CacheStats {
456+ size : 42 ,
457+ capacity : 100 ,
458+ } ;
459+
460+ let debug_str = format ! ( "{stats:?}" ) ;
461+ assert ! ( debug_str. contains( "42" ) ) ;
462+ assert ! ( debug_str. contains( "100" ) ) ;
463+ }
464+
465+ /// Tests Bridge Debug implementation
466+ #[ test]
467+ fn test_bridge_debug ( ) {
468+ let bridge = Bridge :: new ( 100 ) ;
469+ let debug_str = format ! ( "{bridge:?}" ) ;
470+
471+ assert ! ( debug_str. contains( "Bridge" ) ) ;
472+ }
473+
474+ /// Tests command validation with special characters
475+ #[ tokio:: test]
476+ async fn test_command_validation_special_chars ( ) {
477+ let bridge = Bridge :: new ( 100 ) ;
478+ let server_id = ServerId :: new ( "test" ) ;
479+
480+ // Commands with special characters that might indicate injection attempts
481+ let dangerous_commands = vec ! [
482+ "cmd && malicious" ,
483+ "cmd || fallback" ,
484+ "cmd | pipe" ,
485+ "cmd > redirect" ,
486+ "cmd < input" ,
487+ "cmd `backtick`" ,
488+ "cmd $(substitution)" ,
489+ "cmd;chain" ,
490+ ] ;
491+
492+ for cmd in dangerous_commands {
493+ let result = bridge. connect ( server_id. clone ( ) , cmd) . await ;
494+ // Should fail validation or connection
495+ assert ! ( result. is_err( ) , "Command should be rejected: {cmd}" ) ;
496+ }
497+ }
498+
499+ /// Tests connection limits percentage calculation
500+ #[ tokio:: test]
501+ #[ allow( clippy:: cast_precision_loss) ]
502+ async fn test_connection_usage_percentage ( ) {
503+ let bridge = Bridge :: with_limits ( 100 , 10 ) ;
504+
505+ let ( current, max) = bridge. connection_limits ( ) . await ;
506+ let usage_percent = ( current as f64 / max as f64 ) * 100.0 ;
507+
508+ assert ! ( ( usage_percent - 0.0 ) . abs( ) < f64 :: EPSILON ) ;
509+ assert_eq ! ( max, 10 ) ;
510+ }
511+
512+ /// Tests cache enable/disable state persistence
513+ #[ test]
514+ fn test_cache_state_persistence ( ) {
515+ let mut bridge = Bridge :: new ( 100 ) ;
516+
517+ // Initial state
518+ assert ! ( !format!( "{bridge:?}" ) . is_empty( ) ) ;
519+
520+ // Toggle state multiple times
521+ bridge. disable_cache ( ) ;
522+ bridge. disable_cache ( ) ; // Double disable
523+ bridge. enable_cache ( ) ;
524+ bridge. enable_cache ( ) ; // Double enable
525+
526+ // Should be stable
527+ assert ! ( !format!( "{bridge:?}" ) . is_empty( ) ) ;
528+ }
0 commit comments