feat: 增强SaaS后端功能与安全性

refactor: 重构数据库连接使用PostgreSQL替代SQLite
feat(auth): 增加JWT验证的audience和issuer检查
feat(crypto): 添加AES-256-GCM字段加密支持
feat(api): 集成utoipa实现OpenAPI文档
fix(admin): 修复配置项表单验证逻辑
style: 统一代码格式与类型定义
docs: 更新技术栈文档说明PostgreSQL
This commit is contained in:
iven
2026-03-31 00:12:53 +08:00
parent 4d8d560d1f
commit 44256a511c
177 changed files with 9731 additions and 948 deletions

View File

@@ -1,4 +1,6 @@
//! 集成测试 (Phase 1 + Phase 2)
//!
//! 所有测试通过全局 Mutex 串行执行,避免共享数据库导致的 UNIQUE 约束冲突和数据竞争。
use axum::{
body::Body,
@@ -9,8 +11,16 @@ use tower::ServiceExt;
const MAX_BODY_SIZE: usize = 1024 * 1024; // 1MB
/// 全局 Mutex 用于序列化所有集成测试
/// tokio::test 默认并行执行,但共享数据库要求串行访问
static INTEGRATION_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
async fn build_test_app() -> axum::Router {
use zclaw_saas::{config::SaaSConfig, db::init_memory_db, state::AppState};
let _ = tracing_subscriber::fmt()
.with_env_filter("error")
.with_test_writer()
.try_init();
use zclaw_saas::{config::SaaSConfig, db::init_test_db, state::AppState};
use axum::extract::ConnectInfo;
use std::net::SocketAddr;
@@ -18,7 +28,7 @@ async fn build_test_app() -> axum::Router {
std::env::set_var("ZCLAW_SAAS_DEV", "true");
std::env::set_var("ZCLAW_SAAS_JWT_SECRET", "test-secret-for-integration-tests-only");
let db = init_memory_db().await.unwrap();
let db = init_test_db().await.unwrap();
let mut config = SaaSConfig::default();
config.auth.jwt_expiration_hours = 24;
let state = AppState::new(db, config).expect("测试环境 AppState 初始化失败");
@@ -85,6 +95,7 @@ fn auth_header(token: &str) -> String {
#[tokio::test]
async fn test_register_and_login() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "testuser", "test@example.com").await;
assert!(!token.is_empty());
@@ -92,6 +103,7 @@ async fn test_register_and_login() {
#[tokio::test]
async fn test_register_duplicate_fails() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let body = json!({
@@ -123,6 +135,7 @@ async fn test_register_duplicate_fails() {
#[tokio::test]
async fn test_unauthorized_access() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let req = Request::builder()
@@ -137,6 +150,7 @@ async fn test_unauthorized_access() {
#[tokio::test]
async fn test_login_wrong_password() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
register_and_login(&app, "wrongpwd", "wrongpwd@example.com").await;
@@ -156,6 +170,7 @@ async fn test_login_wrong_password() {
#[tokio::test]
async fn test_full_authenticated_flow() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "fulltest", "full@example.com").await;
@@ -204,6 +219,7 @@ async fn test_full_authenticated_flow() {
#[tokio::test]
async fn test_providers_crud() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
// 注册 super_admin 角色用户 (通过直接插入角色权限)
let token = register_and_login(&app, "adminprov", "adminprov@example.com").await;
@@ -239,6 +255,7 @@ async fn test_providers_crud() {
#[tokio::test]
async fn test_models_list_and_usage() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "modeluser", "modeluser@example.com").await;
@@ -274,6 +291,7 @@ async fn test_models_list_and_usage() {
#[tokio::test]
async fn test_api_keys_lifecycle() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "keyuser", "keyuser@example.com").await;
@@ -309,6 +327,7 @@ async fn test_api_keys_lifecycle() {
#[tokio::test]
async fn test_relay_models_list() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "relayuser", "relayuser@example.com").await;
@@ -329,6 +348,7 @@ async fn test_relay_models_list() {
#[tokio::test]
async fn test_relay_chat_no_model() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "relayfail", "relayfail@example.com").await;
@@ -351,6 +371,7 @@ async fn test_relay_chat_no_model() {
#[tokio::test]
async fn test_relay_tasks_list() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "relaytasks", "relaytasks@example.com").await;
@@ -369,6 +390,7 @@ async fn test_relay_tasks_list() {
#[tokio::test]
async fn test_config_analysis_empty() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "cfguser", "cfguser@example.com").await;
@@ -389,6 +411,7 @@ async fn test_config_analysis_empty() {
#[tokio::test]
async fn test_config_seed_and_list() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "cfgseed", "cfgseed@example.com").await;
@@ -423,6 +446,7 @@ async fn test_config_seed_and_list() {
#[tokio::test]
async fn test_device_register_and_list() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "devuser", "devuser@example.com").await;
@@ -463,6 +487,7 @@ async fn test_device_register_and_list() {
#[tokio::test]
async fn test_device_upsert_on_reregister() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "upsertdev", "upsertdev@example.com").await;
@@ -516,6 +541,7 @@ async fn test_device_upsert_on_reregister() {
#[tokio::test]
async fn test_device_heartbeat() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "hbuser", "hbuser@example.com").await;
@@ -563,6 +589,7 @@ async fn test_device_heartbeat() {
#[tokio::test]
async fn test_device_register_missing_id() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "baddev", "baddev@example.com").await;
@@ -578,11 +605,12 @@ async fn test_device_register_missing_id() {
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
assert!(resp.status() == StatusCode::BAD_REQUEST || resp.status() == StatusCode::UNPROCESSABLE_ENTITY);
}
#[tokio::test]
async fn test_change_password() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "pwduser", "pwduser@example.com").await;
@@ -632,6 +660,7 @@ async fn test_change_password() {
#[tokio::test]
async fn test_change_password_wrong_old() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "wrongold", "wrongold@example.com").await;
@@ -655,6 +684,7 @@ async fn test_change_password_wrong_old() {
#[tokio::test]
async fn test_e2e_full_lifecycle() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
// 1. 注册
@@ -771,6 +801,7 @@ async fn test_e2e_full_lifecycle() {
#[tokio::test]
async fn test_config_sync() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "cfgsync", "cfgsync@example.com").await;
@@ -808,6 +839,7 @@ async fn test_config_sync() {
#[tokio::test]
async fn test_totp_setup_and_verify() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "totpuser", "totp@example.com").await;
@@ -825,7 +857,7 @@ async fn test_totp_setup_and_verify() {
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
assert!(body["otpauth_uri"].is_string());
assert!(body["secret"].is_string());
let secret = body["secret"].as_str().unwrap();
let _secret = body["secret"].as_str().unwrap();
// 2. Verify with wrong code → 400
let bad_verify = Request::builder()
@@ -868,6 +900,7 @@ async fn test_totp_setup_and_verify() {
#[tokio::test]
async fn test_totp_disabled_login_without_code() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "nototp", "nototp@example.com").await;
@@ -913,6 +946,7 @@ async fn test_totp_disabled_login_without_code() {
#[tokio::test]
async fn test_totp_disable_wrong_password() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "totpwrong", "totpwrong@example.com").await;
@@ -932,6 +966,7 @@ async fn test_totp_disable_wrong_password() {
#[tokio::test]
async fn test_config_diff() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "diffuser", "diffuser@example.com").await;
@@ -959,6 +994,7 @@ async fn test_config_diff() {
#[tokio::test]
async fn test_config_sync_push() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "syncpush", "syncpush@example.com").await;
@@ -987,6 +1023,7 @@ async fn test_config_sync_push() {
#[tokio::test]
async fn test_relay_retry_unauthorized() {
let _guard = INTEGRATION_TEST_LOCK.lock().unwrap();
let app = build_test_app().await;
let token = register_and_login(&app, "retryuser", "retryuser@example.com").await;