//! Integration tests for the channel-relay client and channel. //! //! Uses real HTTP servers on random ports (no mock framework). use axum::{ Json, Router, extract::Query, routing::{get, post}, }; use optimclaw::channels::relay::client::{ChannelEvent, RelayClient}; use secrecy::SecretString; use serde::Deserialize; use tokio::net::TcpListener; /// Start an axum server on a random port, returning the base URL. async fn start_server(app: Router) -> String { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); format!("http://{}", addr) } fn test_client(base_url: &str) -> RelayClient { RelayClient::new( base_url.to_string(), SecretString::from("test-api-key".to_string()), 5, ) .expect("client build") } // ── Signing secret fetch ───────────────────────────────────────────────── #[tokio::test] async fn test_get_signing_secret_returns_decoded_bytes() { let secret_hex = hex::encode([1u8; 32]); let secret_hex_clone = secret_hex.clone(); let app = Router::new().route( "/relay/signing-secret", get(move || { let s = secret_hex_clone.clone(); async move { Json(serde_json::json!({"signing_secret": s})) } }), ); let base_url = start_server(app).await; let client = test_client(&base_url); let secret = client.get_signing_secret("T123").await.unwrap(); assert_eq!(secret, vec![1u8; 32]); } #[tokio::test] async fn test_get_signing_secret_404_returns_error() { let app = Router::new().route( "/relay/signing-secret", get(|| async { (axum::http::StatusCode::NOT_FOUND, "not found") }), ); let base_url = start_server(app).await; let client = test_client(&base_url); let result = client.get_signing_secret("T123").await; assert!(result.is_err()); } #[tokio::test] async fn test_get_signing_secret_invalid_hex_returns_protocol_error() { let app = Router::new().route( "/relay/signing-secret", get(|| async { Json(serde_json::json!({"signing_secret": "not-hex"})) }), ); let base_url = start_server(app).await; let client = test_client(&base_url); let err = client .get_signing_secret("T123") .await .unwrap_err() .to_string(); assert!(err.contains("invalid signing_secret hex"), "got: {err}"); } #[tokio::test] async fn test_get_signing_secret_wrong_length_returns_protocol_error() { let short_secret_hex = hex::encode([7u8; 31]); let app = Router::new().route( "/relay/signing-secret", get(move || { let s = short_secret_hex.clone(); async move { Json(serde_json::json!({"signing_secret": s})) } }), ); let base_url = start_server(app).await; let client = test_client(&base_url); let err = client .get_signing_secret("T123") .await .unwrap_err() .to_string(); assert!(err.contains("expected 32 bytes"), "got: {err}"); } // ── Proxy call ────────────────────────────────────────────────────────── #[derive(Deserialize)] struct ProxyQuery { team_id: String, } #[tokio::test] async fn test_proxy_provider_sends_correct_payload() { let app = Router::new().route( "/proxy/slack/chat.postMessage", post( |Query(q): Query, Json(body): Json| async move { assert_eq!(q.team_id, "T123"); assert_eq!(body["channel"], "C456"); assert_eq!(body["text"], "Hello from test"); Json(serde_json::json!({"ok": true})) }, ), ); let base_url = start_server(app).await; let client = test_client(&base_url); let body = serde_json::json!({ "channel": "C456", "text": "Hello from test", }); let resp = client .proxy_provider("slack", "T123", "chat.postMessage", body) .await .unwrap(); assert_eq!(resp["ok"], true); } // ── List connections ──────────────────────────────────────────────────── #[tokio::test] async fn test_list_connections() { let app = Router::new().route( "/connections", get(|| async { Json(serde_json::json!([ {"provider": "slack", "team_id": "T123", "team_name": "Test Team", "connected": true}, {"provider": "slack", "team_id": "T456", "team_name": "Other", "connected": false}, ])) }), ); let base_url = start_server(app).await; let client = test_client(&base_url); let conns = client.list_connections("inst-1").await.unwrap(); assert_eq!(conns.len(), 2); assert!(conns[0].connected); assert!(!conns[1].connected); } // ── Bearer token auth ──────────────────────────────────────────────────── #[tokio::test] async fn test_bearer_token_sent_in_header() { let app = Router::new().route( "/connections", get(|headers: axum::http::HeaderMap| async move { let auth = headers .get("authorization") .and_then(|v| v.to_str().ok()) .unwrap_or(""); assert_eq!(auth, "Bearer test-api-key"); Json(serde_json::json!([])) }), ); let base_url = start_server(app).await; let client = test_client(&base_url); let _ = client.list_connections("inst-1").await.unwrap(); } // ── Client builder error propagation ──────────────────────────────────── #[test] fn test_relay_client_new_succeeds() { let client = RelayClient::new( "http://localhost:9999".to_string(), SecretString::from("key".to_string()), 30, ); assert!(client.is_ok()); } // ── Channel event field validation ────────────────────────────────────── #[test] fn test_channel_event_missing_fields_detected() { // Event with empty sender_id should be detectable let json = r#"{"event_type": "message", "provider_scope": "T1", "channel_id": "C1", "sender_id": "", "content": "test"}"#; let event: ChannelEvent = serde_json::from_str(json).unwrap(); assert!(event.sender_id.is_empty()); // Event with all fields present let json = r#"{"event_type": "message", "provider_scope": "T1", "channel_id": "C1", "sender_id": "U1", "content": "test"}"#; let event: ChannelEvent = serde_json::from_str(json).unwrap(); assert!(!event.sender_id.is_empty()); assert!(!event.channel_id.is_empty()); assert!(!event.provider_scope.is_empty()); }