Codex Handbook
app-server-transport/src/transport/remote_control/tests/clients_tests.rs 371 lines
use super::super::clients::list_remote_control_clients;use super::super::clients::revoke_remote_control_client;use super::*;use codex_app_server_protocol::RemoteControlClient;use codex_app_server_protocol::RemoteControlClientsListOrder;use codex_app_server_protocol::RemoteControlClientsListParams;use codex_app_server_protocol::RemoteControlClientsListResponse;use codex_app_server_protocol::RemoteControlClientsRevokeParams;use codex_app_server_protocol::RemoteControlClientsRevokeResponse;use codex_login::AuthKeyringBackendKind;use pretty_assertions::assert_eq;fn client_management_handle(    remote_control_url: String,    auth_manager: Arc<AuthManager>,) -> RemoteControlHandle {    let desired_state_tx = watch::channel(RemoteControlDesiredState::Disabled).0;    let (status_tx, _status_rx) = watch::channel(RemoteControlStatusChangedNotification {        status: RemoteControlConnectionStatus::Disabled,        server_name: test_server_name(),        installation_id: TEST_INSTALLATION_ID.to_string(),        environment_id: None,    });    RemoteControlHandle {        policy: RemoteControlPolicy::Allowed,        desired_state_tx: Arc::new(desired_state_tx),        desired_state_rpc_lock: Arc::new(Semaphore::new(1)),        desired_state_persistence_lock: Arc::new(Semaphore::new(1)),        status_tx: Arc::new(status_tx),        state_db: None,        remote_control_url,        current_enrollment: Arc::new(RemoteControlEnrollmentState::new(/*enrollment*/ None)),        pairing_persistence_key: watch::channel(None).0,        pairing_persistence_key_required: false,        auth_manager,    }}fn empty_client_list() -> serde_json::Value {    json!({        "items": [],        "cursor": null,    })}#[tokio::test]async fn remote_control_handle_lists_clients_while_disabled() {    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 request = accept_http_request(&listener).await;        assert_eq!(            request.request_line,            "GET /backend-api/wham/remote/control/environments/env%20%2F%3F/clients?cursor=cursor+%2F%3F&limit=10&order=asc HTTP/1.1"        );        assert_eq!(            request.headers.get("authorization"),            Some(&"Bearer Access Token".to_string())        );        assert_eq!(            request.headers.get(REMOTE_CONTROL_ACCOUNT_ID_HEADER),            Some(&"account_id".to_string())        );        respond_with_json(            request.stream,            json!({                "items": [{                    "client_id": "client-123",                    "account_user_id": "user-123",                    "enrollment_status": "enrolled_device_key",                    "display_name": "Anton Phone",                    "device_type": "phone",                    "platform": "ios",                    "os_version": "19.0",                    "device_model": "iPhone",                    "app_version": "1.2.3",                    "last_seen_at": "2026-03-05T07:00:00Z",                    "last_seen_city": "San Francisco",                }],                "cursor": "next-cursor",            }),        )        .await;    });    let handle = client_management_handle(remote_control_url, remote_control_auth_manager());    let response = handle        .list_clients(RemoteControlClientsListParams {            environment_id: "env /?".to_string(),            cursor: Some("cursor /?".to_string()),            limit: Some(10),            order: Some(RemoteControlClientsListOrder::Asc),        })        .await        .expect("client list should succeed while remote control is disabled");    server_task.await.expect("server task should finish");    assert_eq!(        response,        RemoteControlClientsListResponse {            data: vec![RemoteControlClient {                client_id: "client-123".to_string(),                display_name: Some("Anton Phone".to_string()),                device_type: Some("phone".to_string()),                platform: Some("ios".to_string()),                os_version: Some("19.0".to_string()),                device_model: Some("iPhone".to_string()),                app_version: Some("1.2.3".to_string()),                last_seen_at: Some(1_772_694_000),            }],            next_cursor: Some("next-cursor".to_string()),        }    );}#[tokio::test]async fn remote_control_handle_revokes_client_while_disabled() {    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 request = accept_http_request(&listener).await;        assert_eq!(            request.request_line,            "DELETE /backend-api/wham/remote/control/environments/env%20%2F%3F/clients/client%20%2F%3F HTTP/1.1"        );        respond_with_status(request.stream, "204 No Content", "").await;    });    let handle = client_management_handle(remote_control_url, remote_control_auth_manager());    let response = handle        .revoke_client(RemoteControlClientsRevokeParams {            environment_id: "env /?".to_string(),            client_id: "client /?".to_string(),        })        .await        .expect("client revoke should succeed while remote control is disabled");    server_task.await.expect("server task should finish");    assert_eq!(response, RemoteControlClientsRevokeResponse {});}#[tokio::test]async fn list_remote_control_clients_recovers_auth_after_unauthorized() {    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_request = accept_http_request(&listener).await;        assert_eq!(            stale_request.headers.get("authorization"),            Some(&"Bearer stale-token".to_string())        );        respond_with_status(stale_request.stream, "401 Unauthorized", "").await;        let recovered_request = accept_http_request(&listener).await;        assert_eq!(            recovered_request.headers.get("authorization"),            Some(&"Bearer fresh-token".to_string())        );        respond_with_json(recovered_request.stream, empty_client_list()).await;    });    let codex_home = TempDir::new().expect("temp dir should create");    let mut stale_auth = remote_control_auth_dot_json(Some("account_id"));    stale_auth        .tokens        .as_mut()        .expect("stale auth should include tokens")        .access_token = "stale-token".to_string();    save_auth(        codex_home.path(),        &stale_auth,        AuthCredentialsStoreMode::File,        AuthKeyringBackendKind::default(),    )    .expect("stale auth should save");    let auth_manager = AuthManager::shared(        codex_home.path().to_path_buf(),        /*enable_codex_api_key_env*/ false,        AuthCredentialsStoreMode::File,        /*chatgpt_base_url*/ None,        AuthKeyringBackendKind::default(),    )    .await;    let mut fresh_auth = remote_control_auth_dot_json(Some("account_id"));    fresh_auth        .tokens        .as_mut()        .expect("fresh auth should include tokens")        .access_token = "fresh-token".to_string();    save_auth(        codex_home.path(),        &fresh_auth,        AuthCredentialsStoreMode::File,        AuthKeyringBackendKind::default(),    )    .expect("fresh auth should save");    let response = list_remote_control_clients(        &remote_control_url,        &auth_manager,        RemoteControlClientsListParams {            environment_id: "env-123".to_string(),            ..Default::default()        },    )    .await    .expect("client list should recover auth");    server_task.await.expect("server task should finish");    assert_eq!(        response,        RemoteControlClientsListResponse {            data: Vec::new(),            next_cursor: None,        }    );}#[tokio::test]async fn list_remote_control_clients_retries_unauthorized_only_once() {    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_request = accept_http_request(&listener).await;        assert_eq!(            stale_request.headers.get("authorization"),            Some(&"Bearer stale-token".to_string())        );        respond_with_status(stale_request.stream, "401 Unauthorized", "").await;        let recovered_request = accept_http_request(&listener).await;        assert_eq!(            recovered_request.headers.get("authorization"),            Some(&"Bearer fresh-token".to_string())        );        respond_with_status(recovered_request.stream, "401 Unauthorized", "").await;        assert!(            timeout(Duration::from_millis(100), accept_http_request(&listener))                .await                .is_err()        );    });    let codex_home = TempDir::new().expect("temp dir should create");    let mut stale_auth = remote_control_auth_dot_json(Some("account_id"));    stale_auth        .tokens        .as_mut()        .expect("stale auth should include tokens")        .access_token = "stale-token".to_string();    save_auth(        codex_home.path(),        &stale_auth,        AuthCredentialsStoreMode::File,        AuthKeyringBackendKind::default(),    )    .expect("stale auth should save");    let auth_manager = AuthManager::shared(        codex_home.path().to_path_buf(),        /*enable_codex_api_key_env*/ false,        AuthCredentialsStoreMode::File,        /*chatgpt_base_url*/ None,        AuthKeyringBackendKind::default(),    )    .await;    let mut fresh_auth = remote_control_auth_dot_json(Some("account_id"));    fresh_auth        .tokens        .as_mut()        .expect("fresh auth should include tokens")        .access_token = "fresh-token".to_string();    save_auth(        codex_home.path(),        &fresh_auth,        AuthCredentialsStoreMode::File,        AuthKeyringBackendKind::default(),    )    .expect("fresh auth should save");    let err = list_remote_control_clients(        &remote_control_url,        &auth_manager,        RemoteControlClientsListParams {            environment_id: "env-123".to_string(),            ..Default::default()        },    )    .await    .expect_err("second unauthorized response should fail");    server_task.await.expect("server task should finish");    assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);}#[tokio::test]async fn revoke_remote_control_client_does_not_retry_forbidden() {    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 request = accept_http_request(&listener).await;        respond_with_status_and_headers(            request.stream,            "403 Forbidden",            &[("x-request-id", "request-123"), ("cf-ray", "ray-123")],            "forbidden",        )        .await;    });    let err = revoke_remote_control_client(        &remote_control_url,        &remote_control_auth_manager(),        RemoteControlClientsRevokeParams {            environment_id: "env-123".to_string(),            client_id: "client-123".to_string(),        },    )    .await    .expect_err("forbidden revoke should fail");    server_task.await.expect("server task should finish");    assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);    assert_eq!(        err.to_string(),        format!(            "remote control client revoke failed at `{remote_control_url}wham/remote/control/environments/env-123/clients/client-123`: HTTP 403 Forbidden, request-id: request-123, cf-ray: ray-123, body: forbidden"        )    );}#[tokio::test]async fn list_remote_control_clients_preserves_decode_error_context() {    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 request = accept_http_request(&listener).await;        respond_with_status(request.stream, "200 OK", "{").await;    });    let err = list_remote_control_clients(        &remote_control_url,        &remote_control_auth_manager(),        RemoteControlClientsListParams {            environment_id: "env-123".to_string(),            ..Default::default()        },    )    .await    .expect_err("malformed client list should fail");    server_task.await.expect("server task should finish");    assert!(        err.to_string().contains(            "failed to parse remote control client list response from `http://127.0.0.1:"        )    );    assert!(err.to_string().contains("HTTP 200 OK"));    assert!(err.to_string().contains("body: {"));    assert!(err.to_string().contains("decode error:"));}