//! E2E tests for Remote Agent CRUD, connection test, and handshake endpoints. mod common; use axum::http::StatusCode; use serde_json::json; use tower::ServiceExt; use common::{body_json, build_app, delete_with_token, get_with_token, json_with_token, setup_and_login}; // ── Helpers ─────────────────────────────────────────────────────────── fn bearer_agent_body() -> serde_json::Value { json!({ "name": "Test Remote Server", "protocol": "acp", "url": "wss://remote.example.com", "auth_type": "bearer", "auth_token": "my-secret-token-1234", "description": "Production agent" }) } fn openclaw_agent_body() -> serde_json::Value { json!({ "name": "OpenClaw Agent", "protocol": "openclaw", "url": "wss://openclaw.example.com", "auth_type": "none" }) } async fn create_agent(app: &mut axum::Router, token: &str, csrf: &str, body: serde_json::Value) -> serde_json::Value { let req = json_with_token("POST", "/api/remote-agents", body, token, csrf); let resp = app.clone().oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::CREATED); let json = body_json(resp).await; assert_eq!(json["success"], true); json } // ── 1.1 Create Remote Agent ───────────────────────────────────────── #[tokio::test] async fn t1_1_create_bearer_agent() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let json = create_agent(&mut app, &token, &csrf, bearer_agent_body()).await; let data = &json["data"]; assert!(data["id"].as_i64().is_some()); assert_eq!(data["name"], "Test Remote Server"); assert_eq!(data["protocol"], "acp"); assert_eq!(data["url"], "wss://remote.example.com"); assert_eq!(data["auth_type"], "bearer"); // Auth token should be masked assert_eq!(data["auth_token"], "***1234"); assert_eq!(data["status"], "unknown"); assert_eq!(data["description"], "Production agent"); assert!(data["created_at"].as_i64().is_some()); assert!(data["updated_at"].as_i64().is_some()); } #[tokio::test] async fn t1_2_create_openclaw_agent_generates_device_keys() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let json = create_agent(&mut app, &token, &csrf, openclaw_agent_body()).await; let data = &json["data"]; assert_eq!(data["protocol"], "openclaw"); // Device ID and public key should be generated assert!(data["device_id"].as_str().unwrap().starts_with("dev_")); assert!(data["device_public_key"].as_str().is_some()); // Private key should NOT be in the response assert!(data.get("device_private_key").is_none()); } #[tokio::test] async fn t1_3_create_missing_required_fields() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let body = json!({ "name": "test" }); let req = json_with_token("POST", "/api/remote-agents", body, &token, &csrf); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn t1_4_create_unauthenticated() { let (app, _services) = build_app().await; let body = bearer_agent_body(); let req = axum::http::Request::builder() .method("POST") .uri("/api/remote-agents") .header("content-type", "application/json") .body(axum::body::Body::from(serde_json::to_vec(&body).unwrap())) .unwrap(); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::FORBIDDEN); } // ── 1.2 List Remote Agents ────────────────────────────────────────── #[tokio::test] async fn t2_1_list_returns_agents_without_auth_token() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; create_agent(&mut app, &token, &csrf, bearer_agent_body()).await; let req = get_with_token("/api/remote-agents", &token); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let json = body_json(resp).await; let data = json["data"].as_array().unwrap(); assert_eq!(data.len(), 1); // auth_token should NOT appear in list response assert!(data[0].get("auth_token").is_none()); assert_eq!(data[0]["name"], "Test Remote Server"); } #[tokio::test] async fn t2_2_list_empty() { let (mut app, services) = build_app().await; let (token, _csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let req = get_with_token("/api/remote-agents", &token); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let json = body_json(resp).await; let data = json["data"].as_array().unwrap(); assert!(data.is_empty()); } // ── 1.3 Get Single Remote Agent ───────────────────────────────────── #[tokio::test] async fn t3_1_get_single_agent_with_masked_token() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let created = create_agent(&mut app, &token, &csrf, bearer_agent_body()).await; let id = created["data"]["id"].as_i64().unwrap(); let req = get_with_token(&format!("/api/remote-agents/{id}"), &token); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let json = body_json(resp).await; let data = &json["data"]; assert_eq!(data["id"], id); assert_eq!(data["auth_token"], "***1234"); assert_eq!(data["description"], "Production agent"); } #[tokio::test] async fn t3_2_get_nonexistent_agent() { let (mut app, services) = build_app().await; let (token, _csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let req = get_with_token("/api/remote-agents/nonexistent-uuid", &token); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } // ── 1.4 Update Remote Agent ──────────────────────────────────────── #[tokio::test] async fn t4_1_update_name_only() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let created = create_agent(&mut app, &token, &csrf, bearer_agent_body()).await; let id = created["data"]["id"].as_i64().unwrap(); let body = json!({ "name": "Updated Name" }); let req = json_with_token("PUT", &format!("/api/remote-agents/{id}"), body, &token, &csrf); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let json = body_json(resp).await; assert_eq!(json["data"]["name"], "Updated Name"); // Other fields preserved assert_eq!(json["data"]["protocol"], "acp"); assert_eq!(json["data"]["url"], "wss://remote.example.com"); } #[tokio::test] async fn t4_2_update_multiple_fields() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let created = create_agent(&mut app, &token, &csrf, bearer_agent_body()).await; let id = created["data"]["id"].as_i64().unwrap(); let body = json!({ "name": "Updated", "url": "wss://new-url.example.com", "auth_token": "new-super-secret-token" }); let req = json_with_token("PUT", &format!("/api/remote-agents/{id}"), body, &token, &csrf); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let json = body_json(resp).await; assert_eq!(json["data"]["name"], "Updated"); assert_eq!(json["data"]["url"], "wss://new-url.example.com"); assert_eq!(json["data"]["auth_token"], "***oken"); } #[tokio::test] async fn t4_3_update_nonexistent_agent() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let body = json!({ "name": "Doesn't Matter" }); let req = json_with_token("PUT", "/api/remote-agents/nonexistent-uuid", body, &token, &csrf); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } // ── 1.5 Delete Remote Agent ──────────────────────────────────────── #[tokio::test] async fn t5_1_delete_agent() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let created = create_agent(&mut app, &token, &csrf, bearer_agent_body()).await; let id = created["data"]["id"].as_i64().unwrap(); let req = delete_with_token(&format!("/api/remote-agents/{id}"), &token, &csrf); let resp = app.clone().oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // Verify it's gone let req = get_with_token(&format!("/api/remote-agents/{id}"), &token); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn t5_2_delete_nonexistent_agent() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let req = delete_with_token("/api/remote-agents/nonexistent-uuid", &token, &csrf); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } // ── 1.6 Connection Test ───────────────────────────────────────────── #[tokio::test] async fn t6_1_test_connection_invalid_protocol() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let body = json!({ "url": "http://example.com", "auth_type": "bearer" }); let req = json_with_token("POST", "/api/remote-agents/test-connection", body, &token, &csrf); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let json = body_json(resp).await; assert_eq!(json["success"], false); } #[tokio::test] async fn t6_2_test_connection_unauthenticated() { let (app, _services) = build_app().await; let body = json!({ "url": "wss://remote.example.com" }); let req = axum::http::Request::builder() .method("POST") .uri("/api/remote-agents/test-connection") .header("content-type", "application/json") .body(axum::body::Body::from(serde_json::to_vec(&body).unwrap())) .unwrap(); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::FORBIDDEN); } // ── 1.7 Handshake ─────────────────────────────────────────────────── #[tokio::test] async fn t7_1_handshake_non_openclaw_protocol() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let created = create_agent(&mut app, &token, &csrf, bearer_agent_body()).await; let id = created["data"]["id"].as_i64().unwrap(); let req = json_with_token( "POST", &format!("/api/remote-agents/{id}/handshake"), json!({}), &token, &csrf, ); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn t7_2_handshake_nonexistent_agent() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; let req = json_with_token( "POST", "/api/remote-agents/nonexistent-uuid/handshake", json!({}), &token, &csrf, ); let resp = app.oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } // ── Full CRUD lifecycle ───────────────────────────────────────────── #[tokio::test] async fn t8_full_crud_lifecycle() { let (mut app, services) = build_app().await; let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await; // Create let created = create_agent(&mut app, &token, &csrf, bearer_agent_body()).await; let id = created["data"]["id"].as_i64().unwrap(); // Read list let req = get_with_token("/api/remote-agents", &token); let resp = app.clone().oneshot(req).await.unwrap(); let json = body_json(resp).await; assert_eq!(json["data"].as_array().unwrap().len(), 1); // Read single let req = get_with_token(&format!("/api/remote-agents/{id}"), &token); let resp = app.clone().oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // Update let body = json!({ "name": "Renamed Server", "description": "Updated desc" }); let req = json_with_token("PUT", &format!("/api/remote-agents/{id}"), body, &token, &csrf); let resp = app.clone().oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); let json = body_json(resp).await; assert_eq!(json["data"]["name"], "Renamed Server"); assert_eq!(json["data"]["description"], "Updated desc"); // Delete let req = delete_with_token(&format!("/api/remote-agents/{id}"), &token, &csrf); let resp = app.clone().oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); // Verify deleted let req = get_with_token("/api/remote-agents", &token); let resp = app.oneshot(req).await.unwrap(); let json = body_json(resp).await; assert!(json["data"].as_array().unwrap().is_empty()); }