mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(remote-control): add pairing status transport (#26449)
## What
Adds transport support for checking remote-control pairing status
against the backend.
- Adds the normalized `server/pair/status` backend URL.
- Adds backend request/response structs for exactly one lookup key:
`pairing_code` or `manual_pairing_code`, returning `{ claimed }`.
- Adds `RemoteControlEnrollment::pairing_status` and
`RemoteControlHandle::pairing_status`.
- Preserves auth refresh/retry behavior and backend error mapping.
- Adds transport coverage for pending, claimed, manual-code payloads,
token refresh, mapped backend errors, malformed responses, and URL
normalization.
## Why
Desktop needs a host-authenticated way to poll whether a QR or manual
pairing code has been claimed.
Related backend change: https://github.com/openai/openai/pull/990244
## Verification
- `cargo test --manifest-path app-server-transport/Cargo.toml
remote_control::tests::pairing_tests`
- `cargo fmt --all --check`
- `git diff --check`
This commit is contained in:
committed by
GitHub
Unverified
parent
9ddb1de633
commit
da490ba9de
@@ -62,6 +62,23 @@ pub struct RemoteControlPairingStartResponse {
|
||||
pub expires_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlPairingStatusParams {
|
||||
#[ts(optional = nullable)]
|
||||
pub pairing_code: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub manual_pairing_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlPairingStatusResponse {
|
||||
pub claimed: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -3,11 +3,14 @@ use super::pairing_unavailable_error;
|
||||
use super::protocol::EnrollRemoteServerRequest;
|
||||
use super::protocol::EnrollRemoteServerResponse;
|
||||
use super::protocol::RefreshRemoteServerRequest;
|
||||
use super::protocol::RemoteControlPairingStatusRequest;
|
||||
use super::protocol::RemoteControlPairingStatusResponse as BackendRemoteControlPairingStatusResponse;
|
||||
use super::protocol::RemoteControlTarget;
|
||||
use super::protocol::StartRemoteControlPairingRequest;
|
||||
use super::protocol::StartRemoteControlPairingResponse;
|
||||
use axum::http::HeaderMap;
|
||||
use codex_app_server_protocol::RemoteControlPairingStartResponse;
|
||||
use codex_app_server_protocol::RemoteControlPairingStatusResponse;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
use codex_state::RemoteControlEnrollmentRecord;
|
||||
use codex_state::StateRuntime;
|
||||
@@ -136,6 +139,69 @@ impl RemoteControlEnrollment {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn pairing_status(
|
||||
&self,
|
||||
request: RemoteControlPairingStatusRequest,
|
||||
) -> io::Result<RemoteControlPairingStatusResponse> {
|
||||
if self.should_refresh_server_token() {
|
||||
return Err(pairing_unavailable_error());
|
||||
}
|
||||
let remote_control_token = self
|
||||
.remote_control_token
|
||||
.as_deref()
|
||||
.ok_or_else(pairing_unavailable_error)?;
|
||||
|
||||
let response = build_reqwest_client()
|
||||
.post(&self.remote_control_target.pair_status_url)
|
||||
.timeout(REMOTE_CONTROL_PAIRING_TIMEOUT)
|
||||
.bearer_auth(remote_control_token)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
io::Error::other(format!(
|
||||
"failed to check remote control pairing status at `{}`: {err}",
|
||||
self.remote_control_target.pair_status_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 status response from `{}`: {err}",
|
||||
self.remote_control_target.pair_status_url
|
||||
))
|
||||
})?;
|
||||
let body_preview = preview_remote_control_response_body(&body);
|
||||
if !status.is_success() {
|
||||
let error_kind = match status.as_u16() {
|
||||
401 | 403 => ErrorKind::PermissionDenied,
|
||||
404 | 410 => ErrorKind::InvalidInput,
|
||||
_ => ErrorKind::Other,
|
||||
};
|
||||
return Err(io::Error::new(
|
||||
error_kind,
|
||||
format!(
|
||||
"remote control pairing status failed at `{}`: HTTP {status}, {}, body: {body_preview}",
|
||||
self.remote_control_target.pair_status_url,
|
||||
format_headers(&headers)
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let response = serde_json::from_slice::<BackendRemoteControlPairingStatusResponse>(&body)
|
||||
.map_err(|err| {
|
||||
io::Error::other(format!(
|
||||
"failed to parse remote control pairing status response from `{}`: HTTP {status}, {}, body: {body_preview}, decode error: {err}",
|
||||
self.remote_control_target.pair_status_url,
|
||||
format_headers(&headers)
|
||||
))
|
||||
})?;
|
||||
Ok(RemoteControlPairingStatusResponse {
|
||||
claimed: response.claimed,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn should_refresh_server_token(&self) -> bool {
|
||||
self.remote_control_token.is_none()
|
||||
|| self.expires_at.is_none_or(|expires_at| {
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::transport::remote_control::websocket::RemoteControlStatusPublisher;
|
||||
use crate::transport::remote_control::websocket::RemoteControlWebsocket;
|
||||
|
||||
pub use self::protocol::ClientId;
|
||||
use self::protocol::RemoteControlPairingStatusCode;
|
||||
use self::protocol::ServerEvent;
|
||||
use self::protocol::StreamId;
|
||||
use self::protocol::normalize_remote_control_url;
|
||||
@@ -31,6 +32,8 @@ use codex_app_server_protocol::RemoteControlClientsRevokeResponse;
|
||||
use codex_app_server_protocol::RemoteControlConnectionStatus;
|
||||
use codex_app_server_protocol::RemoteControlPairingStartParams;
|
||||
use codex_app_server_protocol::RemoteControlPairingStartResponse;
|
||||
use codex_app_server_protocol::RemoteControlPairingStatusParams;
|
||||
use codex_app_server_protocol::RemoteControlPairingStatusResponse;
|
||||
use codex_app_server_protocol::RemoteControlStatusChangedNotification;
|
||||
use codex_login::AuthManager;
|
||||
use codex_state::StateRuntime;
|
||||
@@ -391,6 +394,89 @@ impl RemoteControlHandle {
|
||||
Ok(self.pairing_persistence_key.borrow().clone())
|
||||
}
|
||||
|
||||
pub async fn pairing_status(
|
||||
&self,
|
||||
params: RemoteControlPairingStatusParams,
|
||||
) -> io::Result<RemoteControlPairingStatusResponse> {
|
||||
if !*self.enabled_tx.borrow() {
|
||||
return Err(Self::pairing_disabled_error());
|
||||
}
|
||||
let mut auth = load_remote_control_auth(&self.auth_manager)
|
||||
.await
|
||||
.map_err(|_| pairing_unavailable_error())?;
|
||||
let app_server_client_name = self.pairing_persistence_key.borrow().clone();
|
||||
let app_server_client_name = app_server_client_name.as_deref();
|
||||
let mut current_enrollment = self.current_enrollment.lock().await;
|
||||
let mut enrollment = current_enrollment
|
||||
.as_ref()
|
||||
.filter(|enrollment| enrollment.account_id == auth.account_id)
|
||||
.cloned()
|
||||
.ok_or_else(pairing_unavailable_error)?;
|
||||
let installation_id = self.status().installation_id;
|
||||
if enrollment.should_refresh_server_token() {
|
||||
refresh_pairing_enrollment(
|
||||
&mut current_enrollment,
|
||||
self.state_db.as_deref(),
|
||||
app_server_client_name,
|
||||
&self.auth_manager,
|
||||
&mut auth,
|
||||
&installation_id,
|
||||
&mut enrollment,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let status_code = remote_control_pairing_status_code(¶ms)?;
|
||||
let pairing_status_request =
|
||||
|| protocol::RemoteControlPairingStatusRequest::from(status_code.clone());
|
||||
let pairing_status_response =
|
||||
match enrollment.pairing_status(pairing_status_request()).await {
|
||||
Err(err) if err.kind() == io::ErrorKind::PermissionDenied => {
|
||||
clear_pairing_server_token(&mut current_enrollment, &mut enrollment)?;
|
||||
refresh_pairing_enrollment(
|
||||
&mut current_enrollment,
|
||||
self.state_db.as_deref(),
|
||||
app_server_client_name,
|
||||
&self.auth_manager,
|
||||
&mut auth,
|
||||
&installation_id,
|
||||
&mut enrollment,
|
||||
)
|
||||
.await?;
|
||||
enrollment.pairing_status(pairing_status_request()).await
|
||||
}
|
||||
pairing_status_response => pairing_status_response,
|
||||
};
|
||||
if let Err(err) = &pairing_status_response {
|
||||
match err.kind() {
|
||||
io::ErrorKind::NotFound => {
|
||||
clear_pairing_enrollment(
|
||||
&mut current_enrollment,
|
||||
self.state_db.as_deref(),
|
||||
app_server_client_name,
|
||||
&enrollment,
|
||||
)
|
||||
.await;
|
||||
return Err(pairing_unavailable_error());
|
||||
}
|
||||
io::ErrorKind::PermissionDenied => {
|
||||
clear_pairing_server_token(&mut current_enrollment, &mut enrollment)?;
|
||||
return Err(pairing_unavailable_error());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if !*self.enabled_tx.borrow() {
|
||||
return Err(Self::pairing_disabled_error());
|
||||
}
|
||||
let current_auth = load_remote_control_auth(&self.auth_manager)
|
||||
.await
|
||||
.map_err(|_| pairing_unavailable_error())?;
|
||||
if current_auth.account_id != auth.account_id {
|
||||
return Err(pairing_unavailable_error());
|
||||
}
|
||||
pairing_status_response
|
||||
}
|
||||
|
||||
pub async fn list_clients(
|
||||
&self,
|
||||
params: RemoteControlClientsListParams,
|
||||
@@ -407,6 +493,13 @@ impl RemoteControlHandle {
|
||||
.await
|
||||
}
|
||||
|
||||
fn pairing_disabled_error() -> io::Error {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"remote control pairing requires remote control to be enabled",
|
||||
)
|
||||
}
|
||||
|
||||
fn publish_status(
|
||||
&self,
|
||||
connection_status: RemoteControlConnectionStatus,
|
||||
@@ -464,6 +557,27 @@ async fn enroll_pairing_server(
|
||||
enroll_remote_control_server(remote_control_target, auth, installation_id, server_name).await
|
||||
}
|
||||
|
||||
fn remote_control_pairing_status_code(
|
||||
params: &RemoteControlPairingStatusParams,
|
||||
) -> io::Result<RemoteControlPairingStatusCode> {
|
||||
match (¶ms.pairing_code, ¶ms.manual_pairing_code) {
|
||||
(Some(pairing_code), None) => Ok(RemoteControlPairingStatusCode::PairingCode(
|
||||
pairing_code.clone(),
|
||||
)),
|
||||
(None, Some(manual_pairing_code)) => Ok(RemoteControlPairingStatusCode::ManualPairingCode(
|
||||
manual_pairing_code.clone(),
|
||||
)),
|
||||
(Some(_), Some(_)) => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"remote control pairing status accepts either pairingCode or manualPairingCode, not both",
|
||||
)),
|
||||
(None, None) => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"remote control pairing status requires pairingCode or manualPairingCode",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_pairing_enrollment(
|
||||
current_enrollment: &mut Option<RemoteControlEnrollment>,
|
||||
state_db: Option<&StateRuntime>,
|
||||
|
||||
@@ -13,6 +13,7 @@ pub(super) struct RemoteControlTarget {
|
||||
pub(super) enroll_url: String,
|
||||
pub(super) refresh_url: String,
|
||||
pub(super) pair_url: String,
|
||||
pub(super) pair_status_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -52,6 +53,40 @@ pub(super) struct StartRemoteControlPairingResponse {
|
||||
pub(super) expires_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(super) struct RemoteControlPairingStatusRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(super) pairing_code: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(super) manual_pairing_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) enum RemoteControlPairingStatusCode {
|
||||
PairingCode(String),
|
||||
ManualPairingCode(String),
|
||||
}
|
||||
|
||||
impl From<RemoteControlPairingStatusCode> for RemoteControlPairingStatusRequest {
|
||||
fn from(code: RemoteControlPairingStatusCode) -> Self {
|
||||
match code {
|
||||
RemoteControlPairingStatusCode::PairingCode(pairing_code) => Self {
|
||||
pairing_code: Some(pairing_code),
|
||||
manual_pairing_code: None,
|
||||
},
|
||||
RemoteControlPairingStatusCode::ManualPairingCode(manual_pairing_code) => Self {
|
||||
pairing_code: None,
|
||||
manual_pairing_code: Some(manual_pairing_code),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct RemoteControlPairingStatusResponse {
|
||||
pub(super) claimed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ClientId(pub String);
|
||||
@@ -194,6 +229,9 @@ pub(super) fn normalize_remote_control_url(
|
||||
let pair_url = remote_control_url
|
||||
.join("wham/remote/control/server/pair")
|
||||
.map_err(map_url_parse_error)?;
|
||||
let pair_status_url = remote_control_url
|
||||
.join("wham/remote/control/server/pair/status")
|
||||
.map_err(map_url_parse_error)?;
|
||||
let mut websocket_url = remote_control_url
|
||||
.join("wham/remote/control/server")
|
||||
.map_err(map_url_parse_error)?;
|
||||
@@ -215,6 +253,7 @@ pub(super) fn normalize_remote_control_url(
|
||||
enroll_url: enroll_url.to_string(),
|
||||
refresh_url: refresh_url.to_string(),
|
||||
pair_url: pair_url.to_string(),
|
||||
pair_status_url: pair_status_url.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -269,6 +308,9 @@ mod tests {
|
||||
.to_string(),
|
||||
pair_url: "https://chatgpt.com/backend-api/wham/remote/control/server/pair"
|
||||
.to_string(),
|
||||
pair_status_url:
|
||||
"https://chatgpt.com/backend-api/wham/remote/control/server/pair/status"
|
||||
.to_string(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -287,6 +329,9 @@ mod tests {
|
||||
pair_url:
|
||||
"https://api.chatgpt-staging.com/backend-api/wham/remote/control/server/pair"
|
||||
.to_string(),
|
||||
pair_status_url:
|
||||
"https://api.chatgpt-staging.com/backend-api/wham/remote/control/server/pair/status"
|
||||
.to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -305,6 +350,9 @@ mod tests {
|
||||
.to_string(),
|
||||
pair_url: "http://localhost:8080/backend-api/wham/remote/control/server/pair"
|
||||
.to_string(),
|
||||
pair_status_url:
|
||||
"http://localhost:8080/backend-api/wham/remote/control/server/pair/status"
|
||||
.to_string(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -320,6 +368,9 @@ mod tests {
|
||||
.to_string(),
|
||||
pair_url: "https://localhost:8443/backend-api/wham/remote/control/server/pair"
|
||||
.to_string(),
|
||||
pair_status_url:
|
||||
"https://localhost:8443/backend-api/wham/remote/control/server/pair/status"
|
||||
.to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::RemoteControlConnectionStatus;
|
||||
use codex_app_server_protocol::RemoteControlPairingStartParams;
|
||||
use codex_app_server_protocol::RemoteControlPairingStatusParams;
|
||||
use codex_app_server_protocol::RemoteControlStatusChangedNotification;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::super::protocol::RemoteControlPairingStatusRequest;
|
||||
use super::super::protocol::StartRemoteControlPairingRequest;
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::io;
|
||||
|
||||
fn remote_control_enrollment(
|
||||
remote_control_url: &str,
|
||||
@@ -66,6 +68,36 @@ async fn pairing_response_error(body: serde_json::Value) -> String {
|
||||
err.to_string()
|
||||
}
|
||||
|
||||
async fn pairing_status_error(status: &'static str, body: &'static str) -> (io::Error, String) {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("listener should bind");
|
||||
let remote_control_url = remote_control_url_for_listener(&listener);
|
||||
let expected_status_url = normalize_remote_control_url(&remote_control_url)
|
||||
.expect("target should normalize")
|
||||
.pair_status_url;
|
||||
let server_task = tokio::spawn(async move {
|
||||
let status_request = accept_http_request(&listener).await;
|
||||
respond_with_status_and_headers(
|
||||
status_request.stream,
|
||||
status,
|
||||
&[("x-request-id", "request-123"), ("cf-ray", "ray-123")],
|
||||
body,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
let err = remote_control_enrollment(&remote_control_url, "remote-control-token")
|
||||
.pairing_status(RemoteControlPairingStatusRequest {
|
||||
pairing_code: Some("pairing-code".to_string()),
|
||||
manual_pairing_code: None,
|
||||
})
|
||||
.await
|
||||
.expect_err("pairing status should fail");
|
||||
server_task.await.expect("server task should finish");
|
||||
(err, expected_status_url)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_handle_starts_pairing_before_websocket_connects() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
@@ -156,6 +188,190 @@ async fn remote_control_handle_starts_pairing_before_websocket_connects() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_pairing_status_returns_pending() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("listener should bind");
|
||||
let remote_control_url = remote_control_url_for_listener(&listener);
|
||||
let server_task = tokio::spawn(async move {
|
||||
let status_request = accept_http_request(&listener).await;
|
||||
assert_eq!(
|
||||
status_request.request_line,
|
||||
"POST /backend-api/wham/remote/control/server/pair/status HTTP/1.1"
|
||||
);
|
||||
assert_eq!(
|
||||
status_request.headers.get("authorization"),
|
||||
Some(&"Bearer remote-control-token".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&status_request.body)
|
||||
.expect("status request body should deserialize"),
|
||||
json!({ "pairing_code": "pairing-code" })
|
||||
);
|
||||
respond_with_json(status_request.stream, json!({ "claimed": false })).await;
|
||||
});
|
||||
|
||||
let response = remote_control_enrollment(&remote_control_url, "remote-control-token")
|
||||
.pairing_status(RemoteControlPairingStatusRequest {
|
||||
pairing_code: Some("pairing-code".to_string()),
|
||||
manual_pairing_code: None,
|
||||
})
|
||||
.await
|
||||
.expect("pairing status should succeed");
|
||||
server_task.await.expect("server task should finish");
|
||||
|
||||
assert!(!response.claimed);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_pairing_status_accepts_manual_pairing_code() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("listener should bind");
|
||||
let remote_control_url = remote_control_url_for_listener(&listener);
|
||||
let server_task = tokio::spawn(async move {
|
||||
let status_request = accept_http_request(&listener).await;
|
||||
assert_eq!(
|
||||
status_request.request_line,
|
||||
"POST /backend-api/wham/remote/control/server/pair/status HTTP/1.1"
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&status_request.body)
|
||||
.expect("status request body should deserialize"),
|
||||
json!({ "manual_pairing_code": "ABCD-EFGH" })
|
||||
);
|
||||
respond_with_json(status_request.stream, json!({ "claimed": false })).await;
|
||||
});
|
||||
|
||||
let response = remote_control_enrollment(&remote_control_url, "remote-control-token")
|
||||
.pairing_status(RemoteControlPairingStatusRequest {
|
||||
pairing_code: None,
|
||||
manual_pairing_code: Some("ABCD-EFGH".to_string()),
|
||||
})
|
||||
.await
|
||||
.expect("pairing status should succeed");
|
||||
server_task.await.expect("server task should finish");
|
||||
|
||||
assert!(!response.claimed);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_pairing_status_returns_claimed() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("listener should bind");
|
||||
let remote_control_url = remote_control_url_for_listener(&listener);
|
||||
let server_task = tokio::spawn(async move {
|
||||
let status_request = accept_http_request(&listener).await;
|
||||
assert_eq!(
|
||||
status_request.request_line,
|
||||
"POST /backend-api/wham/remote/control/server/pair/status HTTP/1.1"
|
||||
);
|
||||
respond_with_json(status_request.stream, json!({ "claimed": true })).await;
|
||||
});
|
||||
|
||||
let response = remote_control_enrollment(&remote_control_url, "remote-control-token")
|
||||
.pairing_status(RemoteControlPairingStatusRequest {
|
||||
pairing_code: Some("pairing-code".to_string()),
|
||||
manual_pairing_code: None,
|
||||
})
|
||||
.await
|
||||
.expect("pairing status should succeed");
|
||||
server_task.await.expect("server task should finish");
|
||||
|
||||
assert!(response.claimed);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_handle_refreshes_after_pairing_status_auth_failure() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("listener should bind");
|
||||
let remote_control_url = remote_control_url_for_listener(&listener);
|
||||
let server_task = tokio::spawn(async move {
|
||||
let stale_status_request = accept_http_request(&listener).await;
|
||||
assert_eq!(
|
||||
stale_status_request.request_line,
|
||||
"POST /backend-api/wham/remote/control/server/pair/status HTTP/1.1"
|
||||
);
|
||||
assert_eq!(
|
||||
stale_status_request.headers.get("authorization"),
|
||||
Some(&format!("Bearer {TEST_REMOTE_CONTROL_SERVER_TOKEN}"))
|
||||
);
|
||||
respond_with_status(stale_status_request.stream, "401 Unauthorized", "").await;
|
||||
|
||||
let refresh_request = accept_http_request(&listener).await;
|
||||
assert_eq!(
|
||||
refresh_request.request_line,
|
||||
"POST /backend-api/wham/remote/control/server/refresh HTTP/1.1"
|
||||
);
|
||||
respond_with_json(
|
||||
refresh_request.stream,
|
||||
remote_control_server_token_response(
|
||||
"srv_e_test",
|
||||
"env_test",
|
||||
TEST_REFRESHED_REMOTE_CONTROL_SERVER_TOKEN,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let refreshed_status_request = accept_http_request(&listener).await;
|
||||
assert_eq!(
|
||||
refreshed_status_request.request_line,
|
||||
"POST /backend-api/wham/remote/control/server/pair/status HTTP/1.1"
|
||||
);
|
||||
assert_eq!(
|
||||
refreshed_status_request.headers.get("authorization"),
|
||||
Some(&format!(
|
||||
"Bearer {TEST_REFRESHED_REMOTE_CONTROL_SERVER_TOKEN}"
|
||||
))
|
||||
);
|
||||
respond_with_json(refreshed_status_request.stream, json!({ "claimed": true })).await;
|
||||
});
|
||||
let remote_handle = remote_control_handle_with_current_enrollment(
|
||||
&remote_control_url,
|
||||
remote_control_auth_manager(),
|
||||
);
|
||||
|
||||
let response = remote_handle
|
||||
.pairing_status(RemoteControlPairingStatusParams {
|
||||
pairing_code: Some("pairing-code".to_string()),
|
||||
manual_pairing_code: None,
|
||||
})
|
||||
.await
|
||||
.expect("pairing status should refresh after server token auth failure");
|
||||
server_task.await.expect("server task should finish");
|
||||
|
||||
assert!(response.claimed);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_pairing_status_maps_user_actionable_backend_errors() {
|
||||
for (status, expected_kind) in [
|
||||
("403 Forbidden", io::ErrorKind::PermissionDenied),
|
||||
("404 Not Found", io::ErrorKind::InvalidInput),
|
||||
("410 Gone", io::ErrorKind::InvalidInput),
|
||||
] {
|
||||
let (err, _expected_status_url) = pairing_status_error(status, "not available").await;
|
||||
assert_eq!(err.kind(), expected_kind);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_pairing_status_preserves_decode_error_context() {
|
||||
let (err, expected_status_url) = pairing_status_error("200 OK", "{").await;
|
||||
let err = err.to_string();
|
||||
|
||||
assert!(err.contains(&format!(
|
||||
"failed to parse remote control pairing status response from `{expected_status_url}`: HTTP 200 OK"
|
||||
)));
|
||||
assert!(err.contains("request-id: request-123"));
|
||||
assert!(err.contains("cf-ray: ray-123"));
|
||||
assert!(err.contains("body: {"));
|
||||
assert!(err.contains("decode error:"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_handle_refreshes_after_pairing_auth_failure() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
|
||||
Reference in New Issue
Block a user