Files
optimclaw/tests/relay_integration.rs
OutBack Dingo 6d9dbbb3b9 fork: rename IronClaw → OptimClaw
Full rename of all identifiers, filenames, and references:
  ironclaw → optimclaw
  IronClaw → OptimClaw
  IRONCLAW → OPTIMCLAW
  ironclaw_common → optimclaw_common
  ironclaw_safety → optimclaw_safety

Upstream: nearai/ironclaw
2026-03-29 06:27:52 +07:00

216 lines
7.1 KiB
Rust

//! 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<ProxyQuery>, Json(body): Json<serde_json::Value>| 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());
}