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

604 lines
19 KiB
Rust

//! Black-box integration tests for protocol detection endpoint.
//!
//! Uses `wiremock` to mock remote API responses and tests the full
//! HTTP flow: request -> handler -> remote API probe -> response.
use std::sync::Arc;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use serde_json::json;
use tower::ServiceExt;
use wiremock::matchers::{header, method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
use nomifun_db::{
SqliteBrandingConfigRepository, SqliteClientPreferenceRepository, SqliteProviderRepository,
SqliteSettingsRepository, SqliteSystemConfigRepository, init_database_memory,
};
use nomifun_system::{
ClientPrefService, ModelFetchService, ProtocolDetectionService, ProviderService, SettingsService,
SystemRouterState, VersionCheckService, system_routes,
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const TEST_KEY: [u8; 32] = [0x42; 32];
fn build_state(db: &nomifun_db::Database) -> SystemRouterState {
let provider_repo = Arc::new(SqliteProviderRepository::new(db.pool().clone()));
let http_client = reqwest::Client::new();
let system_config_repo = Arc::new(SqliteSystemConfigRepository::new(db.pool().clone()));
let branding_config_repo = Arc::new(SqliteBrandingConfigRepository::new(db.pool().clone()));
SystemRouterState {
settings_service: SettingsService::new(Arc::new(SqliteSettingsRepository::new(db.pool().clone()))),
client_pref_service: ClientPrefService::new(Arc::new(SqliteClientPreferenceRepository::new(db.pool().clone()))),
provider_service: ProviderService::new(provider_repo.clone(), TEST_KEY),
model_fetch_service: ModelFetchService::new(provider_repo, TEST_KEY, http_client.clone()),
protocol_detection_service: ProtocolDetectionService::new(http_client.clone()),
version_check_service: VersionCheckService::new(http_client, "0.1.0".to_owned()),
data_dir: std::env::temp_dir(),
system_config_repo,
branding_config_repo,
}
}
async fn setup() -> axum::Router {
let db = init_database_memory().await.unwrap();
let state = build_state(&db);
system_routes(state)
}
async fn detect(router: &axum::Router, body: serde_json::Value) -> (StatusCode, serde_json::Value) {
let req = Request::builder()
.method("POST")
.uri("/api/providers/detect-protocol")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&body).unwrap()))
.unwrap();
let resp = router.clone().oneshot(req).await.unwrap();
let status = resp.status();
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
(status, json)
}
// ---------------------------------------------------------------------------
// Input validation
// ---------------------------------------------------------------------------
#[tokio::test]
async fn detect_protocol_missing_base_url() {
let router = setup().await;
let (status, json) = detect(&router, json!({"api_key": "sk-xxx"})).await;
assert_eq!(status, StatusCode::BAD_REQUEST);
assert!(!json["success"].as_bool().unwrap());
}
#[tokio::test]
async fn detect_protocol_missing_api_key() {
let router = setup().await;
let (status, json) = detect(&router, json!({"base_url": "https://example.com"})).await;
assert_eq!(status, StatusCode::BAD_REQUEST);
assert!(!json["success"].as_bool().unwrap());
}
#[tokio::test]
async fn detect_protocol_empty_base_url() {
let router = setup().await;
let (status, _) = detect(&router, json!({"base_url": " ", "api_key": "sk-test"})).await;
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn detect_protocol_empty_api_key() {
let router = setup().await;
let (status, _) = detect(&router, json!({"base_url": "https://example.com", "api_key": " "})).await;
assert_eq!(status, StatusCode::BAD_REQUEST);
}
// ---------------------------------------------------------------------------
// OpenAI detection with mock server
// ---------------------------------------------------------------------------
#[tokio::test]
async fn detect_openai_protocol_success() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/models"))
.and(header("Authorization", "Bearer sk-test-key"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [
{"id": "gpt-4"},
{"id": "gpt-3.5-turbo"}
]
})))
.mount(&mock_server)
.await;
let router = setup().await;
let (status, json) = detect(
&router,
json!({
"base_url": mock_server.uri(),
"api_key": "sk-test-key"
}),
)
.await;
assert_eq!(status, StatusCode::OK);
assert!(json["success"].as_bool().unwrap());
let data = &json["data"];
assert_eq!(data["protocol"], "openai");
assert!(data["confidence"].as_u64().unwrap() > 0);
let models = data["models"].as_array().unwrap();
assert!(models.contains(&json!("gpt-4")));
assert!(models.contains(&json!("gpt-3.5-turbo")));
assert_eq!(data["suggestion"]["type"], "none");
}
// ---------------------------------------------------------------------------
// Anthropic detection with mock server
// ---------------------------------------------------------------------------
#[tokio::test]
async fn detect_anthropic_protocol_success() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/models"))
.and(header("x-api-key", "sk-ant-test-key"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [
{"id": "claude-sonnet-4-20250514"},
{"id": "claude-opus-4-20250514"}
]
})))
.mount(&mock_server)
.await;
let router = setup().await;
let (status, json) = detect(
&router,
json!({
"base_url": mock_server.uri(),
"api_key": "sk-ant-test-key",
"preferred_protocol": "anthropic"
}),
)
.await;
assert_eq!(status, StatusCode::OK);
let data = &json["data"];
assert_eq!(data["protocol"], "anthropic");
assert!(data["confidence"].as_u64().unwrap() >= 90);
let models = data["models"].as_array().unwrap();
assert!(models.contains(&json!("claude-sonnet-4-20250514")));
}
// ---------------------------------------------------------------------------
// Gemini detection with mock server
// ---------------------------------------------------------------------------
#[tokio::test]
async fn detect_gemini_protocol_success() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1beta/models"))
.and(query_param("key", "AIzaSyBtest"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"models": [
{"name": "models/gemini-2.5-pro"},
{"name": "models/gemini-2.5-flash"}
]
})))
.mount(&mock_server)
.await;
let router = setup().await;
let (status, json) = detect(
&router,
json!({
"base_url": mock_server.uri(),
"api_key": "AIzaSyBtest",
"preferred_protocol": "gemini"
}),
)
.await;
assert_eq!(status, StatusCode::OK);
let data = &json["data"];
assert_eq!(data["protocol"], "gemini");
assert!(data["confidence"].as_u64().unwrap() >= 80);
let models = data["models"].as_array().unwrap();
// Prefix stripped
assert!(models.contains(&json!("gemini-2.5-pro")));
assert!(models.contains(&json!("gemini-2.5-flash")));
}
// ---------------------------------------------------------------------------
// All protocols fail → unknown
// ---------------------------------------------------------------------------
#[tokio::test]
async fn detect_protocol_all_fail_returns_unknown() {
let mock_server = MockServer::start().await;
// All endpoints return 404
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(404))
.mount(&mock_server)
.await;
let router = setup().await;
let (status, json) = detect(
&router,
json!({
"base_url": mock_server.uri(),
"api_key": "sk-unknown-key"
}),
)
.await;
assert_eq!(status, StatusCode::OK);
let data = &json["data"];
assert_eq!(data["protocol"], "unknown");
assert_eq!(data["confidence"], 0);
assert_eq!(data["suggestion"]["type"], "check_key");
}
// ---------------------------------------------------------------------------
// Auth failure detection (401)
// ---------------------------------------------------------------------------
#[tokio::test]
async fn detect_protocol_auth_failure_returns_check_key() {
let mock_server = MockServer::start().await;
// OpenAI endpoint returns 401
Mock::given(method("GET"))
.and(path("/models"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"error": {"message": "Invalid API key"}
})))
.mount(&mock_server)
.await;
// /v1/models also returns 401
Mock::given(method("GET"))
.and(path("/v1/models"))
.respond_with(ResponseTemplate::new(401))
.mount(&mock_server)
.await;
let router = setup().await;
let (status, json) = detect(
&router,
json!({
"base_url": mock_server.uri(),
"api_key": "invalid-key"
}),
)
.await;
assert_eq!(status, StatusCode::OK);
let data = &json["data"];
// Should detect a protocol (OpenAI likely) with check_key suggestion
assert!(data["confidence"].as_u64().unwrap() > 0);
assert_eq!(data["suggestion"]["type"], "check_key");
}
// ---------------------------------------------------------------------------
// URL fix variant detection
// ---------------------------------------------------------------------------
#[tokio::test]
async fn detect_openai_via_v1_variant() {
let mock_server = MockServer::start().await;
// /models returns 404
Mock::given(method("GET"))
.and(path("/models"))
.respond_with(ResponseTemplate::new(404))
.mount(&mock_server)
.await;
// /v1/models returns success
Mock::given(method("GET"))
.and(path("/v1/models"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [{"id": "gpt-4"}]
})))
.mount(&mock_server)
.await;
let router = setup().await;
let (status, json) = detect(
&router,
json!({
"base_url": mock_server.uri(),
"api_key": "sk-test"
}),
)
.await;
assert_eq!(status, StatusCode::OK);
let data = &json["data"];
assert_eq!(data["protocol"], "openai");
// fixed_base_url should be set when using /v1 variant
assert!(data["fixed_base_url"].is_string());
assert!(data["fixed_base_url"].as_str().unwrap().ends_with("/v1"));
}
// ---------------------------------------------------------------------------
// Multi-key testing
// ---------------------------------------------------------------------------
#[tokio::test]
async fn detect_with_multi_key_test() {
let mock_server = MockServer::start().await;
// /models returns success for any key
Mock::given(method("GET"))
.and(path("/models"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [{"id": "gpt-4"}]
})))
.mount(&mock_server)
.await;
let router = setup().await;
let (status, json) = detect(
&router,
json!({
"base_url": mock_server.uri(),
"api_key": "key1,key2,key3",
"test_all_keys": true
}),
)
.await;
assert_eq!(status, StatusCode::OK);
let data = &json["data"];
assert_eq!(data["protocol"], "openai");
let mkr = &data["multi_key_result"];
assert_eq!(mkr["total"], 3);
assert_eq!(mkr["details"].as_array().unwrap().len(), 3);
// All keys should be valid (mock returns 200 for any key)
assert_eq!(mkr["valid"], 3);
assert_eq!(mkr["invalid"], 0);
}
// ---------------------------------------------------------------------------
// Multi-key partial validity
// ---------------------------------------------------------------------------
#[tokio::test]
async fn detect_multi_key_partial_validity() {
let mock_server = MockServer::start().await;
// /models returns success only for "good-key"
Mock::given(method("GET"))
.and(path("/models"))
.and(header("Authorization", "Bearer good-key"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [{"id": "gpt-4"}]
})))
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/models"))
.and(header("Authorization", "Bearer bad-key"))
.respond_with(ResponseTemplate::new(401))
.mount(&mock_server)
.await;
let router = setup().await;
let (status, json) = detect(
&router,
json!({
"base_url": mock_server.uri(),
"api_key": "good-key,bad-key",
"test_all_keys": true
}),
)
.await;
assert_eq!(status, StatusCode::OK);
let data = &json["data"];
let mkr = &data["multi_key_result"];
assert_eq!(mkr["total"], 2);
assert_eq!(mkr["valid"], 1);
assert_eq!(mkr["invalid"], 1);
// Verify details are sorted by index
let details = mkr["details"].as_array().unwrap();
assert_eq!(details[0]["index"], 0);
assert!(details[0]["valid"].as_bool().unwrap());
assert_eq!(details[1]["index"], 1);
assert!(!details[1]["valid"].as_bool().unwrap());
}
// ---------------------------------------------------------------------------
// Preferred protocol takes priority
// ---------------------------------------------------------------------------
#[tokio::test]
async fn preferred_protocol_tested_first() {
let mock_server = MockServer::start().await;
// Only Anthropic endpoint works
Mock::given(method("GET"))
.and(path("/v1/models"))
.and(header("x-api-key", "test-key"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [{"id": "claude-3"}]
})))
.mount(&mock_server)
.await;
let router = setup().await;
let (status, json) = detect(
&router,
json!({
"base_url": mock_server.uri(),
"api_key": "test-key",
"preferred_protocol": "anthropic"
}),
)
.await;
assert_eq!(status, StatusCode::OK);
let data = &json["data"];
assert_eq!(data["protocol"], "anthropic");
}
// ---------------------------------------------------------------------------
// Single key → no multiKeyResult
// ---------------------------------------------------------------------------
#[tokio::test]
async fn single_key_no_multi_key_result() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/models"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [{"id": "gpt-4"}]
})))
.mount(&mock_server)
.await;
let router = setup().await;
let (status, json) = detect(
&router,
json!({
"base_url": mock_server.uri(),
"api_key": "sk-single-key",
"test_all_keys": true
}),
)
.await;
assert_eq!(status, StatusCode::OK);
let data = &json["data"];
// Single key → multi_key_result should be null
assert!(data["multi_key_result"].is_null());
}
// ---------------------------------------------------------------------------
// Timeout configuration
// ---------------------------------------------------------------------------
#[tokio::test]
async fn detect_protocol_with_custom_timeout() {
let mock_server = MockServer::start().await;
// Return success after a small delay (well within timeout)
Mock::given(method("GET"))
.and(path("/models"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(json!({"data": [{"id": "gpt-4"}]}))
.set_delay(std::time::Duration::from_millis(50)),
)
.mount(&mock_server)
.await;
let router = setup().await;
let (status, json) = detect(
&router,
json!({
"base_url": mock_server.uri(),
"api_key": "sk-test",
"timeout": 5000
}),
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(json["data"]["protocol"], "openai");
}
// ---------------------------------------------------------------------------
// Multi-protocol detection (detectedProtocols)
// ---------------------------------------------------------------------------
#[tokio::test]
async fn detect_multiple_protocols_reports_all_successes() {
let mock_server = MockServer::start().await;
// OpenAI probe: GET /models with Authorization header
Mock::given(method("GET"))
.and(path("/models"))
.and(header("Authorization", "Bearer multi-test-key"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [
{"id": "gpt-4"},
{"id": "gpt-3.5-turbo"}
]
})))
.mount(&mock_server)
.await;
// Anthropic probe: GET /v1/models with x-api-key header
Mock::given(method("GET"))
.and(path("/v1/models"))
.and(header("x-api-key", "multi-test-key"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [
{"id": "claude-sonnet-4-20250514"},
{"id": "claude-opus-4-20250514"}
]
})))
.mount(&mock_server)
.await;
let router = setup().await;
let (status, json) = detect(
&router,
json!({
"base_url": mock_server.uri(),
"api_key": "multi-test-key"
}),
)
.await;
assert_eq!(status, StatusCode::OK);
assert!(json["success"].as_bool().unwrap());
let data = &json["data"];
// Primary result is still the first successful probe in priority order (OpenAI)
assert_eq!(data["protocol"], "openai");
assert_eq!(data["success"], true);
// detectedProtocols contains both OpenAI and Anthropic
let detected = data["detectedProtocols"].as_array().unwrap();
assert!(
detected.len() >= 2,
"Expected at least 2 detected protocols, got {}",
detected.len()
);
let protocols: Vec<&str> = detected.iter().map(|d| d["protocol"].as_str().unwrap()).collect();
assert!(protocols.contains(&"openai"), "Expected openai in detectedProtocols");
assert!(
protocols.contains(&"anthropic"),
"Expected anthropic in detectedProtocols"
);
// Each entry should have confidence > 0
for entry in detected {
assert!(entry["confidence"].as_u64().unwrap() > 0);
}
}