@@ -866,8 +866,11 @@ mod tests {
866866 use tokio:: net:: TcpListener ;
867867 use tokio:: time:: Duration ;
868868 use tokio:: time:: timeout;
869- use tokio_tungstenite:: accept_async ;
869+ use tokio_tungstenite:: accept_hdr_async ;
870870 use tokio_tungstenite:: tungstenite:: Message ;
871+ use tokio_tungstenite:: tungstenite:: handshake:: server:: Request as WebSocketRequest ;
872+ use tokio_tungstenite:: tungstenite:: handshake:: server:: Response as WebSocketResponse ;
873+ use tokio_tungstenite:: tungstenite:: http:: header:: AUTHORIZATION ;
871874
872875 async fn build_test_config ( ) -> Config {
873876 match ConfigBuilder :: default ( ) . build ( ) . await {
@@ -906,6 +909,19 @@ mod tests {
906909 }
907910
908911 async fn start_test_remote_server < F , Fut > ( handler : F ) -> String
912+ where
913+ F : FnOnce ( tokio_tungstenite:: WebSocketStream < tokio:: net:: TcpStream > ) -> Fut
914+ + Send
915+ + ' static ,
916+ Fut : std:: future:: Future < Output = ( ) > + Send + ' static ,
917+ {
918+ start_test_remote_server_with_auth ( None , handler) . await
919+ }
920+
921+ async fn start_test_remote_server_with_auth < F , Fut > (
922+ expected_auth_token : Option < String > ,
923+ handler : F ,
924+ ) -> String
909925 where
910926 F : FnOnce ( tokio_tungstenite:: WebSocketStream < tokio:: net:: TcpStream > ) -> Fut
911927 + Send
@@ -918,9 +934,23 @@ mod tests {
918934 let addr = listener. local_addr ( ) . expect ( "listener address" ) ;
919935 tokio:: spawn ( async move {
920936 let ( stream, _) = listener. accept ( ) . await . expect ( "accept should succeed" ) ;
921- let websocket = accept_async ( stream)
922- . await
923- . expect ( "websocket upgrade should succeed" ) ;
937+ let websocket = accept_hdr_async (
938+ stream,
939+ move |request : & WebSocketRequest , response : WebSocketResponse | {
940+ let provided_auth_token = request
941+ . headers ( )
942+ . get ( AUTHORIZATION )
943+ . and_then ( |value| value. to_str ( ) . ok ( ) )
944+ . map ( str:: to_owned) ;
945+ let expected_auth_token = expected_auth_token
946+ . as_ref ( )
947+ . map ( |token| format ! ( "Bearer {token}" ) ) ;
948+ assert_eq ! ( provided_auth_token, expected_auth_token) ;
949+ Ok ( response)
950+ } ,
951+ )
952+ . await
953+ . expect ( "websocket upgrade should succeed" ) ;
924954 handler ( websocket) . await ;
925955 } ) ;
926956 format ! ( "ws://{addr}" )
@@ -988,6 +1018,7 @@ mod tests {
9881018 fn test_remote_connect_args ( websocket_url : String ) -> RemoteAppServerConnectArgs {
9891019 RemoteAppServerConnectArgs {
9901020 websocket_url,
1021+ auth_token : None ,
9911022 client_name : "codex-app-server-client-test" . to_string ( ) ,
9921023 client_version : "0.0.0-test" . to_string ( ) ,
9931024 experimental_api : true ,
@@ -1114,6 +1145,7 @@ mod tests {
11141145 } ) ,
11151146 )
11161147 . await ;
1148+ websocket. close ( None ) . await . expect ( "close should succeed" ) ;
11171149 } )
11181150 . await ;
11191151 let client = RemoteAppServerClient :: connect ( test_remote_connect_args ( websocket_url) )
@@ -1134,6 +1166,58 @@ mod tests {
11341166 client. shutdown ( ) . await . expect ( "shutdown should complete" ) ;
11351167 }
11361168
1169+ async fn remote_connect_includes_auth_header_when_configured ( ) {
1170+ let auth_token = "remote-bearer-token" . to_string ( ) ;
1171+ let websocket_url = start_test_remote_server_with_auth (
1172+ Some ( auth_token. clone ( ) ) ,
1173+ |mut websocket| async move {
1174+ expect_remote_initialize ( & mut websocket) . await ;
1175+ websocket. close ( None ) . await . expect ( "close should succeed" ) ;
1176+ } ,
1177+ )
1178+ . await ;
1179+ let client = RemoteAppServerClient :: connect ( RemoteAppServerConnectArgs {
1180+ auth_token : Some ( auth_token) ,
1181+ ..test_remote_connect_args ( websocket_url)
1182+ } )
1183+ . await
1184+ . expect ( "remote client should connect" ) ;
1185+
1186+ client. shutdown ( ) . await . expect ( "shutdown should complete" ) ;
1187+ }
1188+
1189+ #[ tokio:: test]
1190+ async fn remote_connect_rejects_non_loopback_ws_when_auth_configured ( ) {
1191+ let result = RemoteAppServerClient :: connect ( RemoteAppServerConnectArgs {
1192+ websocket_url : "ws://example.com:4500" . to_string ( ) ,
1193+ auth_token : Some ( "remote-bearer-token" . to_string ( ) ) ,
1194+ ..test_remote_connect_args ( "ws://127.0.0.1:1" . to_string ( ) )
1195+ } )
1196+ . await ;
1197+ let err = match result {
1198+ Ok ( _) => panic ! ( "non-loopback ws should be rejected before connect" ) ,
1199+ Err ( err) => err,
1200+ } ;
1201+ assert_eq ! ( err. kind( ) , ErrorKind :: InvalidInput ) ;
1202+ assert ! (
1203+ err. to_string( )
1204+ . contains( "remote auth tokens require `wss://` or loopback `ws://` URLs" )
1205+ ) ;
1206+ }
1207+
1208+ #[ test]
1209+ fn remote_auth_token_transport_policy_allows_wss_and_loopback_ws ( ) {
1210+ assert ! ( crate :: remote:: websocket_url_supports_auth_token(
1211+ & url:: Url :: parse( "wss://example.com:443" ) . expect( "wss URL should parse" )
1212+ ) ) ;
1213+ assert ! ( crate :: remote:: websocket_url_supports_auth_token(
1214+ & url:: Url :: parse( "ws://127.0.0.1:4500" ) . expect( "loopback ws URL should parse" )
1215+ ) ) ;
1216+ assert ! ( !crate :: remote:: websocket_url_supports_auth_token(
1217+ & url:: Url :: parse( "ws://example.com:4500" ) . expect( "non-loopback ws URL should parse" )
1218+ ) ) ;
1219+ }
1220+
11371221 #[ tokio:: test]
11381222 async fn remote_duplicate_request_id_keeps_original_waiter ( ) {
11391223 let ( first_request_seen_tx, first_request_seen_rx) = tokio:: sync:: oneshot:: channel ( ) ;
0 commit comments