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:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user