Files
MyAiDesk/nomifun-tauri/crates/backend/nomifun-app/tests/mcp_e2e.rs
T
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

235 lines
8.0 KiB
Rust

//! MCP E2E tests beyond CRUD: connection test, agent config discovery, OAuth, auth.
//!
//! Covers test-plan sections 2 (connection test error paths), 3 (agent config discovery),
//! 4 (OAuth status), and 6 (authentication).
mod common;
use axum::http::StatusCode;
use serde_json::json;
use tower::ServiceExt;
use common::{body_json, build_app, get_with_token, json_with_token, setup_and_login};
// ===========================================================================
// CT-3: Connection test — command not found (ENOENT)
// ===========================================================================
#[tokio::test]
async fn connection_test_enoent_command() {
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/mcp/test-connection",
json!({
"name": "enoent-test",
"transport": {
"type": "stdio",
"command": "nonexistent-mcp-command-xyz-12345"
}
}),
&token,
&csrf,
);
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNPROCESSABLE_ENTITY);
let json = body_json(resp).await;
assert!(!json["success"].as_bool().unwrap());
assert_eq!(json["code"], "MCP_COMMAND_NOT_FOUND");
assert_eq!(json["details"]["command"], "nonexistent-mcp-command-xyz-12345");
assert!(!json["error"].as_str().unwrap().is_empty());
}
// ===========================================================================
// CT-4: Connection test — unreachable URL
// ===========================================================================
#[tokio::test]
async fn connection_test_unreachable_url() {
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/mcp/test-connection",
json!({
"name": "unreachable-test",
"transport": {
"type": "http",
"url": "http://127.0.0.1:19999/mcp"
}
}),
&token,
&csrf,
);
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_GATEWAY);
let json = body_json(resp).await;
assert!(!json["success"].as_bool().unwrap());
assert_eq!(json["code"], "MCP_CONNECTION_FAILED");
assert_eq!(json["details"]["transport"], "http");
assert!(!json["error"].as_str().unwrap().is_empty());
}
// ===========================================================================
// AS-1: Get agent configs (may return empty in test env)
// ===========================================================================
#[tokio::test]
async fn get_agent_configs() {
let (mut app, services) = build_app().await;
let (token, _csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let resp = app
.clone()
.oneshot(get_with_token("/api/mcp/agent-configs", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert!(json["success"].as_bool().unwrap());
// In test env, data is an array (may be empty or contain nomifun adapter)
assert!(json["data"].is_array());
}
// ===========================================================================
// OA-1: OAuth check status — unauthenticated server
// ===========================================================================
#[tokio::test]
async fn oauth_check_status_unauthenticated_server() {
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/mcp/oauth/check-status",
json!({ "server_url": "https://unknown-server.example.com" }),
&token,
&csrf,
);
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert!(json["success"].as_bool().unwrap());
assert!(!json["data"]["authenticated"].as_bool().unwrap());
}
// ===========================================================================
// OA-3: Get all authenticated servers (empty at start)
// ===========================================================================
#[tokio::test]
async fn oauth_authenticated_servers_empty() {
let (mut app, services) = build_app().await;
let (token, _csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let resp = app
.clone()
.oneshot(get_with_token("/api/mcp/oauth/authenticated", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert!(json["success"].as_bool().unwrap());
assert_eq!(json["data"], json!([]));
}
// ===========================================================================
// OA-7: Logout from never-authenticated server (idempotent)
// ===========================================================================
#[tokio::test]
async fn oauth_logout_idempotent() {
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/mcp/oauth/logout",
json!({ "server_url": "https://never-authed.example.com" }),
&token,
&csrf,
);
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert!(json["success"].as_bool().unwrap());
}
// ===========================================================================
// AU-1: Unauthenticated access to various MCP endpoints
// ===========================================================================
#[tokio::test]
async fn unauthenticated_get_servers_rejected() {
let (app, _services) = build_app().await;
let req = axum::http::Request::builder()
.method("GET")
.uri("/api/mcp/servers")
.body(axum::body::Body::empty())
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
// CSRF middleware rejects before auth can run
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn unauthenticated_post_server_rejected() {
let (app, _services) = build_app().await;
let req = axum::http::Request::builder()
.method("POST")
.uri("/api/mcp/servers")
.header("content-type", "application/json")
.body(axum::body::Body::from(
serde_json::to_vec(&json!({
"name": "test",
"transport": { "type": "stdio", "command": "npx" }
}))
.unwrap(),
))
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}
// AU-3: Valid token accesses MCP routes successfully
#[tokio::test]
async fn authenticated_access_succeeds() {
let (mut app, services) = build_app().await;
let (token, _csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let resp = app
.clone()
.oneshot(get_with_token("/api/mcp/servers", &token))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
// AU-2: Invalid Bearer token is rejected by auth middleware (403 per API spec)
#[tokio::test]
async fn invalid_token_rejected() {
let (app, _services) = build_app().await;
// GET bypasses CSRF → auth middleware sees invalid Bearer → 403
let req = axum::http::Request::builder()
.method("GET")
.uri("/api/mcp/servers")
.header("authorization", "Bearer invalid-jwt-token-abc123")
.body(axum::body::Body::empty())
.unwrap();
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}