Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
//! 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!([]));
|
||||
}
|
||||
Reference in New Issue
Block a user