diff --git a/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs b/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs index c8ad617e7d..e5c39a7773 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs @@ -44,6 +44,24 @@ pub struct RemoteControlStatusReadResponse { pub environment_id: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RemoteControlPairingStartParams { + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub manual_code: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RemoteControlPairingStartResponse { + pub pairing_code: String, + pub manual_pairing_code: Option, + pub environment_id: String, + pub expires_at: i64, +} + #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase", export_to = "v2/")] diff --git a/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs b/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs index 6037caa651..ec76158586 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/enroll.rs @@ -211,8 +211,14 @@ fn redact_remote_control_response_body(body: &str) -> String { let Some(body_object) = body_json.as_object_mut() else { return body.to_string(); }; - if let Some(remote_control_token) = body_object.get_mut("remote_control_token") { - *remote_control_token = serde_json::Value::String("".to_string()); + for sensitive_field in [ + "remote_control_token", + "pairing_code", + "manual_pairing_code", + ] { + if let Some(value) = body_object.get_mut(sensitive_field) { + *value = serde_json::Value::String("".to_string()); + } } body_json.to_string() } @@ -443,6 +449,20 @@ mod tests { ); } + #[test] + fn preview_remote_control_response_body_redacts_pairing_codes() { + assert_eq!( + serde_json::from_str::(&preview_remote_control_response_body( + br#"{"pairing_code":"pairing-code","manual_pairing_code":"ABCD-EFGH"}"# + )) + .expect("redacted response preview should stay valid json"), + json!({ + "pairing_code": "", + "manual_pairing_code": "", + }) + ); + } + #[tokio::test] async fn persisted_remote_control_enrollment_round_trips_by_target_and_account() { let codex_home = TempDir::new().expect("temp dir should create"); diff --git a/codex-rs/app-server-transport/src/transport/remote_control/mod.rs b/codex-rs/app-server-transport/src/transport/remote_control/mod.rs index 357a98a30d..065eb735c4 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/mod.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/mod.rs @@ -1,9 +1,11 @@ mod client_tracker; mod enroll; +mod pairing; mod protocol; mod segment; mod websocket; +use self::pairing::RemoteControlPairingClient; use crate::transport::remote_control::websocket::RemoteControlChannels; use crate::transport::remote_control::websocket::RemoteControlStatusPublisher; use crate::transport::remote_control::websocket::RemoteControlWebsocket; @@ -16,6 +18,8 @@ use super::CHANNEL_CAPACITY; use super::TransportEvent; use super::next_connection_id; use codex_app_server_protocol::RemoteControlConnectionStatus; +use codex_app_server_protocol::RemoteControlPairingStartParams; +use codex_app_server_protocol::RemoteControlPairingStartResponse; use codex_app_server_protocol::RemoteControlStatusChangedNotification; use codex_login::AuthManager; use codex_state::StateRuntime; @@ -26,6 +30,7 @@ use std::fmt; use std::io; use std::panic::AssertUnwindSafe; use std::sync::Arc; +use std::sync::Mutex as StdMutex; use tokio::sync::mpsc; use tokio::sync::oneshot; use tokio::sync::watch; @@ -52,6 +57,7 @@ pub struct RemoteControlHandle { enabled_tx: Arc>, status_tx: Arc>, state_db_available: bool, + pairing_client: Arc>>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -129,6 +135,35 @@ impl RemoteControlHandle { self.status_tx.subscribe() } + pub async fn start_pairing( + &self, + params: RemoteControlPairingStartParams, + ) -> io::Result { + if !*self.enabled_tx.borrow() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "remote control pairing requires remote control to be enabled", + )); + } + + let pairing_client = self + .pairing_client + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone() + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "remote control pairing is unavailable until enrollment completes", + ) + })?; + pairing_client + .start(protocol::StartRemoteControlPairingRequest { + manual_code: params.manual_code, + }) + .await + } + fn publish_status( &self, connection_status: RemoteControlConnectionStatus, @@ -198,6 +233,8 @@ pub async fn start_remote_control( }; let (enabled_tx, enabled_rx) = watch::channel(initial_enabled); + let pairing_client = Arc::new(StdMutex::new(None)); + let websocket_pairing_client = Arc::clone(&pairing_client); let server_name = gethostname().to_string_lossy().trim().to_string(); let remote_control_url = config.remote_control_url; let installation_id = config.installation_id; @@ -245,6 +282,7 @@ pub async fn start_remote_control( RemoteControlChannels { transport_event_tx, status_publisher, + pairing_client: websocket_pairing_client, }, shutdown_token, enabled_rx, @@ -289,10 +327,13 @@ pub async fn start_remote_control( enabled_tx: Arc::new(enabled_tx), status_tx: Arc::new(status_tx), state_db_available, + pairing_client, }, )) } +#[cfg(test)] +mod pairing_tests; #[cfg(test)] mod segment_tests; #[cfg(test)] diff --git a/codex-rs/app-server-transport/src/transport/remote_control/pairing.rs b/codex-rs/app-server-transport/src/transport/remote_control/pairing.rs new file mode 100644 index 0000000000..1b8acd2ad3 --- /dev/null +++ b/codex-rs/app-server-transport/src/transport/remote_control/pairing.rs @@ -0,0 +1,109 @@ +use super::enroll::format_headers; +use super::enroll::preview_remote_control_response_body; +use super::protocol::RemoteControlTarget; +use super::protocol::StartRemoteControlPairingRequest; +use super::protocol::StartRemoteControlPairingResponse; +use codex_app_server_protocol::RemoteControlPairingStartResponse; +use codex_login::default_client::build_reqwest_client; +use std::io; +use std::io::ErrorKind; +use time::OffsetDateTime; +use time::format_description::well_known::Rfc3339; + +const REMOTE_CONTROL_PAIRING_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); + +#[derive(Debug, Clone)] +pub(super) struct RemoteControlPairingClient { + pairing_url: String, + remote_control_token: String, + expires_at: OffsetDateTime, +} + +impl RemoteControlPairingClient { + pub(super) fn new( + remote_control_target: &RemoteControlTarget, + remote_control_token: String, + expires_at: OffsetDateTime, + ) -> Self { + Self { + pairing_url: remote_control_target.pair_url.clone(), + remote_control_token, + expires_at, + } + } + + pub(super) async fn start( + &self, + request: StartRemoteControlPairingRequest, + ) -> io::Result { + if self.expires_at <= OffsetDateTime::now_utc() { + return Err(io::Error::new( + ErrorKind::InvalidInput, + "remote control pairing is unavailable because the server token expired", + )); + } + + let response = build_reqwest_client() + .post(&self.pairing_url) + .timeout(REMOTE_CONTROL_PAIRING_TIMEOUT) + .bearer_auth(&self.remote_control_token) + .json(&request) + .send() + .await + .map_err(|err| { + io::Error::other(format!( + "failed to start remote control pairing at `{}`: {err}", + self.pairing_url + )) + })?; + let headers = response.headers().clone(); + let status = response.status(); + let body = response.bytes().await.map_err(|err| { + io::Error::other(format!( + "failed to read remote control pairing response from `{}`: {err}", + self.pairing_url + )) + })?; + let body_preview = preview_remote_control_response_body(&body); + if !status.is_success() { + return Err(io::Error::other(format!( + "remote control pairing failed at `{}`: HTTP {status}, {}, body: {body_preview}", + self.pairing_url, + format_headers(&headers) + ))); + } + + let pairing = serde_json::from_slice::(&body).map_err( + |err| { + io::Error::other(format!( + "failed to parse remote control pairing response from `{}`: HTTP {status}, {}, body: {body_preview}, decode error: {err}", + self.pairing_url, + format_headers(&headers) + )) + }, + )?; + let StartRemoteControlPairingResponse { + pairing_code, + manual_pairing_code, + server_id, + environment_id, + expires_at, + } = pairing; + let _ = server_id; + let expires_at = OffsetDateTime::parse(&expires_at, &Rfc3339) + .map_err(|err| { + io::Error::new( + ErrorKind::InvalidData, + format!("invalid remote control pairing expires_at: {err}"), + ) + })? + .unix_timestamp(); + + Ok(RemoteControlPairingStartResponse { + pairing_code, + manual_pairing_code, + environment_id, + expires_at, + }) + } +} diff --git a/codex-rs/app-server-transport/src/transport/remote_control/pairing_tests.rs b/codex-rs/app-server-transport/src/transport/remote_control/pairing_tests.rs new file mode 100644 index 0000000000..77dd7c2a2f --- /dev/null +++ b/codex-rs/app-server-transport/src/transport/remote_control/pairing_tests.rs @@ -0,0 +1,186 @@ +use super::pairing::RemoteControlPairingClient; +use super::protocol::RemoteControlTarget; +use super::protocol::StartRemoteControlPairingRequest; +use codex_app_server_protocol::RemoteControlPairingStartResponse; +use pretty_assertions::assert_eq; +use serde_json::json; +use time::OffsetDateTime; +use tokio::io::AsyncBufReadExt; +use tokio::io::AsyncReadExt; +use tokio::io::AsyncWriteExt; +use tokio::io::BufReader; +use tokio::net::TcpListener; + +#[tokio::test] +async fn start_remote_control_pairing_uses_server_token_and_maps_response() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("listener should bind"); + let pair_url = format!( + "http://{}/backend-api/wham/remote/control/server/pair", + listener.local_addr().expect("listener should have addr") + ); + let server_task = tokio::spawn(async move { + let (stream, _) = listener.accept().await.expect("request should arrive"); + let mut reader = BufReader::new(stream); + + let mut request_line = String::new(); + reader + .read_line(&mut request_line) + .await + .expect("request line should read"); + assert_eq!( + request_line.trim_end(), + "POST /backend-api/wham/remote/control/server/pair HTTP/1.1" + ); + + let mut authorization = None; + let mut content_length = None; + loop { + let mut line = String::new(); + reader + .read_line(&mut line) + .await + .expect("header line should read"); + if line == "\r\n" { + break; + } + let (name, value) = line + .trim_end() + .split_once(": ") + .expect("header should split"); + match name.to_ascii_lowercase().as_str() { + "authorization" => authorization = Some(value.to_string()), + "content-length" => { + content_length = + Some(value.parse::().expect("content length should parse")) + } + _ => {} + } + } + assert_eq!( + authorization, + Some("Bearer remote-control-token".to_string()) + ); + + let mut body = vec![0; content_length.expect("request should have body")]; + reader + .read_exact(&mut body) + .await + .expect("request body should read"); + assert_eq!( + serde_json::from_slice::(&body) + .expect("request body should be json"), + json!({ "manual_code": true }) + ); + + let response_body = json!({ + "pairing_code": "pairing-code", + "manual_pairing_code": "ABCD-EFGH", + "server_id": "server-id", + "environment_id": "environment-id", + "expires_at": "3026-05-22T12:34:56Z", + }) + .to_string(); + reader + .get_mut() + .write_all( + format!( + "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\r\n{response_body}", + response_body.len() + ) + .as_bytes(), + ) + .await + .expect("response should write"); + }); + let client = RemoteControlPairingClient::new( + &RemoteControlTarget { + websocket_url: "ws://unused".to_string(), + enroll_url: "http://unused".to_string(), + refresh_url: "http://unused".to_string(), + pair_url, + }, + "remote-control-token".to_string(), + OffsetDateTime::from_unix_timestamp(33_336_362_096).expect("future timestamp should parse"), + ); + + let response = client + .start(StartRemoteControlPairingRequest { manual_code: true }) + .await + .expect("pairing should succeed"); + server_task.await.expect("server task should finish"); + + assert_eq!( + response, + RemoteControlPairingStartResponse { + pairing_code: "pairing-code".to_string(), + manual_pairing_code: Some("ABCD-EFGH".to_string()), + environment_id: "environment-id".to_string(), + expires_at: 33_336_362_096, + } + ); +} + +#[tokio::test] +async fn start_remote_control_pairing_preserves_backend_error_context() { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("listener should bind"); + let pair_url = format!( + "http://{}/backend-api/wham/remote/control/server/pair", + listener.local_addr().expect("listener should have addr") + ); + let expected_pair_url = pair_url.clone(); + let server_task = tokio::spawn(async move { + let (stream, _) = listener.accept().await.expect("request should arrive"); + let mut reader = BufReader::new(stream); + + loop { + let mut line = String::new(); + reader + .read_line(&mut line) + .await + .expect("request line should read"); + if line == "\r\n" { + break; + } + } + + let response_body = "pairing unavailable"; + reader + .get_mut() + .write_all( + format!( + "HTTP/1.1 503 Service Unavailable\r\nx-request-id: request-123\r\ncf-ray: ray-123\r\ncontent-length: {}\r\n\r\n{response_body}", + response_body.len() + ) + .as_bytes(), + ) + .await + .expect("response should write"); + }); + let client = RemoteControlPairingClient::new( + &RemoteControlTarget { + websocket_url: "ws://unused".to_string(), + enroll_url: "http://unused".to_string(), + refresh_url: "http://unused".to_string(), + pair_url, + }, + "remote-control-token".to_string(), + OffsetDateTime::from_unix_timestamp(33_336_362_096).expect("future timestamp should parse"), + ); + + let err = client + .start(StartRemoteControlPairingRequest { manual_code: false }) + .await + .expect_err("pairing should fail"); + server_task.await.expect("server task should finish"); + + assert_eq!( + err.to_string(), + format!( + "remote control pairing failed at `{expected_pair_url}`: HTTP 503 Service Unavailable, request-id: request-123, cf-ray: ray-123, body: pairing unavailable" + ) + ); +} diff --git a/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs b/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs index b10af42e86..8eeb28e266 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/protocol.rs @@ -12,6 +12,7 @@ pub(super) struct RemoteControlTarget { pub(super) websocket_url: String, pub(super) enroll_url: String, pub(super) refresh_url: String, + pub(super) pair_url: String, } #[derive(Debug, Serialize)] @@ -37,6 +38,20 @@ pub(super) struct RefreshRemoteServerRequest { pub(super) installation_id: String, } +#[derive(Debug, Serialize)] +pub(super) struct StartRemoteControlPairingRequest { + pub(super) manual_code: bool, +} + +#[derive(Debug, Deserialize)] +pub(super) struct StartRemoteControlPairingResponse { + pub(super) pairing_code: String, + pub(super) manual_pairing_code: Option, + pub(super) server_id: String, + pub(super) environment_id: String, + pub(super) expires_at: String, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct ClientId(pub String); @@ -189,6 +204,9 @@ pub(super) fn normalize_remote_control_url( let refresh_url = remote_control_url .join("wham/remote/control/server/refresh") .map_err(map_url_parse_error)?; + let pair_url = remote_control_url + .join("wham/remote/control/server/pair") + .map_err(map_url_parse_error)?; let mut websocket_url = remote_control_url .join("wham/remote/control/server") .map_err(map_url_parse_error)?; @@ -207,6 +225,7 @@ pub(super) fn normalize_remote_control_url( websocket_url: websocket_url.to_string(), enroll_url: enroll_url.to_string(), refresh_url: refresh_url.to_string(), + pair_url: pair_url.to_string(), }) } @@ -227,6 +246,8 @@ mod tests { .to_string(), refresh_url: "https://chatgpt.com/backend-api/wham/remote/control/server/refresh" .to_string(), + pair_url: "https://chatgpt.com/backend-api/wham/remote/control/server/pair" + .to_string(), } ); assert_eq!( @@ -242,6 +263,9 @@ mod tests { refresh_url: "https://api.chatgpt-staging.com/backend-api/wham/remote/control/server/refresh" .to_string(), + pair_url: + "https://api.chatgpt-staging.com/backend-api/wham/remote/control/server/pair" + .to_string(), } ); } @@ -258,6 +282,8 @@ mod tests { .to_string(), refresh_url: "http://localhost:8080/backend-api/wham/remote/control/server/refresh" .to_string(), + pair_url: "http://localhost:8080/backend-api/wham/remote/control/server/pair" + .to_string(), } ); assert_eq!( @@ -271,6 +297,8 @@ mod tests { refresh_url: "https://localhost:8443/backend-api/wham/remote/control/server/refresh" .to_string(), + pair_url: "https://localhost:8443/backend-api/wham/remote/control/server/pair" + .to_string(), } ); } diff --git a/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs b/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs index 21710b5e26..fa4901e08a 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs @@ -9,6 +9,7 @@ use crate::transport::remote_control::enroll::load_persisted_remote_control_enro use crate::transport::remote_control::enroll::preview_remote_control_response_body; use crate::transport::remote_control::enroll::refresh_remote_control_server; use crate::transport::remote_control::enroll::update_persisted_remote_control_enrollment; +use crate::transport::remote_control::pairing::RemoteControlPairingClient; use super::protocol::ClientEnvelope; use super::protocol::ClientEvent; @@ -39,6 +40,7 @@ use std::collections::VecDeque; use std::io; use std::io::ErrorKind; use std::sync::Arc; +use std::sync::Mutex as StdMutex; use tokio::net::TcpStream; use tokio::sync::Mutex; use tokio::sync::mpsc; @@ -251,6 +253,7 @@ pub(crate) struct RemoteControlWebsocket { enrollment: Option, auth_recovery: UnauthorizedRecovery, auth_change_rx: watch::Receiver, + pairing_client: Arc>>, client_tracker: Arc>, state: Arc>, server_event_rx: Arc>>, @@ -288,6 +291,7 @@ enum ConnectionEndReason { pub(super) struct RemoteControlChannels { pub(super) transport_event_tx: mpsc::Sender, pub(super) status_publisher: RemoteControlStatusPublisher, + pub(super) pairing_client: Arc>>, } #[derive(Clone)] @@ -404,6 +408,7 @@ impl RemoteControlWebsocket { enrollment: None, auth_recovery, auth_change_rx, + pairing_client: channels.pairing_client, client_tracker: Arc::new(Mutex::new(client_tracker)), state: Arc::new(Mutex::new(WebsocketState { outbound_buffer, @@ -611,6 +616,7 @@ impl RemoteControlWebsocket { &mut self.enrollment, connect_options, &self.status_publisher, + &self.pairing_client, ) => connect_result, }; @@ -1229,6 +1235,7 @@ pub(super) async fn connect_remote_control_websocket( enrollment: &mut Option, connect_options: RemoteControlConnectOptions<'_>, status_publisher: &RemoteControlStatusPublisher, + pairing_client: &Arc>>, ) -> io::Result<( WebSocketStream>, tungstenite::http::Response<()>, @@ -1237,6 +1244,7 @@ pub(super) async fn connect_remote_control_websocket( let Some(state_db) = state_db else { *enrollment = None; + clear_pairing_client(pairing_client); return Err(io::Error::new( ErrorKind::NotFound, "remote control requires sqlite state db", @@ -1249,6 +1257,7 @@ pub(super) async fn connect_remote_control_websocket( if err.kind() == ErrorKind::PermissionDenied { *enrollment = None; status_publisher.publish_environment_id(/*environment_id*/ None); + clear_pairing_client(pairing_client); } return Err(err); } @@ -1265,6 +1274,7 @@ pub(super) async fn connect_remote_control_websocket( ); *enrollment = None; status_publisher.publish_environment_id(/*environment_id*/ None); + clear_pairing_client(pairing_client); } if let Some(enrollment) = enrollment.as_ref() { @@ -1342,6 +1352,7 @@ pub(super) async fn connect_remote_control_websocket( connect_options.app_server_client_name, enrollment, status_publisher, + pairing_client, ) .await; enroll_remote_control_server_if_missing( @@ -1374,6 +1385,7 @@ pub(super) async fn connect_remote_control_websocket( let enrollment_ref = enrollment.as_ref().ok_or_else(|| { io::Error::other("missing remote control enrollment after enrollment step") })?; + set_pairing_client(pairing_client, remote_control_target, enrollment_ref)?; let request = build_remote_control_websocket_request( &remote_control_target.websocket_url, enrollment_ref, @@ -1415,6 +1427,7 @@ pub(super) async fn connect_remote_control_websocket( connect_options.app_server_client_name, enrollment, status_publisher, + pairing_client, ) .await; } @@ -1429,6 +1442,7 @@ pub(super) async fn connect_remote_control_websocket( ) })? .clear_server_token(); + clear_pairing_client(pairing_client); return Err(io::Error::other(format!( "remote control websocket auth failed with HTTP {}; refreshing server token before reconnect", response.status() @@ -1453,6 +1467,7 @@ async fn clear_remote_control_enrollment( app_server_client_name: Option<&str>, enrollment: &mut Option, status_publisher: &RemoteControlStatusPublisher, + pairing_client: &Arc>>, ) { if let Err(clear_err) = update_persisted_remote_control_enrollment( Some(state_db), @@ -1467,6 +1482,38 @@ async fn clear_remote_control_enrollment( } *enrollment = None; status_publisher.publish_environment_id(/*environment_id*/ None); + clear_pairing_client(pairing_client); +} + +fn set_pairing_client( + pairing_client: &Arc>>, + remote_control_target: &RemoteControlTarget, + enrollment: &RemoteControlEnrollment, +) -> io::Result<()> { + let remote_control_token = enrollment.remote_control_token.clone().ok_or_else(|| { + io::Error::new( + ErrorKind::InvalidInput, + "remote control pairing is unavailable until enrollment completes", + ) + })?; + let expires_at = enrollment.expires_at.ok_or_else(|| { + io::Error::new( + ErrorKind::InvalidInput, + "remote control pairing is unavailable until enrollment completes", + ) + })?; + *pairing_client + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) = Some( + RemoteControlPairingClient::new(remote_control_target, remote_control_token, expires_at), + ); + Ok(()) +} + +fn clear_pairing_client(pairing_client: &Arc>>) { + *pairing_client + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) = None; } async fn enroll_remote_control_server_if_missing( @@ -1657,6 +1704,10 @@ mod tests { } } + fn test_pairing_client() -> Arc>> { + Arc::new(StdMutex::new(None)) + } + #[test] fn next_reconnect_delay_resets_after_cap() { let mut reconnect_attempt = 9; @@ -1810,6 +1861,7 @@ mod tests { let mut enrollment = Some(remote_control_enrollment(Some( TEST_REMOTE_CONTROL_SERVER_TOKEN, ))); + let pairing_client = test_pairing_client(); let (status_publisher, status_rx) = remote_control_status_channel(); let err = match connect_remote_control_websocket( @@ -1828,6 +1880,7 @@ mod tests { app_server_client_name: None, }, &status_publisher, + &pairing_client, ) .await { @@ -1864,6 +1917,7 @@ mod tests { let mut enrollment = Some(remote_control_enrollment(Some( TEST_REMOTE_CONTROL_SERVER_TOKEN, ))); + let pairing_client = test_pairing_client(); let (status_publisher, status_rx) = remote_control_status_channel(); let server_task = tokio::spawn(async move { @@ -1891,6 +1945,7 @@ mod tests { app_server_client_name: None, }, &status_publisher, + &pairing_client, ) .await .expect_err("unauthorized response should fail the websocket connect"); @@ -1915,6 +1970,13 @@ mod tests { /*remote_control_token*/ None )) ); + assert_eq!( + pairing_client + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .is_none(), + true + ); } #[tokio::test] @@ -1976,6 +2038,7 @@ mod tests { app_server_client_name: None, }, &status_publisher, + &test_pairing_client(), ) .await .expect_err("unauthorized enrollment should fail the websocket connect"); @@ -2070,6 +2133,7 @@ mod tests { app_server_client_name: None, }, &status_publisher, + &test_pairing_client(), ) .await .expect_err("unauthorized refresh should fail the websocket connect"); @@ -2135,6 +2199,7 @@ mod tests { app_server_client_name: None, }, &status_publisher, + &test_pairing_client(), ) .await .expect_err("missing sqlite state db should fail remote control"); @@ -2185,6 +2250,7 @@ mod tests { app_server_client_name: None, }, &status_publisher, + &test_pairing_client(), ) .await .expect_err("missing auth should fail remote control"); @@ -2236,6 +2302,7 @@ mod tests { RemoteControlChannels { transport_event_tx, status_publisher, + pairing_client: test_pairing_client(), }, shutdown_token, enabled_rx,