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

474 lines
16 KiB
Rust

//! Black-box integration tests for provider CRUD routes.
//!
//! Tests exercise the HTTP layer (request -> handler -> response) via
//! `tower::ServiceExt::oneshot`, without authentication middleware.
//! Auth protection is verified at the app-level E2E tests (task 3.9).
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 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_ENCRYPTION_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_ENCRYPTION_KEY),
model_fetch_service: ModelFetchService::new(provider_repo, TEST_ENCRYPTION_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, nomifun_db::Database) {
let db = init_database_memory().await.unwrap();
let state = build_state(&db);
(system_routes(state), db)
}
async fn body_json(resp: axum::response::Response) -> serde_json::Value {
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
serde_json::from_slice(&bytes).unwrap()
}
fn get_request(uri: &str) -> Request<Body> {
Request::builder().method("GET").uri(uri).body(Body::empty()).unwrap()
}
fn json_request(method: &str, uri: &str, body: serde_json::Value) -> Request<Body> {
Request::builder()
.method(method)
.uri(uri)
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&body).unwrap()))
.unwrap()
}
fn delete_request(uri: &str) -> Request<Body> {
Request::builder()
.method("DELETE")
.uri(uri)
.body(Body::empty())
.unwrap()
}
fn sample_create_body() -> serde_json::Value {
json!({
"platform": "anthropic",
"name": "Anthropic",
"base_url": "https://api.anthropic.com",
"api_key": "sk-ant-api03-test1234"
})
}
/// Create a provider and return (response_json, provider_id, fresh_router).
async fn create_one(db: &nomifun_db::Database) -> (serde_json::Value, String) {
let app = system_routes(build_state(db));
let resp = app
.oneshot(json_request("POST", "/api/providers", sample_create_body()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
let json = body_json(resp).await;
let id = json["data"]["id"].as_str().unwrap().to_string();
(json, id)
}
// ===========================================================================
// GET /api/providers — list
// ===========================================================================
#[tokio::test]
async fn list_providers_empty() {
let (app, _db) = setup().await;
let resp = app.oneshot(get_request("/api/providers")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert_eq!(json["success"], true);
assert_eq!(json["data"], json!([]));
}
#[tokio::test]
async fn list_providers_returns_plaintext_api_key() {
let (_app, db) = setup().await;
create_one(&db).await;
let app2 = system_routes(build_state(&db));
let resp = app2.oneshot(get_request("/api/providers")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
let providers = json["data"].as_array().unwrap();
assert_eq!(providers.len(), 1);
let api_key = providers[0]["api_key"].as_str().unwrap();
// Pre-launch: api_key is returned plaintext on the wire (encrypted at rest).
assert_eq!(api_key, "sk-ant-api03-test1234");
assert!(!api_key.contains("***"));
}
// ===========================================================================
// POST /api/providers — create
// ===========================================================================
#[tokio::test]
async fn create_provider_success() {
let (app, _db) = setup().await;
let resp = app
.oneshot(json_request("POST", "/api/providers", sample_create_body()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
let json = body_json(resp).await;
assert_eq!(json["success"], true);
let data = &json["data"];
assert!(data["id"].as_str().unwrap().starts_with("prov_"));
assert_eq!(data["platform"], "anthropic");
assert_eq!(data["name"], "Anthropic");
assert_eq!(data["base_url"], "https://api.anthropic.com");
assert_eq!(data["api_key"], "sk-ant-api03-test1234");
assert!(data["enabled"].as_bool().unwrap());
assert!(data["models"].as_array().unwrap().is_empty());
assert!(data["created_at"].as_i64().unwrap() > 0);
assert!(data["updated_at"].as_i64().unwrap() > 0);
}
#[tokio::test]
async fn create_provider_with_supplied_id() {
let (app, _db) = setup().await;
let body = json!({
"id": "caller-id-123",
"platform": "openai",
"name": "OpenAI",
"base_url": "https://api.openai.com",
"api_key": "sk-test",
"model_enabled": {"gpt-4": true, "gpt-3.5": false}
});
let resp = app.oneshot(json_request("POST", "/api/providers", body)).await.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
let json = body_json(resp).await;
let data = &json["data"];
assert_eq!(data["id"], "caller-id-123");
assert_eq!(data["api_key"], "sk-test");
assert_eq!(data["model_enabled"]["gpt-4"], true);
assert_eq!(data["model_enabled"]["gpt-3.5"], false);
}
#[tokio::test]
async fn create_provider_with_duplicate_id_returns_conflict() {
let (_app, db) = setup().await;
let body = json!({
"id": "dup-id",
"platform": "openai",
"name": "OpenAI",
"base_url": "https://api.openai.com",
"api_key": "sk-test"
});
let app1 = system_routes(build_state(&db));
let resp = app1
.oneshot(json_request("POST", "/api/providers", body.clone()))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
let app2 = system_routes(build_state(&db));
let resp = app2
.oneshot(json_request("POST", "/api/providers", body))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::CONFLICT);
}
#[tokio::test]
async fn create_provider_with_invalid_id_rejected() {
let (app, _db) = setup().await;
let body = json!({
"id": "bad/slash",
"platform": "openai",
"name": "OpenAI",
"base_url": "https://api.openai.com",
"api_key": "sk-test"
});
let resp = app.oneshot(json_request("POST", "/api/providers", body)).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn create_provider_with_optional_fields() {
let (app, _db) = setup().await;
let body = json!({
"platform": "bedrock",
"name": "AWS Bedrock",
"base_url": "https://bedrock.us-east-1.amazonaws.com",
"api_key": "test-key-abcd",
"models": ["anthropic.claude-3-sonnet"],
"enabled": false,
"capabilities": [{"type": "text"}, {"type": "vision", "is_user_selected": true}],
"context_limit": 200000,
"bedrock_config": {
"auth_method": "accessKey",
"region": "us-east-1",
"access_key_id": "AKIA...",
"secret_access_key": "secret"
}
});
let resp = app.oneshot(json_request("POST", "/api/providers", body)).await.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
let json = body_json(resp).await;
let data = &json["data"];
assert!(!data["enabled"].as_bool().unwrap());
assert_eq!(data["models"].as_array().unwrap().len(), 1);
assert_eq!(data["capabilities"].as_array().unwrap().len(), 2);
assert_eq!(data["context_limit"], 200000);
assert_eq!(data["bedrock_config"]["auth_method"], "accessKey");
assert_eq!(data["bedrock_config"]["region"], "us-east-1");
}
#[tokio::test]
async fn create_provider_missing_platform() {
let (app, _db) = setup().await;
let body = json!({
"name": "Test",
"base_url": "https://api.example.com",
"api_key": "sk-test"
});
let resp = app.oneshot(json_request("POST", "/api/providers", body)).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn create_provider_missing_name() {
let (app, _db) = setup().await;
let body = json!({
"platform": "openai",
"base_url": "https://api.example.com",
"api_key": "sk-test"
});
let resp = app.oneshot(json_request("POST", "/api/providers", body)).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn create_provider_missing_base_url() {
let (app, _db) = setup().await;
let body = json!({
"platform": "openai",
"name": "Test",
"api_key": "sk-test"
});
let resp = app.oneshot(json_request("POST", "/api/providers", body)).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn create_provider_missing_api_key() {
let (app, _db) = setup().await;
let body = json!({
"platform": "openai",
"name": "Test",
"base_url": "https://api.example.com"
});
let resp = app.oneshot(json_request("POST", "/api/providers", body)).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn create_provider_invalid_url() {
let (app, _db) = setup().await;
let body = json!({
"platform": "openai",
"name": "Test",
"base_url": "not-a-url",
"api_key": "sk-test"
});
let resp = app.oneshot(json_request("POST", "/api/providers", body)).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
// ===========================================================================
// PUT /api/providers/{id} — update
// ===========================================================================
#[tokio::test]
async fn update_provider_name() {
let (_app, db) = setup().await;
let (_, id) = create_one(&db).await;
let app2 = system_routes(build_state(&db));
let resp = app2
.oneshot(json_request(
"PUT",
&format!("/api/providers/{id}"),
json!({"name": "New Name"}),
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert_eq!(json["data"]["name"], "New Name");
assert_eq!(json["data"]["platform"], "anthropic");
}
#[tokio::test]
async fn update_provider_api_key_returns_plaintext() {
let (_app, db) = setup().await;
let (_, id) = create_one(&db).await;
let app2 = system_routes(build_state(&db));
let resp = app2
.oneshot(json_request(
"PUT",
&format!("/api/providers/{id}"),
json!({"api_key": "new-key-abcdefgh"}),
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
let api_key = json["data"]["api_key"].as_str().unwrap();
assert_eq!(api_key, "new-key-abcdefgh");
}
#[tokio::test]
async fn update_provider_nonexistent() {
let (app, _db) = setup().await;
let resp = app
.oneshot(json_request("PUT", "/api/providers/nonexistent", json!({"name": "X"})))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
// ===========================================================================
// DELETE /api/providers/{id}
// ===========================================================================
#[tokio::test]
async fn delete_provider_success() {
let (_app, db) = setup().await;
let (_, id) = create_one(&db).await;
let app2 = system_routes(build_state(&db));
let resp = app2
.oneshot(delete_request(&format!("/api/providers/{id}")))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert_eq!(json["success"], true);
}
#[tokio::test]
async fn delete_provider_then_list_excludes_deleted() {
let (_app, db) = setup().await;
let (_, id) = create_one(&db).await;
let app2 = system_routes(build_state(&db));
let resp = app2
.oneshot(delete_request(&format!("/api/providers/{id}")))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let app3 = system_routes(build_state(&db));
let resp = app3.oneshot(get_request("/api/providers")).await.unwrap();
let json = body_json(resp).await;
assert_eq!(json["data"], json!([]));
}
#[tokio::test]
async fn delete_provider_nonexistent() {
let (app, _db) = setup().await;
let resp = app.oneshot(delete_request("/api/providers/nonexistent")).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
// ===========================================================================
// Full CRUD flow
// ===========================================================================
#[tokio::test]
async fn full_crud_flow() {
let (_app, db) = setup().await;
// 1. Create
let (create_json, id) = create_one(&db).await;
assert_eq!(create_json["data"]["platform"], "anthropic");
// 2. List — should contain one
let app2 = system_routes(build_state(&db));
let resp = app2.oneshot(get_request("/api/providers")).await.unwrap();
let list_json = body_json(resp).await;
assert_eq!(list_json["data"].as_array().unwrap().len(), 1);
// 3. Update
let app3 = system_routes(build_state(&db));
let resp = app3
.oneshot(json_request(
"PUT",
&format!("/api/providers/{id}"),
json!({"name": "Updated", "enabled": false}),
))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let update_json = body_json(resp).await;
assert_eq!(update_json["data"]["name"], "Updated");
assert!(!update_json["data"]["enabled"].as_bool().unwrap());
// 4. Verify update via list
let app4 = system_routes(build_state(&db));
let resp = app4.oneshot(get_request("/api/providers")).await.unwrap();
let list_json = body_json(resp).await;
assert_eq!(list_json["data"][0]["name"], "Updated");
// 5. Delete
let app5 = system_routes(build_state(&db));
let resp = app5
.oneshot(delete_request(&format!("/api/providers/{id}")))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
// 6. Verify deleted
let app6 = system_routes(build_state(&db));
let resp = app6.oneshot(get_request("/api/providers")).await.unwrap();
let list_json = body_json(resp).await;
assert_eq!(list_json["data"], json!([]));
}